In this post, I will explain how I recently “Dockerized” a standard MVC web application. This is more of a “how-to Dockerize” than a “why Dockerize” post.
For my web application, I have a Postgres database, with PostGIS extension, running as the data store layer, a Nodejs RESTful API layer as my controller, and a Nodejs web server to serve my website.
I have a working Docker Host (Lubuntu) with Docker Engine (version 1.11.1) installed, and an existing web application with a loosely coupled MVC architecture.
Part 1: Dockerizing the Database
I choose to work on the lowest layer with the least/no dependencies, and build the rest of my container components (i.e. the API and web server) on top of it.
My intention is to reuse existing images on the Docker Registry (hub.docker.com) so as to reduce the amount of work required. I have decided to use mdillon’s Postgis image (tag 9.5) as my base, and work on from there. There are alternatives to this approach, which I explain in Part 4.
I create a Dockerfile to 1) modify the pg_hba.conf file to allow the remote access to it, 2) add a startup script to the docker-entrypoint-initdb.d folder, and 3) load demo data in from the host to the image. My Dockerfile looks like this:
FROM mdillon/postgis:9.5 MAINTAINER Lup Peng # Allow the api to remotely access the database # RUN echo 'host all all 0.0.0.0/0 trust' > $PGDATA/pg_hba.conf # add the database scripts to the initdb folder COPY ./webapp-database.sql /docker-entrypoint-initdb.d/webapp-database.sql # load the demo data to the image RUN mkdir -p /webapp-data COPY ./*.csv /webapp-data/
Together with my Dockerfile, I have the webapp-database.sql with the data CSVs for loading into the database.
My webapp-database.sql has the following steps:
- create a non-root user for the API to access,
- create and connect to the application database, and create the PostGIS extension,
- create all the tables required by the web application
- \copy data from the CSV files into the tables
- change the owner of the tables to the non-root user in step 1
Next, I open a terminal and navigate to the folder of the Dockerfile and build an Image from it.
$ docker build -t="webappdb:0.1" .
After the image has been built successfully, I run a container of the webbappdb:0.1 image. Let’s call this container “init_db”.
$ docker run --rm --name init_db -v ~/webappdb/datavolume:/var/lib/postgresql/data webappdb:0.1
You notice that I passed in the --rm flag to the init_db container, which means that after running this container, it will be removed. The reason why I do this is because this container will run the webapp-database.sql, which loads the CSV files into the container’s file system, and \copy it to the postgres database.
However, I would want my containers to be lightweight, without any unnecessary bloat. Thus what I would want to do is to create an instance of my web application’s Postgres database as a data volume stored in my host directory, and remove the init_db container with all the unnecessary CSVs inside it. (Also, giving it a name is optional)
I now have the web application’s Postgres database files as a host directory data volume, which I can mount to any standard (read:unmodified) Postgres (with Postgis extension) container. This allows for ease of setting up, as well as efficient scaling out:
+------------+ +------------+ +------------+ | Postgres 1 | | Postgres 2 | ... | Postgres N | +------------+ +------------+ +------------+ +----------------------------------------------+ | (Shared) Web Application Data Volume | +----------------------------------------------+
Last but not least, I will create a Postgres (with PostGIS) database container, with the web application Postgres data volume mounted.
$ docker run -v ~/webappdb/datavolume:/var/lib/postgresql/data --name mywebappdb -d -e POSTGRES_PASSWORD=MySecretPass -p 5432:5432 mdillon/postgis
I can check that my database instance is running properly by creating a new disposable (i.e. --rm) container to connect to it, or entering the running database container with a bash script and connecting via the psql CLI.
$ docker run --rm -it mdillon/postgis sh -c 'exec psql -h "" -p "" -U postgres'
$ docker exec -it mywebappdb /bin/bash root@cc5d0d51c8b5:/# psql -U postgres
Part 2: Dockerizing the API Layer
For the API server, we will try our best not to modify the image/containers that we use to run them. So we will mount a host directory as a data volume inside the container and have the Nodejs engine within the container call the .js server file.
First, navigate to the RESTful API application folder, then create the Nodejs container to run it:
restapi-dir$ docker run --name webapi --link mywebappdb:mywebappdb -v "$PWD":/usr/src/app -w /usr/src/app -p 9001:9001 node:wheezy node apiserver.js
In layman terms, I am:
- creating and running a Nodejs container called “webapi” at port 9001,
- with my RESTful API application folder mounted into the container,
- linked to mywebappdb database container (I use the container’s name in my connection string so that I do not need to bother with IP addresses),
- with a host directory mounted as a data volume (the -w flag sets the starting working directory), and
- running the apiserver.js file when the container starts.
This command would have my API listening on port 9001 of the Docker Host.
Part 3: Dockerizing the Web Server
The method is similar to the previous step. The only difference is to use the web server specific directory/file paths.
webserver-dir$ docker run -d --name webserver --link webapi:webapi -v "$PWD":/usr/src/app -w /usr/src/app -p 8080:8080 node:wheezy node webserver.js
And there you have it! I can test my web application by accessing port 8080 of the Docker Host.
To avoid the hassle of finding out and configuring IP addresses in each component, I link the containers together using the –link flag, so that within the application, I can use the alias I give to the container to access it.
For example, in the API container, I can replace the IP address part of the connection string to the database container alias:
[Original] var conString = "postgres://user:firstname.lastname@example.org/webdb" [With Alias] var conString = "postgres://user:pass@mywebappdb/webdb"
Part 4: Design Alternatives
Building My Own Postgres Database with PostGIS extensions
One consideration I did have was to create my own Postgres with PostGIS extension – because why rely on someone else right? This is possible if one knows how to write a proper Dockerfile. I did not take this path because of time constraints.
Creating and Using Named Data Volumes
Instead of using a host directory, it is possible to put the Postgres database files within a data volume. This would similarly allow other containers to mount and access it. I chose to use the host directory method for ease of transporting the database implementation between hosts/VMs.
That being said, it is also possible to make move the Data Volume across hosts/VMs. One would need to use another container to mount the Data Volume, and a host directory, and create a tarball of the Data Volume inside the host directory.
Part 5: Summary
As a finishing touch, I wrote a bash script to start and stop all the containers in the appropriate order with a single command.
And there we have it – the steps I took to Dockerize a MVC Web Application. ‘Till next post!