Run a NodeJS process through forever from within a Docker container

One of the components, Publican, that I had to manage has many moving parts. The end product of that component is basically static HTML documents that ends up on specs.webplatform.org.

Since we need to have many packages installed in very specific version, and automating the installation wouldn’t bring any more benefit than being self-contained, I thought it would be best to go through the steps of converting it into a Docker container.

The following is a procedure I wrote to teach my colleague, Robin Berjon, how to run his system called Publican from within a Docker container. Publican is basically a GitHub hook listener that generates specs written to be parsed by ReSpec or Bikeshed

Run publican inside Docker

What this’ll do is basically build a VM that’ll run a Docker container. The container will write in files outside of it.

You’ll quickly notice that the paths will look the same, its confusing, sorry about that. Fortunately for us, the paths in the procedure are the ones that will be mounted through Docker Volume (the -v option when you call docker) and will, in the end, be the same files.

Once you have a Docker container running on a VM, it’ll replicate how a production VM will run the tasks. Since we know where the container will write files, we’ll have our frontend servers to forward requests to publican, and serve files it generated.

Doing all this removes the need to do any rsync. NGINX within the VM that’ll run Docker will take care of serving static files, and frontend server will expose it to the public.

Steps

  1. Have Vagrant and VirtualBox installed

  2. Follow what’s in renoirb/salt-basesystem README.md

  • Make sure you follow Vagrant Sandbox utilities part

        vagrant ssh
        sudo salt-call state.highstate
        sudo salt-call state.sls vagrantsandbox.docker
        exit
    
  • Reboot the VM by doing vagrant reload

        vagrant reload
    
  1. No need to follow what’s in webplatform/publican DOCKER.md file. Those are notes to show how to build a container. For this time, we’ll use a container I already built and pushed on Docker hub!

  2. Setup what’s required to run the container

        vagrant ssh
    
  • Prepare the folders;

        sudo -s
        su webapps
        id
    
  • You should see

        uid=990(webapps) gid=990(webapps) groups=990(webapps),33(www-data),998(docker)
    
  • Prepare the folders

        cd /srv/webapps
        mkdir publican/data
        cd publican
    
    • If all went well so far; you should be able to do docker ps as the webapps user. Otherwise reboot and/or run salt-call with both state.highstate state.sls vagrantsandbox.docker states. There should be nothing left to do.

      docker ps
      CONTAINER ID        IMAGE                   COMMAND                CREATED...
      
  • Pull the publican Docker image I built (it’ll take about 10 minutes)

        docker pull webspecs/publican:wip
    
  1. Copy the other files in this Gist in your local coputer where you cloned the salt-basesystem repository. From that folder you can move them inside the Vagrant VM where you need.
  • Copy publican config

        cp /vagrant/config.json data/
    
  • Download Bikeshed stuff that i didn’t figure out yet what’s important to keep, extract it in /srv/webapps/publican/spec-data/

        wget https://renoirboulanger.com/spec-data.tar.bz2
        tar xfj spec-data.tar.bz2
    
  • You can open up another terminal session and connect to the Vagrant VM vagrant ssh (e.g. if you don’t use tmux or screen)

        mkdir -p spec-data/readonly/
        mkdir -p data/{gits,logs,publish,queue,temp}
    
  1. Run the container

        docker run -it --rm -v "$(pwd)/data":/srv/webapps/publican/data \
                   -v "$(pwd)/spec-data":/opt/bikeshed/bikeshed/spec-data \
                   -p 7002:7002 webspecs/publican:wip
    
  • If you see the following, you’re in the Docker container!!

        webapps@2f33f5c6e183:~$
    
  • Initiate the empty shell we just created (it’ll create stuff in the data/ folder outside of the container)

        publican.js init
    
  • It should look like this

    publican-init

  • Once done, exit the container. Notice that by doing this, you lose the state of the VM and anything that has been written in the container. But, since we use volumes (notice the -v /host/path:/container/path), we actually wrote outside of the container.

  • We can exit the container

        exit
    
  • At this stage, we had publican and bikeshed to generate files (we may call this a “cache warmup” of softs). Now, let’s prepare the Vagrant VM to serve the static content. Notice that the next commands are there only for the purpose of a local workspace, in production this step will also be managed automatically.

  • Let’s get back as the root user, and create a quick web server;

        exit
        apt-get -yqq install nginx
        mv /vagrant/default.conf /etc/nginx/sites-available/default
        service restart nginx
    
  • Let’s return back to as the webapps user and launch the runner

        su webapps
        cd /srv/webapps/publican/
    
  • Launch the container; this will also be managed automatically in production.

        docker run -it --rm -v "$(pwd)/data":/srv/webapps/publican/data \
                       -v "$(pwd)/spec-data":/opt/bikeshed/bikeshed/spec-data \
                       -p 7002:7002 webspecs/publican:wip bin/run.sh
    

    It should look like this

    publican-run-hook

  • get your Vagrant VM IP address

        ifconfig
    
  • Should start by 172... or 192...; visit a browser to that address

Gists

Here are the files mentioned in this post

config.json

Publican expects this file as data/config.json.

{
    "bikeshed":     "/opt/bikeshed/bikeshed.py"
,   "rsyncPath":    "/srv/webapps/publican/"
,   "python":       "python2"
,   "logFile":      "logs/all.log"
,   "email":        {
        "to":       "renoirb@gmail.com"
    ,   "from":     "publican-noreply@webplatform.org"
    ,   "host":     "localhost"
    ,   "level":    "error"
    ,   "handleExceptions": true
    }
,   "purgeAllURL":  "https://api.fastly.com/service/fooo/purge_all"
,   "purgeAllKey":  "baar"
}

default.conf

A minimal NGINX web server digging for static content that Publican generates.

# file: /etc/nginx/sites-enabled/default

server {
  listen 80 default_server;
  root /srv/webapps/publican/data/publish;
  index index.html index.htm;
  server_name localhost;
  location / { try_files $uri $uri/ =404; }
}
Dockerfile

Here is the project’s Dockerfile I created. I think it should be smaller, but Publican works with the following script.

Each step in a Dockerfile creates a “commit”, make sure you have as few of them as possible, and also make sure that you clean after yourself. Remember that a Docker container is re-deployable and smallest the size of the container, the better!

Notice a few details;

  • ENV DEBIAN_FRONTEND=noninteractive helps with dialogs
  • USER webapps tells “where” the rest of the script will make commands as a different user than root. Make sure what’s required by root to be done before!
  • COPY ... this is basically how you import content inside the container (i.e. make the container heavier)
#
# Publican Docker runner
#
# See also:
#   * https://github.com/nodesource/docker-node/blob/master/ubuntu/trusty/node/0.10.36/Dockerfile

FROM nodesource/trusty:0.10.36

MAINTAINER Renoir Boulanger <renoir@w3.org>

ENV DEBIAN_FRONTEND=noninteractive

# Dependencies: Bikeshed, PhantomJS, Bikshed’s lxml
RUN apt-get update && apt-get -y upgrade && \
    apt-get install -yqq git python2.7 python-dev python-pip libxslt1-dev libxml2-dev zlib1g-dev && \
    apt-get install -yqq libfontconfig1 libfreetype6 curl && \
    apt-get autoremove -yqq --purge && \
    pip install --upgrade lxml

# Copy everything we have locally into the container
# REMINDER: Make sure you run `make clone-bikeshed`, we prefer to keep a copy locally outside
# of the data volume. Otherwise it would make problems saying that bikeshed clone is not in the
# same filesystem.
COPY . /srv/webapps/publican/

# Make sure we have a "non root" user and
# delete any local workbench data/ directory
RUN /usr/sbin/groupadd --system --gid 990 webapps && \
    /usr/sbin/useradd --system --gid 990 --uid 990 -G sudo --home-dir /srv/webapps --shell /bin/bash webapps && \
    sed -i '/^%sudo/d' /etc/sudoers && \
    echo '%sudo ALL=NOPASSWD: ALL' >> /etc/sudoers && \
    mv /srv/webapps/publican/bikeshed /opt && \
    rm -rf data && \
    mkdir -p data/temp && \
    rm -rf Dockerfile Makefile .git .gitignore DOCKER.md && \
    chown -R webapps:webapps /srv/webapps/publican && \
    chown -R webapps:webapps /opt/bikeshed

# Switch from root to webapps system user
# It **HAS to be** the SAME uid/gid as the owner on the host from which we’ll use as volume
USER webapps

# Where the session will start from
WORKDIR /srv/webapps/publican

# Environment variables
ENV PATH /srv/webapps/publican/node_modules/.bin:/srv/webapps/publican/bin:/srv/webapps/publican/.local/bin:$PATH
ENV HOME /srv/webapps/publican
ENV TMPDIR /srv/webapps/publican/data/temp
ENV NODE_ENV production
ENV GIT_DISCOVERY_ACROSS_FILESYSTEM true

# Run what `make deps` would do
RUN pip install --upgrade --user --editable /opt/bikeshed && \
    mkdir -p node_modules && npm install

# Declare which port we expect to expose
EXPOSE 7002

# Allow cli entry for debug, but make sure docker-compose.yml uses "command: bin/run.sh"
ENTRYPOINT ["/bin/bash"]

# Note leftover: Ideally, it should exclusively run
#ENTRYPOINT ["/bin/bash", "/srv/webapps/publican/bin/run.sh"]

# Note leftover: What it ends up doing
#CMD ["node_modules/forever/bin/forever", "--fifo", "logs", "0"]

Forever start script

If you notice in the Docker run command, I call a file bin/run.sh, here it is.

docker run -it --rm -p 7002:7002 \
           webspecs/publican:latest bin/run.sh

Publican runs its process using Forever. The objective of forever is to keep a process to run at all times.

While this isn’t ideal for NodeJS services, in the present use-case of a Docker container who has the only purpose to run a process; Forever apt for the job!

#!/bin/bash

export RUNDIR="/srv/webapps/publican"

cd $RUNDIR

node_modules/forever/bin/forever start $RUNDIR/bin/server.js
node_modules/forever/bin/forever --fifo logs 0

More to come

I have more notes to put up, but not enough time to give more context. Come back later for more!