Running Meteor on docker


#1

Hi all

Which docker approach / image would u use for production hosting using latest meteor?

Anyone know if a guide that works with latest meteor?

Best
Jesper


#2

I use the base node image that is the same as the version of meteor you are developing upon.
I have a bash script that bundles up my app and then copies the bundle, some setting files and a startup script to a new docker image. I can then use this image in a docker service in my production docker swarm.

When I looked for meteor docker images to use, most were trying to download meteor to build the bundle and also act as a production image. I already had a dev environment so do not need a dockerized build environment. I only want a slim image geared for deployment. Meteor is not installed I’m my docker image at all.


#3

Thx @pmcochrane

That sounds completely perfect and just what I need.

Would you care to share a repo or just script and docker file (as I’m not that great at docker nor bash yet).

Best
Jesper


#4

Sorry no repo to share. Best I can do is to share a few files here for now. They may or may not be of use. I’ll struggle to find the time to explain every detail in them as there are a lot of stuff customised for use in my app. I’ll start with the docker config for building the docker image

The docker file:

FROM node:8.11.4-slim
# meteor 1.5.1   === Node 4.8.4
# meteor 1.6     === Node 8.8.1
# meteor 1.6.0.1 === Node 8.9.3
# meteor 1.6.1   === Node 8.9.4
# meteor 1.6.1.1 === Node 8.11.1
# meteor 1.6.1.3 === Node 8.11.3
# meteor 1.7     === Node 8.11.2
# meteor 1.7.0.2 === Node 8.11.3
# meteor 1.7.0.5 === Node 8.11.4

# Default values for Meteor environment variables
ENV APP_DIR=/meteor						\
	ROOT_URL=http://localhost			\
	MAIL_URL=http://localhost:25		\
	MONGO_DB=meteor						\
	MONGO_USER=node						\
	MONGO_PASSWORD=123456				\
	MONGO_SERVER=127.0.0.1:27017		\
	MONGO_REPLICA_SET_NAME=retis_mongo	\
	MONGO_REPLICA_SET_URL=?replicaSet=${MONGO_REPLICA_SET_NAME}	\
	MONGO_URL="mongodb://${MONGO_USER}:${MONGO_PASSWORD}@${MONGO_SERVER}/${MONGO_DB}${MONGO_REPLICA_SET_URL}"	\
	MONGO_OPLOG_URL="mongodb://${MONGO_USER}:${MONGO_PASSWORD}@${MONGO_SERVER}/local${MONGO_REPLICA_SET_URL}&authSource=admin"	\
	PORT=8400							\
	NODE_ENV=production
EXPOSE $PORT

HEALTHCHECK --interval=12s --timeout=12s --start-period=30s CMD node /meteor/healthcheck.js

# Install as root (otherwise node-gyp gets compiled as nobody)
USER root
WORKDIR $APP_DIR/programs/server/

# Copy bundle and scripts to the image APP_DIR
# COPY scripts/ $APP_DIR	# This is now done at bundle time to reduce a layer in the docker image
COPY ./bundle/ $APP_DIR

# the install command for debian
RUN echo "Installing the node modules..." \
	&& npm install -g node-gyp \
    && npm install --production --silent \
	&& echo \
	&& echo \
	&& echo \
	&& echo "Updating file permissions for the node user..." \
	&& chmod -R 750 $APP_DIR \
	&& chown -R node.node $APP_DIR

# Ensure the permissions are correct on the app dir
# RUN  chmod -R 750 $APP_DIR \
# 	&& chown -R node.node $APP_DIR

# start the app
WORKDIR $APP_DIR/
USER node
CMD ["/meteor/startapp.sh"]

The startapp.sh script for the docker image:

#!/bin/bash

set -e

version=$(cat programs/server/package.json | grep -i version)
replace='  "version": "'
replace2='",'
stage1=${version#$replace};
version_number=${stage1%$replace2}
export RETIS_VERSION=$version_number
echo "########################################################################"
echo "### ReTIS app version: ${RETIS_VERSION}"
echo "########################################################################"


# Set a delay to wait to start the Node process
if [[ $STARTUP_DELAY ]]; then
  echo "Delaying startup for $STARTUP_DELAY seconds..."
  sleep $STARTUP_DELAY
fi


export CONTAINER_IP_ADDRESS=$(hostname -i)
export CONTAINER_USER=$(grep $UID /etc/passwd | cut -d : -f1)

# Start app
cd $APP_DIR
echo "=> Starting app:"
echo "===> root_url: ${ROOT_URL}:${PORT}/"
echo "===> port ${PORT}"
echo "===> mail_url: ${MAIL_URL}"

# Expand the mongo_url secret into an environment variable
if [ -f /run/secrets/retis_mongo_url ]; then
	theMongoUrl=$(cat /run/secrets/retis_mongo_url)
	export MONGO_URL=${theMongoUrl}
	echo "===> MONGO_URL: (from secret) ${theMongoUrl}"  | sed -E 's/(.*mongouser:).*@/\1XXXXXXXXXXXXX@/'
	# echo "===> MONGO_URL: (from secret) ${theMongoUrl}"

	export MONGO_DB=$(echo ${MONGO_URL} | sed -E 's/(.*mongouser:).*@/\1XXXXXXXXXXXXX@/' | sed -E "s/(.*.nhs.uk:27017\/)(.*)(\?.*)/\2/")
	echo "===> MONGO_DB: ${MONGO_DB}"

	export MONGO_USER=$(echo ${MONGO_URL} | sed -E 's/(.*mongouser:).*@/\1XXXXXXXXXXXXX@/' | sed -E "s/(mongodb:\/\/)(.*)(:.*)/\2/") | sed -E "s/(mongouser)(.*)/\1/"
	echo "===> MONGO_USER: ${MONGO_USER}"

	export MONGO_SERVER=$(echo ${MONGO_URL} | sed -E 's/(.*mongouser:).*@/\1XXXXXXXXXXXXX@/' | sed -E "s/(.*\@)(.*)(\/.*)/\2/")
	echo "===> MONGO_SERVER: ${MONGO_SERVER}"

	export MONGO_REPLICA_SET_NAME=$(echo ${MONGO_URL} | sed -E 's/(.*mongouser:).*@/\1XXXXXXXXXXXXX@/' | sed -E "s/(.*?replicaSet=)(.*)/\2/")
	echo "===> MONGO_REPLICA_SET_NAME: ${MONGO_REPLICA_SET_NAME}"

else
	unset MONGO_URL
	unset MONGO_DB
fi
echo "===> Database: Using \"${MONGO_DB}\" on \"${MONGO_SERVER}\" as the user \"${MONGO_USER}\""

# Expand the mongo_oplog_url secret into an environment variable
if [ -f /run/secrets/retis_mongo_oplog_url ]; then
	theOplogUrl=$(cat /run/secrets/retis_mongo_oplog_url)
	export MONGO_OPLOG_URL=${theOplogUrl}
	echo "===> MONGO_OPLOG_URL: (from secret) ${theOplogUrl}" | sed -E 's/(.*oplogreader:).*@/\1XXXXXXXXXXXXX@/'
	# echo "===> MONGO_OPLOG_URL: (from secret) ${theOplogUrl}"
else
	unset MONGO_OPLOG_URL
fi

# if [ -f /run/secrets/retis_settings.json ]; then
# 	echo "===> Settings:"
# 	cat /run/secrets/retis_settings.json
# fi

ROOT_URL=${ROOT_URL}:${PORT}/ METEOR_SETTINGS=$(cat /run/secrets/retis_settings.json) node main.js

The Health check script:

var http = require("http");

var options = {
    host : "localhost",
    port : "8400",
    timeout : 5000
};

var request = http.request(options, (res) => {
    console.log(`STATUS: ${res.statusCode}`);
    if (res.statusCode == 200) {
        process.exit(0);
    }
    else {
        process.exit(1);
    }
});

request.on('error', function(err) {
    console.log('ERROR');
    process.exit(1);
});

request.end();

#5

The following script is how to build your meteor app into a docker image:

#!/bin/bash
#
port=8400
mongodbname=retis_testing
settingsFile=./server/settings/dev.testing.json
numberOfImagesToKeep=10

source ./retis_includes.bash

# ./setup_mongo_url.bash retis_pmc; ROOT_URL=http://retismeteord1.luht.scot.nhs.uk:8400/ MAIL_URL=smtp://localhost meteor build ./.dist/ --directory --architecture os.linux.x64_64 --mobile-settings server/settings/dev.pmc.json
usage="$0 [tag: devel/testing/uat] [param-reuse-existing-bundle]"
if [ $# -lt 1 ]; then
	echored "Usage: ${usage}"
	echo "     eg. ./createDockerImage.bash devel"
	echo -e "     This will rebuild the source and create a docker image tagged as ${YELLOW}devel-X.Y.Z${NC} and ${YELLOW}devel-latest${NC}"
	exit 1
fi


timeStart=$(date +%H:%M:%S)

# Extract the tag for the image from the command line
tag=$1
parameterOk=0;
if [ $1 = "devel" ]; then
	parameterOk=1
fi
if [ $1 = "testing" ]; then
	parameterOk=1
fi
if [ $parameterOk -eq 0 ]; then
	echored "Usage: ${usage}"
	echo "    $1 is not a valid parameter"
	exit 2
fi;
shift

# Any other parameter will reuse the existing meteor bundle
createBundle=1
if [ $# -gt 0 ]; then
	createBundle=0
	shift
fi


. /shares/secure.files/mongodb_credentials

dockerRegistry=retismeteord1:5000
imageTag=$dockerRegistry/retis/retis
dockerFolder=.dockerimage
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",\ ]//g')

echo -e "#################################################################"
echo -e "### Build & deploy ReTIS to a Docker image"
echo -e "### Version: ${LIGHTGRAY}${PACKAGE_VERSION}${NC} Tag: ${LIGHTGRAY}${tag}${NC}"
echo -e "#################################################################"
echogreen "==================> $timeStart Starting deployment process ==============================="

export ROOT_URL=http://localhost
export MAIL_URL=smtp://localhost:25
export MONGO_DB=$mongodbname
export MONGO_URL="mongodb://$MONGO_USER:$MONGO_PASSWORD@$MONGO_SERVER/$MONGO_DB"
echoyellow "- Using $MONGO_DB on $MONGO_SERVER as the user $MONGO_USER"
echoyellow "- Working directory: `pwd`"


########################################################################################
if [ $createBundle -gt 0 ]; then

	echoyellow "- Closing any development meteor instances..."
	/etc/nfsFiles/bin/devel-killMyMeteor kill
	sleep 3

	if [ -d ${dockerFolder}/bundle ]; then
		echoyellow "- Removing an existing build bundle to make room for the new one..."
		rm -rf ${dockerFolder}/bundle
	fi


	echoyellow "- Cleaning up build environment of dev stuff..."
	rm -r .meteor/local/dev_bundle*
	rm -rf .meteor/local/build
	rm -rf .meteor/local/bundler-cache
	rm -rf .meteor/local/dev_bundle

	if [ -d node_modules ]; then
		echoyellow "- Caching development node_modules folder..."
		if [ -d .node_modules.dev ]; then
			echoyellow "-- removing leftover dev node_modules backup folder...a previous build must have failed"
			rm -rf .node_modules.dev
		fi
		echoyellow "-- backing up the dev node_modules folder..."
		mv node_modules .node_modules.dev
		if [ -f "./package-lock.json" ]; then
			echoyellow "-- backing up the dev package-lock.json file..."
			mv package-lock.json .node_modules.dev
		fi
		echoyellow "-- clearing the npm cache..."
		meteor npm cache clear --force


	fi

	echoyellow "- Installing production node modules..."
	# meteor npm install >/dev/null
	meteor npm install --production >/dev/null
	meteor npm install @angular/compiler-cli typescript --save-exact

	timeStartMeteor=$(date +%H:%M:%S)
	echogreen "==================> $timeStartMeteor Starting Meteor Build ==============================="
	meteor npm list --depth=0 2>/dev/null
	echoyellow "- Building the ReTIS node app for port $port..."
	echo NODE_OPTIONS="--max_old_space_size=4096" ROOT_URL=${ROOT_URL}:${PORT}/ MAIL_URL=${MAIL_URL} PORT=$port AOT=1 meteor build ${dockerFolder}/ --directory --architecture os.linux.x86_64 --mobile-settings $settingsFile
	# debug mode - no minify
	# SKIP_LEGACY_COMPILATION=0 NODE_OPTIONS="--max_old_space_size=4096" ROOT_URL=${ROOT_URL}:${PORT}/ MAIL_URL=${MAIL_URL} PORT=$port AOT=1 meteor build --debug ${dockerFolder}/ --directory --architecture os.linux.x86_64 --mobile-settings $settingsFile
	# production mode
	SKIP_LEGACY_COMPILATION=0 NODE_OPTIONS="--max_old_space_size=4096" ROOT_URL=${ROOT_URL}:${PORT}/ MAIL_URL=${MAIL_URL} PORT=$port AOT=1 meteor build ${dockerFolder}/ --directory --architecture os.linux.x86_64 --mobile-settings $settingsFile
	retval=$?

	echoyellow "- Remove 'extra' npm files from the bundle..."
	# if [ -d ${dockerFolder}/bundle/programs/server/npm/node_modules ]; then
	# 	rm -rf ${dockerFolder}/bundle/programs/server/npm/node_modules
	# fi
	if [ -d ${dockerFolder}/bundle/programs/server/assets/app/node_modules ]; then
	 	rm -rf ${dockerFolder}/bundle/programs/server/assets/app/node_modules
	fi
	if [ -d ${dockerFolder}/bundle/programs/web.browser/app/node_modules ]; then
		rm -rf ${dockerFolder}/bundle/programs/web.browser/app/node_modules
	fi

	dirToClean=${dockerFolder}/bundle/programs/server/npm/node_modules
	echoyellow "- Remove unneccessary files from the node_modules in the bundle: ${dirToClean}"
	echo "Size before cleaning: $(du -h ${dirToClean} | tail --lines=1)"
	#	package.json package-lock.json yarn.lock .npmignore 			\
	for ext in README \*.md  \*.MD  \*.markdown \*.mkd \*.xlsx               	\
		.idea coverage.html coverage.json index.html CHANGES LICENSE			\
		\*.png \*.jpg
	do
		echo -n "Removing ${dirToClean}/${ext} files:"
		find "${dirToClean}" -name "$ext" -exec du -sch {} + | tail --lines=1
		# find "${dirToClean}" -name "$ext"
		find "${dirToClean}" -name "$ext" -exec rm {} +
		find "${dirToClean}" -name "$ext" -type d -exec rm -rf {} +
	done
	# Remove explicit folders
	for folder in showdown/test \
		domino/test soap/coverage hammerjs/tests \
		meteor/zardak_soap/node_modules/soap/node_modules/ejs/test \
		meteor/cosmos_browserify/node_modules/browserify/test \
		meteor/cosmos_browserify/node_modules/browserify/example \
		meteor/cosmos_browserify/node_modules/browserify/node_modules/crypto-browserify/node_modules/public-encrypt/node_modules/parse-asn1/test \
		meteor/cosmos_browserify/node_modules/browserify/node_modules/stream-http/test \
		xhr2/test
	do
		echo "Removing ${dirToClean}/${folder} folder:"
		find "${dirToClean}" -wholename "${dirToClean}/${folder}" -type d -exec rm -rf {} +
	done
	echo "Size after cleaning: $(du -h ${dirToClean} | tail --lines=1)"


	echoyellow "- Reverting to dev node_modules..."
	if [ -d .node_modules.production ]; then
		echoyellow "-- Removing leftover node_modules.production from a previous build..."
		rm -rf .node_modules.production
	fi
	echoyellow "-- Moving production node_modules to node_modules.production..."
	mv ./node_modules .node_modules.production
	echoyellow "-- Deleting production package-lock.json..."
	mv ./package-lock.json .node_modules.production/
	if [ -d .node_modules.dev ]; then
		echoyellow "-- Reinstalling the development node modules folder..."
		mv .node_modules.dev node_modules
		if [ -f "./node_modules/package-lock.json" ]; then
			echoyellow "-- Reinstalling the development package-lock.json..."
			mv ./node_modules/package-lock.json .
		fi
		echoyellow "-- clearing the npm cache..."
		meteor npm cache clear --force
	fi

	if [ $retval -ne 0 ]; then
		echored "############################################################"
		echored "ERROR: Failed to build the meteor bundle. FAILING..."
		echored "############################################################"
		exit $retval
	fi

	if [ -d ${dockerFolder}/.meteor ]; then
		rm -rf ${dockerFolder}/.meteor
	fi

else
	echoyellow "- Reusing previously bundled application...${PACKAGE_VERSION}"
fi

########################################################################################
timeStartDocker=$(date +%H:%M:%S)
echogreen "==================> $timeStartDocker Starting docker image build ==============================="

echoyellow -n "- Updating the app to the next patch version..."
meteor npm --no-git-tag-version version patch
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",\ ]//g')
(cd ${dockerFolder}/bundle/programs/server && meteor npm --no-git-tag-version version $PACKAGE_VERSION)

echoyellow "- Copying settings to the bundle..."
if [ ! -d ${dockerFolder}/bundle/programs/server/settings ]; then
	mkdir ${dockerFolder}/bundle/programs/server/settings
fi
cp  $settingsFile ${dockerFolder}/bundle/programs/server/settings/settings.json

echoyellow "- Copying the docker startup scripts to the meteor bundle folder..."
cp .dockerimage/scripts/* .dockerimage/bundle/

echoyellow "- Building the docker image...${imageTag}:${PACKAGE_VERSION}"
cd ${dockerFolder}
docker build --squash -t ${imageTag}:${PACKAGE_VERSION} -t ${imageTag}:${tag}-latest -t ${imageTag}:${tag}-${PACKAGE_VERSION} .
retval=$?
if [ $retval -ne 0 ]; then
	echored "###############################################################################################"
	echored "ERROR: Failed to build the docker image. FAILING..."
	echored "###############################################################################################"
	exit $retval
fi

timePushDocker=$(date +%H:%M:%S)

if [ $numberOfImagesToKeep -gt 0 ]; then
	echogreen  "==============================================================================================================="
	echoyellow "Removing out of date image builds - keeping ${numberOfImagesToKeep} copies"
	echoyellow "---------------------------------------------------------------------------------------------------------------"
	docker rmi $(docker image ls | grep "${imageTag}   2." | tail -n +${numberOfImagesToKeep} | awk '{print $3}')
fi


echogreen "==================> $timePushDocker Pushing docker images to registry ==============================="
echoyellow "- Uploading docker image to registry...${imageTag}:${PACKAGE_VERSION}"
docker push ${imageTag}
retval=$?
if [ $retval -ne 0 ]; then
	echored "####################################################################################################"
	echored "ERROR: Failed to push the docker image ${imageTag} to the registry. FAILING..."
	echored "####################################################################################################"
	exit $retval
fi
git tag -a v${PACKAGE_VERSION} -m "Tagged v${PACKAGE_VERSION}"

echoyellow "- Cleansing untagged docker images..."
docker rmi $(docker images | awk '/^<none>/ {print $3}')

echoyellow "Docker images:"
docker image ls | grep ${imageTag}

timeComplete=$(date +%H:%M:%S)
echogreen "==================> $timeComplete Complete ==============================="
echo -e "Build Summary: Version: ${LIGHTGRAY}${PACKAGE_VERSION}${NC} Tag: ${LIGHTGRAY}${tag}${NC}"
echo -e "NPM inst: ${LIGHTGRAY}${timeStart}${NC} -> ${LIGHTGRAY}${timeStartMeteor}${NC}"
echo -e "Bundling: ${LIGHTGRAY}${timeStartMeteor}${NC} -> ${LIGHTGRAY}${timeStartDocker}${NC}"
echo -e "Imaging:  ${LIGHTGRAY}${timeStartDocker}${NC} -> ${LIGHTGRAY}${timePushDocker}${NC}"
echo -e "Pushing:  ${LIGHTGRAY}${timePushDocker}${NC} -> ${LIGHTGRAY}${timeComplete}${NC}"
echogreen "Complete"

I can’t really describe how to setup your docker swarm but the above image will be built and pushed to a private docker registry for deployment.


#6

The script to startup the docker swarm service is as follows:

version: "3.3"
# This service is intended to be deployed using the 'deployService.bash' script

# Define the external networks that this service requires
networks:
  retis-proxy_network:
    driver: overlay
    external: true

services:
  #-------------------------------------------------------------------------------------
  # Setup retis system on 8300 - container runs internally on port 8400
  #-------------------------------------------------------------------------------------
  retis:
    image: ${LOCAL_DOCKER_REGISTRY}:5000/retis/retis:devel-latest
    networks:
      - retis-proxy_network
    environment:
      - ROOT_URL=https://retis.some.where
      - PORT=8400
      - MAIL_URL=smtp://mailserver.dns.name:25
      - HTTP_FORWARDED_COUNT=0
      # - MONGO_URL=xxxx   This is passed in via a secret
      # - MONGO_OPLOG_URL=xxxx   This is passed in via a secret
    secrets:
      - retis_mongo_url
      - retis_mongo_oplog_url
      - retis_settings.json
    deploy:
      labels:
        - traefik.enable=true
        - traefik.docker.network=retis-proxy_network
        - traefik.backend=-
        - traefik.retis.frontend.rule=Host:retis.some.where
        - traefik.retis.frontend.entryPoints=http,https
        - traefik.retis.frontend.redirect.permanent=true
        - traefik.retis.frontend.redirect.regex=^(http|https)://(retis|retis.some.where)/(.*)
        - traefik.retis.frontend.redirect.replacement=https://retis.some.where/$$3
        - traefik.retis.protocol=http
        - traefik.retis.port=8400
      replicas: 1
      placement:
        constraints:
          - node.role == worker
          - node.labels.retisServerType == development
      resources:
        limits:
          memory: 4G
          cpus: '1'


#-------------------------------------------------------------------------------------
# Setup the secrets
#-------------------------------------------------------------------------------------
secrets:
  retis_mongo_url:
    external:
      name: retis_devel_mongo_url
  retis_mongo_oplog_url:
    external:
      name: retis_devel_mongo_oplog_url
  retis_settings.json:
    external:
      name: retis_devel_settings.json

  retis_postgres_readonly_password:
    external:
      name: retis_devel_postgres_readonly_password
  retis_postgres_write_password:
    external:
      name: retis_devel_postgres_write_password

  retis_https_htpasswd:
    external:
      name: retis_https_htpasswd
  retis_ssl_cert.pem:
    external:
      # name: retis_selfsigned_ssl_cert.pem
      name: retis_luht_cert.pem
  retis_ssl_key.pem:
    external:
      # name: retis_selfsigned_ssl_key.pem
      name: retis_luht_key.pem

  dh4096.pem:
    external:
      name: dh4096.pem

#7

:smile:

THANKS for sharing @pmcochrane. I’m gonna try this out. It really a great help.

You are taking care of quite a lot “around” just building which also is really great to learn.

Thanks again,
Jesper


#8

Take a look at Meteor Up too:


#9

Thanks @elie

I did look at mup but two things made me stop:

  1. It seems to only support 1.6 (and lower)
    http://meteor-up.com/docs.html#meteor-support
  2. Seems to target deploying to specific hosts and I couldnt figure out if it would allow me to build a docker image to deploy anywhere.

best
jesper


#10

I deployed my 1.7 app this week and it works fine. It’s still maintained well and being improved.

I’ve deployed to AWS Linux and Ubuntu from a mac and never had issues there. Pretty sure it supports the platforms you need.


#11

Thanks Elie

A few quick questions:

Are you using docker to build your solution or are you using it to create a docker image ready to push to any docker hosting?

Would you share you script/dockerfile?

Seems to support only 1.6 and lower?
http://meteor-up.com/docs.html#meteor-support

best
Jesper


#12

It works for meteor 1.7. I’ve tested it. It’s likely just out of date documentation.

On my phone right now, but if you look through the github repo you’ll see how they use docker.

It’s a fully deployment solution so if you’re just looking for the docker part I assume the both comments are more helpful. A few different docker images exist. Aberbix is one of them.


#13

See Meteor Docker image for multistage Docker builds. There’s no “best” image, it depends on your needs:

  • do you want to build your app inside of the controlled Docker environment or outside in your host/on your development machine
  • are you using Docker for development, deployment or both
  • do you care about the size of the final image

That thread addresses many of these issues.


#14

Hi @pmcochrane,

I got it to work. Thanks again. I stripped it down to make it simple and working and I’ll add back lots of your good stuff that optimizes the bundle later.

Btw in your build script when copying files you reference .dockerImage and not the variable. I also removed set -e from startapp.sh as it exited the script for unknown reasons. Finally I think there was something with port and PORT

For any other trying this out - this is my shaved down version:

/dockerstuff/Dockerfile (thats in the project folder)

FROM node:8.11.4-slim
# meteor 1.5.1   === Node 4.8.4
# meteor 1.6     === Node 8.8.1
# meteor 1.6.0.1 === Node 8.9.3
# meteor 1.6.1   === Node 8.9.4
# meteor 1.6.1.1 === Node 8.11.1
# meteor 1.6.1.3 === Node 8.11.3
# meteor 1.7     === Node 8.11.2
# meteor 1.7.0.2 === Node 8.11.3
# meteor 1.7.0.5 === Node 8.11.4

ENV APP_DIR=/meteor						\
	ROOT_URL=http://localhost			\
	MAIL_URL=http://localhost:25		\
	MONGO_URL="mongodb://user:pass@hostname:31681/databasename"	\
	PORT=8400							\
	NODE_ENV=production
EXPOSE $PORT

# Install as root (otherwise node-gyp gets compiled as nobody)
USER root
WORKDIR $APP_DIR/programs/server/

# Copy bundle and scripts to the image APP_DIR
COPY ./bundle/ $APP_DIR

# the install command for debian
RUN echo "Installing the node modules..." \
	&& npm install -g node-gyp \
    && npm install --production --silent \
	&& echo \
	&& echo \
	&& echo \
	&& echo "Updating file permissions for the node user..." \
	&& chmod -R 750 $APP_DIR \
	&& chown -R node.node $APP_DIR

# start the app
WORKDIR $APP_DIR/
USER node
CMD ["/meteor/startapp.sh"]

and then there is the

/dockerstuff/startapp.sh (this will be copied to image and run in the container)

#!/bin/bash

cd $APP_DIR

echo "===> root_url: ${ROOT_URL}:${PORT}/"
echo "===> port ${PORT}"
echo "===> mail_url: ${MAIL_URL}"
echo "===> Database: ${MONGO_URL}"

ROOT_URL=${ROOT_URL}:${PORT} METEOR_SETTINGS=$(cat programs/server/settings/settings.json) node main.js

Finally, the script that builds it. In this version it builds the bundle every time. Its specific to the solution. change the 3 settings in the first lines

** /build.sh (this you should run manually. I do it using sh build.sh **

#!/bin/bash
#
settingsFile=./settings-development.json
dockerFolder=../dockerFolder
dockerImage=package/name

if [ -d ${dockerFolder}/bundle ]; then
	echo "Removing an existing build bundle"
	rm -rf ${dockerFolder}/bundle
fi

SKIP_LEGACY_COMPILATION=0 NODE_OPTIONS="--max_old_space_size=4096" meteor build ${dockerFolder}/ --directory --architecture os.linux.x86_64 --mobile-settings $settingsFile
retval=$?

if [ -d ${dockerFolder}/.meteor ]; then
echo "Removing meteor from ${dockerFolder}"
	rm -rf ${dockerFolder}/.meteor
fi

echo "- Copying settings to the bundle..."
if [ ! -d ${dockerFolder}/bundle/programs/server/settings ]; then
	mkdir ${dockerFolder}/bundle/programs/server/settings
fi
cp $settingsFile ${dockerFolder}/bundle/programs/server/settings/settings.json

echo "- Copying the Dockerfile ..."
cp dockerstuff/startapp.sh ${dockerFolder}/bundle/

echo "- Copying the docker startup scripts to the meteor bundle folder..."
cp dockerstuff/Dockerfile ${dockerFolder}

cd ${dockerFolder}
docker build --squash -t $dockerImage .
retval=$?
if [ $retval -ne 0 ]; then
	echo "FAILING..."
	exit $retval
fi

#15

Thanks @GeoffreyBooth

I’m gonna save this one for later when ci/cd becomes relevant (right now we’re only 1 (me) working on some early stuff).

Best
Jesper


#16

Glad it helped you out. I spent a good while figuring most of this out myself. I have to host all servers internally on a secure network and I found most of the docker materials to be assuming you were on the internet using one of the cloud providers. It was a case of trial and error for me to get this far.

Btw in your build script when copying files you reference .dockerImage and not the variable. I also removed set -e from startapp.sh as it exited the script for unknown reasons. Finally I think there was something with port and PORT

Thanks for the bugfix with .dockerimage :). Never noticed it as I’ve always used the same folder name.

Set -e I think was something to do with making sure the service automatically restarted if it fails. Can’t quite remember why or when I put it there.

The $port and $PORT variables are not really the same thing in both scripts. $port 8400 in the build script is what port I want the node app to appear on in the docker image.

The service config file passes PORT to the docker service to start the container on. This doesn’t have to be 8400. For instance you can have a development docker service on 8300, a testing service on 8350, a uat service on 8500 and the live service on 8400 by defining 4 docker service compose file with different settings and opening up ports to be accessable.

However, for my docker swarm, I now run a reverse proxy called traefik that handles SSL termination and routing into the docker swarm for the different app versions via dns address resolution in the url. I think the compose file above has some of this config left in.