Deploying a Web App, Redis, Postgres and Nginx with Docker

Why Docker

Docker is a virtualization technology. The key feature I like most is it provides resource isolation. The traditional way of building a (low-traffic) website is we install the web app, cache, database, Nginx directly on a server. It’s not easy to change the settings or the content a lot, because they are in the same environment. Changing one may impact others. With Docker, we can put each service in a container. It keeps the host server very clean. We can easily create/delete/change/re-create containers.

Install Docker on the host

Docker runs on a 64-bit Linux OS only. If your Linux is 32-bit, you have to re-install the 64-bit version. My original OS was 32-bit CentOS. Now I am using 64-bit Debian 8. The main reason I choose Debian is its distribution size is small and Docker recommends it in Best Practices(it’s ridiculous that almost all examples at docker.com use ubuntu). Actually the host’s OS can be different to the container’s OS. I choose Debian instead of 64-bit CentOS because I don’t want to spend any time on the differences. For example, the package management tools on Debian and CentOS are different. One is apt, the other is yum.

sudo su
curl -sSL https://get.docker.com/ | sh
# Add theuser to docker group to run docker as a non-root user
# MUST logout and re-login to let it effective
usermod -aG docker theuser
# Start docker service
systemctl enable docker.service
systemctl start docker.service

Prepare

cd / git clone
https://github.com/vinceyuan/DockerizingWebAppTutorial.git
cd / && mkdir mydata && cd myata
mkdir redis_data && mkdir postgres_data && mkdir nginx_data
root@pophubserver:/mydata# mkdir log_mynodeapp && mkdir log_nginx

Redis

We use the official Redis image. Run it directly with this command:

docker run -d -v /mydata/redis_data:/data --name myredis --restart=always redis
$ docker run -d -v /mydata/redis_data:/data --name myredis --restart=always redis
Unable to find image 'redis:latest' locally
latest: Pulling from redis
7a3e804ed6c0: Pull complete
b96d1548a24e: Pull complete
5ba9a5b9710f: Pull complete
37f07aacbfe5: Pull complete
ec7f3a6b5dc6: Pull complete
499b313c4d4e: Pull complete
4416945429c6: Pull complete
0daf71066555: Pull complete
1f86439b265d: Pull complete
9e6288fa06c0: Pull complete
3c083702089f: Pull complete
71cc4c7123fc: Pull complete
91e5e3734476: Pull complete
8d7fb9bd09ab: Pull complete
e6b7cf8bf1b1: Pull complete
96182c1bd121: Pull complete
4b7672067154: Already exists
redis:latest: The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security.
Digest: sha256:01b59520487a9ada4b8e31558c0580930a4e5f2a565a1cb85b66efe7c6ce810d
Status: Downloaded newer image for redis:latest
a96b6d2555e9f9fb1f70fea60f8cf75326cd331ebef9d4b667e322cea899d48c
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a96b6d2555e9 redis:latest "/entrypoint.sh redi 16 minutes ago Up 16 minutes 6379/tcp myredis
$ docker exec -i -t myredis bash
root@a96b6d2555e9:/data# redis-cli
127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> save
OK
127.0.0.1:6379> exit
root@a96b6d2555e9:/data# exit

Postgres

We use the official Postgres image too. Just run it directly.

docker run -d --name mypostgres -e POSTGRES_PASSWORD=postgres -v /mydata/postgres_data:/var/lib/postgresql/data --restart=always postgres
$ docker exec -i -t mypostgres bash
root@11602c44f706:/# psql -U postgres
psql (9.4.2)
Type "help" for help.
postgres=# create database mynodeappdb;
CREATE DATABASE
postgres=# \q
root@11602c44f706:/# exit
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
11602c44f706 postgres:latest "/docker-entrypoint. About a minute ago Up About a minute 5432/tcp mypostgres
a96b6d2555e9 redis:latest "/entrypoint.sh redi 32 minutes ago Up 32 minutes 6379/tcp myredis

Redis client and Postgres client

The Dockerfile for redis client:

FROM debian:7RUN apt-get update \
&& apt-get install -y redis-server \
&& apt-get clean \
&& apt-get autoremove \
&& rm -rf /var/lib/apt/lists/*
RUN service redis-server stop
cd /DockerizingWebAppTutorial/dockerfiles/myredisclient
docker build -t myredisclient .
FROM myredisclientRUN apt-get update \
&& apt-get install -y wget \
&& echo 'deb http://apt.postgresql.org/pub/repos/apt/ wheezy-pgdg main' >> /etc/apt/sources.list.d/pgdg.list \
&& wget --no-check-certificate --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
&& apt-get update \
&& apt-get install -y --force-yes postgresql-client \
&& apt-get clean \
&& apt-get autoremove \
&& rm -rf /var/lib/apt/lists/*
cd /DockerizingWebAppTutorial/dockerfiles/myredispgclient
docker build -t myredispgclient .
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
myredispgclient latest 78b18351c561 6 minutes ago 132.5 MB
myredisclient latest bb2ac4846244 8 minutes ago 87.7 MB
postgres latest 1636d90f0662 2 days ago 214 MB
redis latest 4b7672067154 4 days ago 111 MB
debian 7 b96d1548a24e 9 days ago 84.97 MB

Node.js

Let’s build a Node.js image. In the Dockerfile for mynodejs image, we install node.js, express, forever and then set NODE_ENV production. In this example, I am not using the latest version.

FROM myredispgclientRUN apt-get update \
&& apt-get install -y --force-yes --no-install-recommends \
apt-transport-https \
build-essential \
curl \
ca-certificates \
git \
lsb-release \
python-all \
rlwrap \
&& apt-get clean \
&& apt-get autoremove \
&& rm -rf /var/lib/apt/lists/*
RUN curl https://deb.nodesource.com/node/pool/main/n/nodejs/nodejs_0.10.30-1nodesource1~wheezy1_amd64.deb > node.deb \
&& dpkg -i node.deb \
&& rm node.deb
RUN npm install -g express@3.4.7 \
&& npm install -g forever \
&& npm cache clear
ENV NODE_ENV production
cd /DockerizingWebAppTutorial/dockerfiles/mynodejs
docker build -t mynodejs .

mynodeapp

Then we build an image for mynodeapp. In Dockerfile, we run npm install, and use forever to run the node.js app. We don’t use forever start, because we don’t run it as a daemon (otherwise, the container will quit immediately).

FROM mynodejsCOPY . /src
RUN cd /src && npm install
VOLUME /log
CMD ["forever", "-l", "/log/forever.log", "-o", "/log/out.log", "-e", "/log/err.log", "/src/app.js"]
cd /DockerizingWebAppTutorial/mynodeapp
docker build -t mynodeapp .
var conString;if ('development' == app.get('env')) {
app.use(express.errorHandler());
conString = "postgres://vince:@localhost/mynodeappdb"; // Use your db, user and password
} else {
conString = "postgres://postgres:postgres@localhost/mynodeappdb"; // Use your db, user and password
}
var pgClient = new pg.Client(conString);pgClient.connect(function(err) {
if(err) return console.error('Could not connect to postgres', err);
console.log('Connected to postgres');
});
var redisClient = redis.createClient(6379, '127.0.0.1', {})app.get('/', function(req, res) {
pgClient.query('SELECT NOW() AS "theTime"', function(err1, result) {
redisClient.get("number", function(err2, reply) {
res.render('index', { pgTime: result.rows[0].theTime, number: reply });
});
}); // Errors Ignored in this example. You should check errors in a real project.
});
http.createServer(app).listen(app.get('port'), function() {
console.log('Express server listening on port ' + app.get('port'));
});
var redis_host = process.env.REDIS_PORT_6379_TCP_ADDR || '127.0.0.1';
var redis_port = process.env.REDIS_PORT_6379_TCP_PORT || 6379;
var db_host = process.env.POSTGRES_PORT_5432_TCP_ADDR || 'localhost';
docker run -d --name mynodeapp --link mypostgres:postgres --link myredis:redis -v /mydata/log_mynodeapp:/log -p 3000:3000 --restart=always mynodeapp
$ curl localhost:3000
<!DOCTYPE html><html><head><title></title><link rel="stylesheet" href="/stylesheets/style.css"></head><body><H1>mynodeapp</H1><p>Number from Redis: 1</p><p>Time from Postgres: Fri May 29 2015 09:47:54 GMT+0000 (UTC)</p></body></html>

Nginx

Now we install Nginx. In the Dockerfile, we make directory /mynodeapp/public. A folder in the host will be mounted here.

FROM nginx# Create folder for static files
RUN mkdir /mynodeapp && mkdir /mynodeapp/public
# copy sslcert files to /etc/nginx/ for https
#COPY mydomain.* /etc/nginx/
# copy conf
COPY nginx-docker.conf /etc/nginx/nginx.conf
    upstream mynodeapp_upstream {
server mynodeapp:3000;
keepalive 64;
}
cd /DockerizingWebAppTutorial/dockerfiles/mynginx
docker build -t mynginx .
docker run -d --name mynginx --link mynodeapp:mynodeapp -v /mydata/nginx_data:/var/cache/nginx -v /mydata/log_nginx:/var/log/nginx -v /DockerizingWebAppTutorial/mynodeapp/public:/mynodeapp/public -p 80:80 -p 443:443 --restart=always mynginx
# curl localhost
<!DOCTYPE html><html><head><title></title><link rel="stylesheet" href="/stylesheets/style.css"></head><body><H1>mynodeapp</H1><p>Number from Redis: 1</p><p>Time from Postgres: Fri May 29 2015 10:12:35 GMT+0000 (UTC)</p></body></html>
root@pophubserver:/DockerizingWebAppTutorial/dockerfiles/mynginx#
root@pophubserver:/DockerizingWebAppTutorial/dockerfiles/mynginx# curl localhost/stylesheets/style.css
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

--

--

iOS/Android/Web developer. UX engineering lead at Grab. Singapore/Hong Kong/Shanghai

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Aijin Yuan (Vince)

Aijin Yuan (Vince)

iOS/Android/Web developer. UX engineering lead at Grab. Singapore/Hong Kong/Shanghai