Welcome to our Docker blog post series. If you're following along, you've seen how Docker helps in managing containers, running web servers, and handling container lifecycle and restart policies. Now, we're going to dive into an incredibly useful concept, the Dockerfile.
A Dockerfile is a text document that contains all the instructions a user could tell Docker to assemble an image. Using a Dockerfile, you can automate the process of building, packaging, and configuring an application or service, ensuring consistency and efficiency across different stages of development and deployment. In this post, we'll explore how to containerize an app using a Dockerfile, turning your code into something that can be easily run and managed by Docker.
Containerizing a Simple Python Script with Docker
print('Hello World')
Think of a scenario where you have an awesome Python script that does some amazing thing (just kidding, it only prints out "Hello World", hehe – but that's awesome, right?). The script works perfectly on your computer, and now you want to send it to your colleagues for them to test.
Here's the problem, they might have different versions of Python, or some of them might not even have Python installed at all. How can you make sure it will run consistently across all of their machines?
Of course, we know Docker can be used, but then you'd want to send the commands that they'd need to run, and it might get a bit messy. Fortunately, there's a slightly better way to ensure that everyone can run your script without any hassle.
This is where the Dockerfile comes in. Within the Dockerfile, you can describe the application and tell Docker how to build an image from it. By using a Dockerfile, you encapsulate all the necessary information in a single file, ensuring that your Python script will run the same way on any system that has Docker installed, no matter what version of Python they have or even if they don't have Python at all. Let's look at a very basic dockerfile. The name of the file has to be Dockerfile
without any extensions.
FROM python:3
WORKDIR /app
COPY ./test_script.py .
CMD [ "python", "./test_script.py" ]
FROM python:3
: This line specifies the base image that Docker will use to build the new image. In this case, it's using the official Python 3 image from the Docker Hub. This image comes with Python 3 pre-installed, so you don't have to worry about installing it yourself.WORKDIR /app
: This command sets the working directory inside the container to/app
. All subsequent commands (likeCOPY
andCMD
) will be run from this directory. If the directory doesn't exist, Docker will create it.COPY ./test_script.py .
: This line copies thetest_script.py
file from your local machine (the current directory) into the working directory inside the container (which is/app
, as set by theWORKDIR
command). The.
at the end signifies the current directory inside the container.CMD [ "python", "./test_script.py" ]
: Finally, this line defines the command that Docker will run when the container is started. In this case, it's executing the Python interpreter on thetest_script.py
file, effectively running your script.
The Dockerfile takes a simple approach: it starts from an existing Python 3 image, sets up the working directory, copies in the script, and then runs the script when the container starts. By defining these steps in a Dockerfile, you can ensure that anyone who builds a Docker container from this Dockerfile will have the same environment, dependencies, and execution behaviour, making it easy to share and run the code across different systems.
Docker Build - Create Image
To demonstrate this, navigate to the directory that has both of the above files. Run the docker build
command as shown below. Please note that you named the Dockerfile without any extensions.
PS C:\Users\vsurr\Documents\python_dockerfile> docker build -t py_image:v1 .
[+] Building 0.9s (8/8) FINISHED
{......TRUNCATED......}
=> => naming to docker.io/library/py_image:v1
The docker build
command is used to build a Docker image from a Dockerfile. In this specific command:
-t py_image:v1
: This part of the command names the image and assigns a tag to it. The namepy_image
is an identifier that you can use when you want to refer to the image later. Thev1
part is a tag that can be useful for versioning. If you don't specify a tag, Docker will use thelatest
tag by default..
: This is the path to the directory containing the Dockerfile. In this case, the.
means the current directory. Docker will look for a file namedDockerfile
in this directory and use it to build the image.
So the whole command can be read as: "Build a Docker image using the Dockerfile in the current directory and name the resulting image py_image
with the tag v1
."
After running this command, the image will be available on your local system, and you can use it to create containers with the exact environment and behaviour defined in the Dockerfile.
PS C:\Users\vsurr\Documents\python_dockerfile> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
py_image v1 8a59595dffed 25 minutes ago 1.01GB
Docker Run - Container
docker run --name py_container py_image:v1
Hello World
--name py_container
: This part of the command names the containerpy_container
. This name can be used to refer to the container in other Docker commands, likedocker stop
ordocker start
.py_image:v1
: This specifies the image from which the container should be created. In this case, it's the image namedpy_image
with the tagv1
that you created earlier with thedocker build
command.
So, the entire command can be read as: "Create and start a new container named py_container
, using the py_image:v1
image.
When you run this command, Docker will create a new container from the specified image, and the command defined in the Dockerfile CMD [ "python", "./test_script.py" ]
will be executed. In this case, that command runs a Python script, so you see the output Hello World
printed to the terminal.
Including Dependencies with requirements.txt
In our previous example, we successfully ran a simple Python script inside a Docker container. Now, we're going to take a step further and include a requirements.txt
file to manage the necessary packages for our script. In many real-world scenarios, scripts depend on external libraries, and managing these dependencies across different environments can be a challenge. In this case, we'll be using the popular requests
library in our script.
The requirements.txt
file is a standard way to define the dependencies in Python projects, and it can be effortlessly utilized within a Docker container. Before we dive into the Dockerfile and build commands, let's first take a look at the contents of the requirements.txt
file and the new version of our Python script.
certifi==2023.7.22
charset-normalizer==3.2.0
idna==3.4
requests==2.31.0
urllib3==2.0.4
import requests
output = requests.get('https://httpbin.org/get')
print(output.json())
New Dockerfile
FROM python:3
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD [ "python", "./api_example.py" ]
FROM python:3
- As before, this sets the base image to Python 3.WORKDIR /app
- This instruction also remains the same, setting the working directory inside the container to/app
.COPY . .
- This time, we're copying all the files from the current directory on the host machine into the working directory inside the container. This includes both the Python script (api_example.py
) and therequirements.txt
file.RUN pip install --no-cache-dir -r requirements.txt
- This is a new instruction that executes the pip command to install the dependencies listed in therequirements.txt
file. The--no-cache-dir
option is used to avoid storing the cache, keeping the image size down.CMD [ "python", "./api_example.py" ]
- This command remains the same but points to the new script,api_example.py
, which will be executed when the container starts.
This Dockerfile builds upon our previous example by adding a layer of complexity, showing how you can easily manage dependencies within a Docker container. It ensures that the specific packages listed in the requirements.txt file are installed, creating a consistent environment across different systems.
Build the Image and Run the Container
Similar to the previous example, let's build the image and run the container.
docker build -t api_image:v1 .
[+] Building 3.7s (9/9) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
{......TRUNCATED......}
=> => naming to docker.io/library/api_image:v1
docker run --name api_script api_image:v1
{'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip,
deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.31.0',
'X-Amzn-Trace-Id': 'Root=1-64d1ed3c-3c7772f3194880982ab563cf'},
'origin': '82.31.82.173',
'url': 'https://httpbin.org/get'}
When we run the container, it executes the Python script inside the container. This script uses the requests
library to connect to the specified URL and fetch a response. The response, returned in JSON format, contains various details such as the headers, origin, and URL.
By including the requirements.txt
file in the Dockerfile, we have ensured that the necessary library is installed in the container, allowing the script to execute consistently across different environments.
Building Custom Docker Images with Additional Utilities
Let's look at another example. We have been using the httpd containers in our examples, but the container doesn't come with some network utilities that we may use most of the time. So, we can't run commands like ping
or ip addr
. I have to install them every time I spin up a new container. Let's look at how we can build an image that includes both of them.
The solution is to create a custom Docker image that builds upon the official httpd image but adds our required networking tools. As usual, create a Dockerfile with the following contents.
FROM httpd:latest
# Install network utilities
RUN apt-get update && \
apt-get install -y iproute2 iputils-ping && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
- Start with the official httpd image (
FROM httpd:latest
) - Run commands to update package lists and install our desired tools
- Clean up unnecessary files to keep the image size smaller
Next, build an image from this Dockerfile. I'm going to give this image a new name, but you can go with whatever you like.
docker build -t httpd-nettools .
Once this image has been created, we can launch a new container using this new image. Within this container, you should be able to run the commands ping
or ip add
docker run -d -p 8080:80 --name web-server httpd-nettools
docker exec -it web-server /bin/bash
root@eedb6d4b429c:/usr/local/apache2# ip ad
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
link/ether ae:4a:b6:82:a1:ca brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
root@eedb6d4b429c:/usr/local/apache2# ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=55 time=22.8 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=55 time=14.9 ms
^C
--- 1.1.1.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 14.870/18.835/22.801/3.965 ms
Conclusion
The examples we explored in this post focused on Python and a simple exmaple, but of course, the Dockerfile is not limited to just one language or simple scripts. It can be used to containerize more complex applications across various programming languages and frameworks.