6 Things To Know When Dockerizing Python Apps in Production

A checklist to run Docker containers with Python in a production environment

6 Things To Know When Dockerizing Python Apps in Production
Photo by J. Kelly Brito on Unsplash.

In a previous article, I wrote a checklist for running Node.js applications in Docker in a production environment.

Recently, I have been busy developing Python applications. If you compare Node.js and Docker against Python and Docker, you see that many things are the same. But there are differences worth noting as well.

As such, I rewrote my checklist. This one is for running Docker containers with Python in a production environment.

You can find all the examples in this GitHub repository.


1. Choose the Correct Base Image

It’s essential to choose the right base Docker image for your Python application. It depends on the application, but always try to use the official Docker images, as they have excellent documentation, use best practices, and are designed for the most common use cases.

There are still many images to choose from if you look at the official Python Docker images. Most of the time, I choose the image with the smallest size to run my Python app. If you want to run your Python 3.8 app on 64-bit Linux, these are some of the options:

  • python:3.8-buster→ 883 MB
  • python:3.8-slim-buster → 114 MB
  • python:3.8-alpine → 43 MB

The Docker team based the Alpine images on the Alpine Linux distribution, a minimal Linux distribution with a hardened kernel. If your app works on Alpine, you should use it as your base image, resulting in the smallest image size.


2. Use a Non-Root Container User

By default, your app runs inside the container as root. However, root in a container is not the same as root on the host. Docker restricts users in containers. But to decrease the security-attack surface, you’d want to run the container as an unprivileged user wherever possible.

Some official images already create a non-root user in their Docker image, but most of the time, you have to add the user yourself in the Dockerfile. See the Dockerfile below for an example of adding and using the Python user:

On the first line, I use the python:3.7.8-alpine image as the base image. The statement on line 6 adds the Python user. The instruction on line 9 creates the directory for my app. I explicitly set the new directory owner to the Python user, as I’m still running as a root user.

The WORKDIR instruction sets the working directory and creates the folder. However, there’s no way to set the directory owner.

I set the user to the Python user on line 12. The USER instruction sets the username (or UID) when running the image. After the Dockerfile selects the user, I execute pip install to install the dependencies of my app. This command will run as the Python user. On line 22, I copy the source of my application to the working folder.

On the last line, the application uses the CMD instruction. This provides the defaults for executing the container.


3. Starting and Stopping Your Python App

It’s crucial to start and stop your Python app inside the Docker container in the correct way. In the Dockerfile shown before, I start the app using the CMD instruction.

When you start your Python app using the exec form of the CMD instruction, you ensure that you can receive signals from the operating system (e.g. SIGINT and SIGTERM) inside your application and handle them to gracefully shut down your application.

If you ignore these signals and stop your container, Docker will wait for ten seconds (default timeout) for your app to respond. If your app does not answer, it’ll kill your Python process and stop the container. So the first thing you want to do is react to the SIGINT and SIGTERM commands and gracefully shut down your application:

The code sample above shows how you can implement responding to the SIGINT and SIGTERM signals. In this example, I exit the process by calling stop(), but this is also the place for cleaning up resources.

If you don’t have access to the source code or do not want to change the source code of an application, there are two different options to shut down your app.

The first is using --init. This flag shows to Docker that it should use an init process as the PID 1 in the container. You use it like this when starting your container:docker run --init -d yourpythonappimage

Your container will then directly react to Ctrl-C or Docker stop commands.

Another more permanent option is to add tini to your Dockerfile and include it in your image. This is also what Docker is doing in the background when you are using the --init flag. Install tini in your image and use ENTRYPOINT to start and wrap the CMD. See the example below:


4. Health Checks

The HEALTHCHECK instruction inside your Dockerfile tells Docker how to validate that a container is still working. This can detect cases such as a web server that’s stuck in an infinite loop and unable to handle new connections even though the server process is still running.

You must implement the functionality to perform the health check, as Docker doesn’t know when your app is functioning correctly or not. I usually add a different route to my server specifically for handling health requests.

There are several packages that can help with implementing a health check with Python. I’ve listed two below:

Most of these packages implement the receiving end of the health check. You still have to add the actual request to the Dockerfile. If you have an HTTP endpoint in your Python application, you can perform the health check using curl. For example, by using the following HEALTHCHECK command:

--interval=21s --timeout=3s --start-period=10s

5. Logging From Your Python Application

Logging from a Python application when running in a Docker container is simple: Log to stdout or stderr, depending on the situation.

The rationale behind this is to let something else handle the logging. Letting something else log makes sense, as we mostly use Docker containers in microservice architectures, where you distribute responsibilities among multiple services.

I wouldn’t recommend using print to write to stdout or stderr from your Python application. Instead, I’d utilize the excellent logging functionality that is available in the standard library.

If you still make use of the print function to log, start Python with the -u parameter. This makes sure that Python won’t buffer the message. So the command to start your Python app in Docker will look like this:CMD ["python", "-u", "main.py"]


6. Configuration Using Environment Variables

When your app is running inside a Docker container, Docker expects you to do all the configuration using environment variables. There is an excellent library called python-dotenv that can help.

This library reads all key-value pairs from a file called .env and adds them to the environment variables. In your Python app, you can read them the same way you would read standard environment variables.

A .env file contains many key value-pairs, with each one on a separate line:# development settings
DB_ADDRESS=localhost:2323
DB_TIMEOUT=4000
LOG_LEVEL=DEBUG

Reading and using dotenv is simple. See the example below:

On line 10, the .env is loaded. The environment variable LOOP, which is defined in the .env file, gets loaded and stored in self.loops. When we use os.getenv to read the variable, we also specify a default value. You can use these default variables to set the values for development.


Conclusion

These are my tips for running your Python applications in Docker containers. You can find all the examples in this GitHub repository.

Thank you for reading. If you have any more tips, let me know. I like to learn.