This tutorial is intended for Docker beginners.
We will use a Django web app as an example, but it can be easily substituted with other web apps such as flask, tornado, etc.
- have docker installed on your local machine
requirements.txtfor all the dependencies of the app
Every Dockerfile has a base image. Choosing the correct base image is important, because docker images tend to be very large in size, and can potentially cause trouble in space and be costly. Bigger images also needs more time to build.
If you have some time to do research, that’s good, but generally speaking I would recommend try the basic one for now, and then you can switch to a smaller one later. For example, for any python projects you can start with python:3.6, or whatever the version of python you’re using; and then after everything is working, you can switch to python:3.6-slim, or alpine.
At the end of this tutorial (or maybe in the next tutorial?) I will also provide a way to use multi-stage building to reduce the image size but still have what you need in the slim version.
Dockerfile almost always starts with the
FROM command. The
FROM command specifies which base image you want to build on top of.
Don’t forget to provide the tag, otherwise docker will automatically use
latest as tag, and it might not be want you want. Check Docker Hub for available tags, don’t just make it up.
You can always use other local images as base. For example, if I already have a custom image called
kelly locally, I can use
kelly:latest in my Dockerfile.
This step is actually optional – if you don’t need any system level packages, such as MySQL-related packages, you can skip this step.
It is common to use Debian/Ubuntu based images, so the first thing you would do is likely just update and install some system packages that you need with the
The official python image is also Debian-based, so we can easily write something like this:
RUN apt-get update && apt-get install -y mysql-client
RUN command executes any provided commands in a new layer on top of the current image.
One thing to keep in mind is, docker images have multiple layers; the more layers, the bigger the file size. That’s why it is common to use
&& to concatenate commands into one line in order to reduce layers.
Now we can finally install python dependencies. I recommend a two-step method here – first copy just the
requirements.txt and install all the dependencies, and then copy the code.
This might seem counter intuitive. If
requirements.txt is part of my code, why can’t I copy everything at once? The reason is to save build time. When you’re developing, the code will change in almost every commit. Since docker is built on layers, if any layers change, all the layers on top of it need to change. This means if we copy all the code in this layer ans run
pip install in next layer, everytime the code changes, you need to re-install everything when building a new image. What about if we reverse it? If we only install dependencies now, and then copy the code in next layer, as long as your
requirements.txt stays the same, you don’t have to rebuild this layer again. In rapid development process, this saves a lot of time.
COPY requirements.txt .
COPY command adds a file or directory from the host machine into the image. Here we need it to copy the
requirements.txt to access it inside the image.
We’re almost there! The next step is to copy the code.
WORKDIR command sets the working directory, similar to
cd command we use in terminal.
ADD command lets you add the whole directory to docker image. It is similar to
ADD also supports adding from an external URL. Here I’m assuming you want to add everything in current working directory on host machine into the image, thus the
Now that we have everything, we just need a way to start running the app when we create a container from image.
Usually for web apps, it can be as simple as:
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
But of course, there are some slight improvements that can be done. For example, we should never use the
python manage.py runserver for production. I personally use
Upon starting the server, you might also want to run some other commands too, such as migrations or create admin user accounts, etc. Therefore, I recommend writing your own
start.sh script and calling it from Dockerfile.
start.sh includes something like:
python manage.py migrate
python manage.py collectstatic --noinput
gunicorn mysite.wsgi:application --bind 0.0.0.0:8000
Finally, don’t forget to expose the correct port. Since
gunicorn is running on port 8000, I have to expose this port by:
Don’t worry, if you don’t want your server to listen on this port, you can map it to other ports later.
Now your Dockerfile should be something like this:
RUN apt-get update && apt-get install -y mysql-client
COPY requirements/prod.txt .
RUN pip install prod.txt
ADD . /app
You can run:
docker build -t <image_name>:<tag> .
If it runs successfully, you will have a docker image with the name you provided.
But how do we run a container with the image we just built?
You need to be able to answet these questions first:
- What port do you want your service to run on?
- Is there any extra file you want to add in?
For example, for me, I always want my web app on run on port 80. I have a secret
.env file that includes credentials I need which is not included in commits, which I need to add in just before the server starts. I can run the container by the below command:
docker run -v ~/.env:/app/.env -p 80:8000 -t <image_name>:<tag>
I like to use
-t to show the logs before ctrl+C out of it, but you can also use
-d for detached mode. If you do not use either, you will find yourself not able to interrupt it and will have to close the terminal, which is not ideal if you’re SSHing into other machines.
There are a lot of other flags that you can use, check the documentation for more info.
In the next tutorial, I will talk about how to use multi-stage building to reduce image size.