Gitlab CI Docker Build Meteor... Please
Jul 22, 2018
5 minute read

TLDR

Building a meteor project with Gitlab CI.

Docker Build

As many developers today are already aware, shipping your code with docker is a great way of keeping things organized. In addition, using a CI tool like GitLab’s CI tool you can automate almost any build/test process. For a couple of recent projects I have been using Meteor which is a batteries included Javascript framework. Meteor’s included build process creates server and web client bundles and is a necessary step before production. My recent problem presented itself when I tried to build a Meteor project into a docker container using Gitlab CI. I tried a number of containers built for the purpose of building meteor apps into containers, with little to no success. Several of these solutions are no longer maintained and the ones that did create a container successful resulted in images larger than necessary. I decide to roll my own solution in attempt to both understand the build process and build a reasonably sized image.

The Goal - A Meteor Bundle

During development meteor expects you to follow certain patterns to your application develop both your frontend and backend. The Meteor CLI development functionality handles standing up our development environment locally, automatically rebuilding the application as changes are made. This state is not suitable for production and deploying our app we need to use the Meteor CLI to build a bundle that we then run like any other regular Nodejs application. This build step proved to non-trivial when I attempted to create a Gitlab CI pipeline.

Gitlab CI

The ultimate goal from our Gitlab CI pipeline is a Docker container containing our bundle. Gitlab CI provides a number of different methods for building docker containers including a shell executor, docker-in-docker and docker socket binding. Each method has it trade offs and you can read more about them at Gitlab.

Docker-in-Docker (dind)

Docker-in-Docker is the recommended choice and provides a clean, conflict free environment to build our app. Docker-in-Docker is built for working with Docker inside a Docker container. As the Docker-in-Docker method dose not interact with the root files system images are not saved on the machine meaning there is no maintenance requirement to periodically remove images from the build machine (When you start using your own runner of course).

My first attempt at a meteor build process was to build the meteor application in a Docker-in-Docker container and then use a Dockerfile to move the bundle into a new container based off the correct Nodejs base container. (Note that a meteor version is explicit in the version of Nodejs that should run the resulting bundle.) Docker-in-Docker is built on Alpine Linux. On my first attempt I downloaded the Meteor CLI tool in to my Docker-in-Docker environment and attempted to build the bundle only to have the build fail. Alpine Linux is significantly stripped down to build small containers and therefore was missing many of the packages necessary for the Meteor CLI tool to work correctly.

Docker-in-Ubuntu

Enter Docker-in-Ubuntu a replacement for Docker-in-Docker build on the ubuntu base image. I wrote a simple Dockerfile to create a container that replicates most of the Docker-in-Docker behavior. Using automated docker builds over at DockerHub the container is build and released publicly.

.gitlab-ci.yml

With our new fancy Docker-in-Ubuntu container we now have everything we need to build Meteor containers at Gitlab. Below is the YAML file that builds the container and stores the container in the Gitlab container registry.

stages:
  - build
  - deploy

docker-build:
  stage: build
  image: stieneee/docker-in-ubuntu:latest
  services:
    - docker:dind
  before_script:
    - echo CI_PIPELINE_ID $CI_PIPELINE_ID
    - curl -sL https://deb.nodesource.com/setup_8.x | bash -
    - apt-get update
    - apt-get install -y nodejs build-essential git
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
    - curl https://install.meteor.com/ | sh
    - mkdir -p /tmp/meteor-build/
  script:
    - npm i
    - meteor build --allow-superuser --directory /tmp/meteor-build/
    - cp Dockerfile /tmp/meteor-build/bundle/
    - cd /tmp/meteor-build/bundle/
    - docker build -t registry.gitlab.com/{USERNAME}/{REPO}:$CI_PIPELINE_ID .
    - docker push registry.gitlab.com/{USERNAME}/{REPO}:$CI_PIPELINE_ID
    - docker tag registry.gitlab.com/{USERNAME}/{REPO}:$CI_PIPELINE_ID registry.gitlab.com/{USERNAME}/{REPO}:latest
    - docker push registry.gitlab.com/{USERNAME}/{REPO}:latest

If you use this as a template replace the {USERNAME} and {REPO} with your repo information. It is important to remember that the work done in this container is thrown away and only the result of the Docker build step is kept.

The Dockerfile

Finally the docker file that takes the Meteor bundle and creates a new container for just that bundle. Unlike some of the other solutions I tested the result is a container only containing the necessary files to run the Meteor application in production. It is also good practice to change the user group the final process will be running as. The node user is already created in the node containers.

FROM node:8.11.3

COPY . /bundle
RUN (cd /bundle/programs/server && npm i)

USER node

ENV NODE_ENV=production

CMD node /bundle/main.js

A reminder that the version of node is specified by the current version of the Meteor app. While this could be scripted to look at the a file created during the Meteor build process I prefer to bump this manually to be aware of when it changes.

Conclusion

While at first building a Meteor bundle in Gitlab CI seemed like a difficult process it was relatively simple to accommodate the Meteor CLI tool and then build a container containing only the necessary files. As an added bonus the base containers are from trusted maintained sources or rebuilt automatically when dependencies are changed resulting in a solution that requires little additional upkeep.

All-in I believe I spent more time testing and tinkering with other solutions the developing this solution. Once I decided to roll my own using as much automation as I could everything came together quite nicely.

I hope you found this helpful!



comments powered by Disqus