Writing your first Dockerfile
Every Docker-based environment starts with Dockerfile. Dockerfile is a format description of how to create a Docker image. You can think about the Docker images in a similar way to how you would think about images of virtual machines. It is a single file (composed of many layers) that encapsulates all system libraries, files, source code, and other dependencies that are required to execute your application.
Every layer of a Docker image is described in the Dockerfile by a single instruction in the following format:
INSTRUCTION arguments
Docker supports plenty of instructions, but the most basic ones that you need to know in order to get started are as follows:
- FROM <image-name>: This describes the base image that your image will be based on.
- COPY <src>... <dst>: This copies files from the local build context (usually project files) and adds them to the container's filesystem.
- ADD <src>... <dst>: This works similarly to COPY but automatically unpacks archives and allows <src> to be URLs.
- RUN <command>: This runs specified commands on top of previous layers, and commits changes that this command made to the filesystem as a new image layer.
- ENTRYPOINT ["<executable>", "<param>", ...]: This configures the default command to be run as your container. If no entry point is specified anywhere in the image layers, then Docker defaults to /bin/sh -c.
- CMD ["<param>", ...]: This specifies the default parameters for image entry points. Knowing that the default entry point for Docker is /bin/sh -c, this instruction can also take the form of CMD ["<executable>", "<param>", ...], although it is recommended to define the target executable directly in the ENTRYPOINT instruction and use CMD only for default arguments.
- WORKDIR <dir>: This sets the current working directory for any of the following RUN, CMD, ENTRYPOINT, COPY, and ADD instructions.
To properly illustrate the typical structure of Dockerfile, let's assume that we want to dockerize the built-in Python web server available through the http.server module with some predefined static files that this server should serve. The structure of our project files could be as follows:
.
├── Dockerfile
├── README
└── static
├── index.html
└── picture.jpg
Locally, you could run that Python's http.server on a default HTTP port with the following simple command:
python3.7 -m http.server --directory static/ 80
This example is of course, very trivial, and using Docker for it is using a sledgehammer to crack a nut. So, just for the purpose of this example, let's pretend that we have a lot of code in the project that generates these static files. We would like to deliver only these static files, and not the code that generates them. Let's also assume that the recipients of our image know how to use Docker but don't know how to use Python.
So, what we want to achieve is the following:
- Hide some complexity from the user—especially the fact that we use Python and the HTTP server that's built-in into Python
- Package Python3.7 executable with all its dependencies and all static files primarily available in our project directory
- Provide some defaults to run the server on port 80
With all these requirements, our Dockerfile could take the following form:
# Let's define base image.
# "python" is official Python image.
# The "slim" versions are sensible starting
# points for other lightweight Python-based images
FROM python:3.7-slim
# In order to keep image clean let's switch
# to selected working directory. "/app/" is
# commonly used for that purpose.
WORKDIR /app/
# These are our static files copied from
# project source tree to the current working
# directory.
COPY static/ static/
# We would run "python -m http.server" locally
# so lets make it an entry point.
ENTRYPOINT ["python3.7", "-m", "http.server"]
# We want to serve files from static/ directory
# on port 80 by default so set this as default arguments
# of the built-in Python HTTP server
CMD ["--directory", "static/", "80"]
Let's take a look at how to run containers in the next section.