I did my very first semi-seriuos test with docker; so far I’m pretty happy with what I have got. I have a small project, let’s call it superduper, composed of

  • frontend written in React
  • backend in Clojure which runs behind Apache Tomcat
  • database on MongoDB

I wanted a CI workflow so that each time I push on git, Jenkins creates a new build with a new docker image and uploads everything on a Docker registry. The Docker registry I’m using is a private Nexus3 instance, but it can be anything. My Jenkins instance has the Docker Pipeline plugin installed.

I have created an overall setup with Docker and Jenkins. Each project (FE, BE) has Jenksinfile and a Dockerfile.

Jenkins

The Jenkinsfile has the following structure:

def version = "1.0.0" // This must be somehow be fetched dynamically OR the Jenkins file must be updated for each new version.
def project = "superduper"

node {
    //Clean Workspace
    //--------------------------------------------------------
    stage "clean workspace"
    deleteDir()

    //Checkout sourcecode
    //--------------------------------------------------------
    stage "checkout"
    checkout scm

    //Unit Tests
    //------------------------------------------------------------
    stage "unit test"

    //Build
    //------------------------------------------------------------
    stage "build"

    // I build inside docker so I do not have to install dependencies on Jenkins. Note that .m2 directory is shared so it is not downloaded each time
    try {
        sh "docker run \
            -w /usr/src/app \
            -v `pwd`:/usr/src/app \
            -v ~/.m2:/home/user/.m2 \
            -e LOCAL_USER_ID=\$UID clojure:lein-2.7.1-alpine /bin/bash \
            -c 'lein with-profile production ring uberwar; chmod -R a+rwx target'" 
            // I need the ugly chmod at the end because of permission issues.
    } catch (e) {
        error("build failed")
    }

    //Tag
    stage "tag git"
    //------------------------------------------------------------
    sh "git tag v${version}"
    sh "git push --tags"

    //Docker stuff
    //--------------------------------------------------------
    stage('Build docker'){
           image = docker.build("${project}:${version}", ".")
           latest = docker.build("${project}:latest", ".")
           docker.withRegistry('https://docker.qaenv.it/', 'Nexus'){
                image.push()
                latest.push()
           }
    }
}

Docker

The backend Dockerfile has the following structure:

# Pull from tomcat
FROM tomcat:9.0.1-jre8-alpine

# Connect to localDB (see docker-compose.yml later)
ENV DB_URI mongodb://mongo:27017/superduper

# Copy the build artifact to the tomcat webapps directory
COPY ./target/superduper.war /usr/local/tomcat/webapps

# Expose tomcat port
EXPOSE 8080

and frontend one is

# Pull from standard apache image
FROM httpd:2.4-alpine

# Inject my local config
# (basically just a virtual host 
# + the httpd.conf that enables virtual hosts)
COPY ./httpd_conf/httpd.conf /usr/local/apache2/conf/
COPY ./httpd_conf/httpd-vhosts.conf /usr/local/apache2/conf/extra/

# Copy code of the frontend into Document Root
COPY ./index.html /usr/local/apache2/htdocs/
COPY ./build /usr/local/apache2/htdocs/

# Export apache port
EXPOSE 80

with the following virtual host httpd-vhost.conf

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot /usr/local/apache2/htdocs

    ErrorLog /usr/local/apache2/logs/error.log
    CustomLog /usr/local/apache2/logs/access.log combined

    ProxyPass /api http://tomcat:8080/
    ProxyPassReverse /api http://tomcat:8080/superduper
</VirtualHost>

The Jenkins job is confiured to run each time a commit is pushed in git. This creates a new Docker image which is uploaded on Nexus. The Docker images is tagged both with the build number and as latest.

Docker Compose

There is a docker-compose.yml that puts everything together:

version: "3"

services:
  httpd:
    image: my-registry.com/superduper-frontend:latest
    ports:
    - 80:80

  tomcat:
    image: my-registry.com/superduper-backend:latest
    environment:
      - DB_URI=mongodb://mongo:27017/superduper # this is used by the backend to connect to the DB
    ports:
      - 8080:8080

  mongo:
    image: mongo:3.5.13
    volumes:
      - ./data:/data/db
    ports:
      - 27017:27017

Bonus, there are a couple of other docker-compose files for production and development overrides:

version: "3"

services:
  mongo:
    volumes:
      - /tmp/data:/data/db
version: "3"

services:
  httpd:
    volumes:
      - ./frontend:/usr/local/apache2/htdocs/

  tomcat:
    volumes:
      - ./webapp:/usr/local/tomcat/webapps

which are run as docker-compose -f docker-compose.yml -f docker-compose.dev.yml up.

Digital Ocean

Finally, to run everything in production I have created a droplet on Digital Ocean. It was super easy to do by using docker-machine. Because I’m lazy, I wrote this easy script

#!/bin/sh

export DIGITALOCEAN_IMAGE='centos-7-x64'
export DIGITALOCEAN_ACCESS_TOKEN='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
export DIGITALOCEAN_REGION='fra1'
export DIGITALOCEAN_SIZE='2gb'

docker-machine create \
--driver digitalocean \
--digitalocean-access-token $DIGITALOCEAN_ACCESS_TOKEN \
--digitalocean-image $DIGITALOCEAN_IMAGE \
--digitalocean-region $DIGITALOCEAN_REGION \
--digitalocean-size $DIGITALOCEAN_SIZE \
--engine-storage-driver=overlay \
superduper

Just a quick note, I had to user Centos in place of Ubuntu because it failed to provision the docker environment. The most difficult part was to find the correct image name for Centos because (incredibly!) Digital Ocean documentation is not precise on this point. I have found a page with the list of all the images supported by DO.

Security

This is a semi-serious project. I wanted to test docker, docker-compose, Jenkins and docker-image, so I did not focus on security yet. Next steps will be:

  • add letsencrypt
  • add some sort of isolation
  • test docker swarm
  • add production-oriented configuration to docker-compose