Recently I have been following this awesome Flask tutorial to build a microblog. The author does a great job breaking things down, and I was able to follow step by step from hello world to containerizing my web app. However, one cruicial step is left as an exercise for the reader to figure out, which is to organize everything with docker-compose, as well as actually deploying the web app on AWS. I took a stab at it and got it working, checkout my microblog here. Looking back, it was easy but not very straight forward. So I thought I will write down what I did and what I learned for future references. Here we go!

Docker Compose

I followed along the Flask tutorial all the way up to the deployment on docker containers chapter, while skipping some chapters like translation and full text search. But either way if you follow the tutorial close enough, you will end up having a web app and a mysql database. In the deployment on docker container chapter, they will be turned into containers, with a microblog:latest image and a public mysql image. And here is where docker-compose come in. Docker-compose is a tool that was developed to help define and share multi-container applications. This means that if you are managing more than one container and their communications, docker-compose can make it easier for you. For example, without docker-compose, the way to get the web app up and running is by running the docker run command with several arguments

1
2
3
4
5
6
$ docker run --name microblog -d -p 8000:5000 --rm -e SECRET_KEY=my-secret-key \
-e MAIL_SERVER=smtp.googlemail.com -e MAIL_PORT=587 -e MAIL_USE_TLS=true \
-e MAIL_USERNAME=<your-gmail-username> -e MAIL_PASSWORD=<your-gmail-password> \
--link mysql:dbserver \
-e DATABASE_URL=mysql+pymysql://microblog:<database-password>@dbserver/microblog \
microblog:latest

This feels cumbersome to me, as I will need to remember the format and arguments, or turn this into a bash script. As you make changes and add more containers, it quickly becomes messy. With docker-compose, you can specify all these commands in a single nicely formatted docker-compose.yml. Here’s an example docker-compose.yml that spins up two containers, one for the web app and one for the database, then link them together so they can communicate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
version: '2'

services:
flask:
image: microblog:latest
ports:
- "8000:5000"
env_file:
- variables.env
links:
- dbserver:dbserver
depends_on:
- dbserver

dbserver:
image: mysql/mysql-server:latest
container_name: dbserver
environment:
- MYSQL_RANDOM_ROOT_PASSWORD=yes
- MYSQL_DATABASE=microblog
- MYSQL_USER=microblog
- MYSQL_PASSWORD=password
volumes:
- data:/var/lib/mysql

volumes:
data:

Let’s step through this line by line:

  • version: ‘2’ specifies the docker-compose version being used. Newer version includes newers functionalities.
  • services: specifies the services and their corresponding configurations. Think of them each as a container. For the microblog app we will have a web app container and a mysql container, thus we are looking at 2 services.
  • The flask service specifies the image to use, which is the microblog:latest, and maps the docker host’s port 8000 to the container’s port 5000. It also takes an environment variable files named variables.env. The links section specifies that the flask service should have access to the dbserver service, and will be able to refer to it with the name dbserver within the flask container. Lastly, the depends_on section indicates that the flask service needs to wait for the dbserver service to start first.
  • The dbserver section is mostly similar to the flask section, except it takes the environment variables as arguments in the envrionment section instead of taking in a env file, and it has a volumes section which specifies a named volume for where the database in the container is stored. The nice thing about the named volume is that it will persist even when the container is torn down, and it is easily migrated to a different container if needed.
  • The bottom volumes section declares the shared volumes that can be accessed by both services. The flask service needs to be able to access this volume as well since it needs to be able to retrieve user data from the database stored in the volume.

Pretty straight forward and well organized, don’t you think? Once the docker-compose.yml file is completed, save it in the same directory as the Dockerfile. And simply do docker-compose up to start the web app.

As a bonus, there was mentioned of using a Nginx server as a reverse proxy for extra security in the comment section. After researching around online, I found that it is a good idea to have a Nginx layer that sits in front of the web app, since it can serve as an extra layer, as well as a load balance that directs traffic. So I have decide to add a Nginx service to the docker-compose file to make the web app more secure.

Here’s the finished docker-compose.yml with Nginx service added:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
version: '2'

services:
flask:
build: .
image: microblog:latest
restart: always
ports:
- "5000"
env_file:
- variables.env
networks:
- app-network
depends_on:
- dbserver

dbserver:
image: mysql/mysql-server:latest
container_name: dbserver
environment:
- MYSQL_RANDOM_ROOT_PASSWORD=yes
- MYSQL_DATABASE=microblog
- MYSQL_USER=microblog
- MYSQL_PASSWORD=password
volumes:
- data:/var/lib/mysql
networks:
- app-network
restart: always

nginx:
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/ssl
depends_on:
- flask
networks:
- app-network
restart: always

volumes:
data:

networks:
app-network:

Here we introduced two new major change:

  1. Instead of links, we switched to using networks. The networks section defines a network for services to communicate within. The services in the same network have access to each other and can reference each other using the service name. This is easier than specifying links between multiple services, and also the fact that links is deprecated makes network the obvious choice.
  2. The Nginx section binds two ports: 80:80 for http requests, and 443:443 for https request. And now the flask service is opening its 5000 port for the Nginx server to communicate with. But where in the file did we instruct Nginx to communicate with flask service on port 5000? This is actually reflected in the volumes section of Nginx service, where a nginx.conf file is mounted from the ./nginx.conf on docker host to /etc/nginx/nginx.conf in the docker container. And the nginx.conf file looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
events {}

http {
server {
listen 80;
server_name localhost;

location / {
proxy_pass http://flask:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

server {
listen 443 ssl;
server_name localhost;

ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;

location / {
proxy_pass http://flask:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}

The nginx.conf file specifies which port the Nginx server is listening at, and how to handle the incoming traffic. The first server section is handling http request at port 80, where the second server section is handling https request at port 443. The http://flask:5000 line indicates the traffic should be directed to 5000 port of flask service.

With the new docker-compose.yml, now when you spin up your service, you will have a Nginx reverse proxy for your web app, and you can access the web app from port 80. How exciting!

This post covered the docker-compose part of the deployment. In part two we will continue on to explain how to deploy this web app on an AWS EC2 machine.