ZetCode

Python Docker

last modified October 30, 2026 Python 3.15

In this article we show how to use Docker for Python applications.

Docker

Docker is a platform for developers and sysadmins to build, run, and share applications with containers. Containers isolate an application with its libraries and runtime, which eliminates many issues caused by differences between environments. Docker helps automate development and deployment.

A Docker image is a read-only template with instructions for creating a Docker container. A Docker container is a runnable instance of an image. Containers share the kernel of the host; unlike virtual machines, they do not run their own operating system kernel.

Docker images are stored in registries. Docker Hub is the default public registry; it hosts the official python images. Docker Engine is the client-server technology that builds and runs containers.

A Dockerfile is a text file which contains the instructions needed to build a Docker image.

$ docker --version
Docker version 29.1.3, build 29.1.3-0ubuntu3~22.04.2

Hello example

In the following example, we create and run a very simple Docker image. When we run the image, a Python program is executed.

hello/hello.py
import platform
import sys

print('hello there!')
print(sys.version)
print(platform.platform())

This is the program to be executed inside the container. It prints the Python version and the platform.

hello/Dockerfile
FROM python:3.15-slim
WORKDIR /app
COPY hello.py .
CMD ["python", "hello.py"]

These are the instructions to build the Docker image.

FROM python:3.15-slim

We base our image on the official python:3.15-slim image. The tag selects the Python version and the image variant.

WORKDIR /app
COPY hello.py .

The WORKDIR instruction sets the working directory for the following instructions and for the running container; the directory is created if it does not exist. The COPY instruction copies the hello.py file from the build context into it.

CMD ["python", "hello.py"]

The CMD instruction sets the default command of the container. The exec form, a JSON array, runs the program directly without a shell.

We build the image with docker build and name it pyhello; the last argument is the build context, the directory whose files are available to the build. The -q option prints only the image ID. Then we list the image and run it. The --rm option removes the container when it exits.

The platform string shows the kernel of the host, since the container does not have its own.

$ docker build -q -t pyhello hello
sha256:e2a17b089d0ca14ae69b1b2505b00ca409dec5de8629f764ea57a968bdd915fa
$ docker image ls --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}' pyhello
REPOSITORY   TAG       IMAGE ID       SIZE
pyhello      latest    e2a17b089d0c   123MB
$ docker run --rm pyhello
hello there!
3.15.2 (main, Oct 28 2026, 01:58:57) [GCC 14.2.0]
Linux-6.8.0-138-generic-x86_64-with-glibc2.41

Image variants

The official Python images come in several variants. The default image is based on a full Debian system with compilers and many libraries, which are needed to build some packages from source. The slim variant contains only the packages needed to run Python, so it is much smaller.

$ docker image ls --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}' --filter reference='python:3.15*'
REPOSITORY   TAG         SIZE
python       3.15        1.13GB
python       3.15-slim   123MB

Running one-off commands

We do not need to build an image to run Python code. We can pass a command to the official image.

squares.py
squares = [n * n for n in range(1, 11)]
print(squares)
print(sum(squares))

This program will be fed to the container on its standard input.

In the first command, we run a one-liner with python -c. In the second, the -i option keeps the standard input open, so the container's Python reads the program from the redirected file.

For an interactive session, we add the -t option, which allocates a pseudo-terminal: docker run -it --rm python:3.15-slim starts the Python REPL and docker run -it --rm python:3.15-slim bash starts a shell inside the container.

$ docker run --rm python:3.15-slim python -c "import sys; print(sys.version_info)"
sys.version_info(major=3, minor=15, micro=2, releaselevel='final', serial=0)
$ docker run --rm -i python:3.15-slim python < squares.py
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
385

Bind mounts

A bind mount makes a host directory available inside the container. This way, we can run programs from the current directory without building an image.

words.py
from collections import Counter
from pathlib import Path

text = Path('data/thermopylae.txt').read_text()
words = Counter(text.lower().replace(',', '').replace('.', '').split())

for word, count in words.most_common(3):
    print(word, count)

Path('data/report.txt').write_text(f'{len(words)} distinct words\n')

The program counts the words in a text file and writes a report into the data directory.

The -v "$PWD":/app option mounts the current directory at /app and -w /app makes it the working directory. Files written by the container appear on the host. Containers run as root by default; with the --user option, the report is owned by our user instead of root.

$ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/app -w /app python:3.15-slim python words.py
of 6
the 4
persian 2
$ cat data/report.txt
29 distinct words

Environment variables

Containers are usually configured with environment variables.

config.py
import os

env = os.environ.get('APP_ENV', 'development')
workers = int(os.environ.get('WORKERS', '1'))

print(f'environment: {env}, workers: {workers}')

The program reads two variables with default values.

The -e option sets an environment variable in the container. Default values can also be set in the Dockerfile with the ENV instruction.

$ docker run --rm -v "$PWD":/app -w /app python:3.15-slim python config.py
environment: development, workers: 1
$ docker run --rm -e APP_ENV=production -e WORKERS=4 -v "$PWD":/app -w /app python:3.15-slim python config.py
environment: production, workers: 4

Installing dependencies

In the next example, we build an image which retrieves a web page with the requests library. The library must be installed in the image.

getreq/get_req.py
import re

import requests

resp = requests.get('https://example.com', timeout=10)

print(f'requests {requests.__version__}')
print(resp.status_code, resp.headers['content-type'])

title = re.search(r'<title>(.*?)</title>', resp.text).group(1)
print(f'title: {title}')

The program sends a GET request and prints the status code, the content type, and the title of the page.

getreq/requirements.txt
requests==2.34.2

The dependencies are listed in the requirements.txt file with pinned versions, so that each build installs the same packages.

getreq/Dockerfile
FROM python:3.15-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY get_req.py .
CMD ["python", "get_req.py"]

This is the Dockerfile.

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY get_req.py .

Each instruction creates a layer, and Docker reuses cached layers when their inputs have not changed. We copy the requirements file and install the packages before copying the program; when we edit only the program, the installation step is taken from the cache. The --no-cache-dir option keeps pip's download cache out of the image.

The image is only slightly bigger than the slim base image. Files that should not be sent to the build, such as virtual environments or the .git directory, can be excluded with a .dockerignore file.

$ docker build -q -t pygetreq getreq
sha256:32599693c27db81a775af1cc3e347cf15551d9c67d5630a9be476fde0f97374c
$ docker image ls --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}' pygetreq
REPOSITORY   TAG       SIZE
pygetreq     latest    135MB
$ docker run --rm pygetreq
requests 2.34.2
200 text/html; charset=utf-8
title: Example Domain

Flask application

In the next example, we run a Flask web application in a Docker container.

flaskapp/app.py
import os
import socket
import sys

from flask import Flask

app = Flask(__name__)


@app.route('/')
def home():
    return 'Hello there!\n'


@app.route('/info')
def info():
    return {
        'hostname': socket.gethostname(),
        'python': sys.version.split()[0],
        'user': os.getuid(),
    }

The application has two routes. The /info route returns the host name of the container, the Python version, and the ID of the user running the application.

flaskapp/requirements.txt
flask==3.1.3
gunicorn==23.0.0

Besides Flask, we install the gunicorn server. The flask run command starts a development server, which is not meant for production.

flaskapp/Dockerfile
FROM python:3.15-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN useradd --create-home appuser
USER appuser
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]

This is the Dockerfile.

RUN useradd --create-home appuser
USER appuser

We create a regular user and run the application under it. If an attacker takes over the application, they do not get root privileges inside the container.

EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]

The EXPOSE instruction documents the port the application listens on; it does not publish it. Gunicorn serves the app object of the app module with two worker processes. We bind to 0.0.0.0; an application listening only on 127.0.0.1 would not be reachable from outside the container.

We run the container in the background with the -d option. The -p 5000:5000 option publishes the container's port 5000 on the host's port 5000. We send requests with curl, list the running containers, and view the container's output with docker logs. The host name of the container is its short ID.

$ docker build -q -t pyflask flaskapp
sha256:a10db49db26fca646d233ad47396d574b27495e7a5db5019c5885be8d9c4c5c7
$ docker run -d --rm -p 5000:5000 --name flaskapp pyflask
5acadb2e5c77bbcf77f4f17bd7524135d6e6f55745a713231bac8885a2663373
$ curl localhost:5000/
Hello there!
$ curl localhost:5000/info
{"hostname":"5acadb2e5c77","python":"3.15.2","user":1000}
$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES      IMAGE     PORTS
flaskapp   pyflask   0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp
$ docker logs flaskapp
[2026-10-30 19:47:41 +0000] [1] [INFO] Starting gunicorn 23.0.0
[2026-10-30 19:47:41 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
[2026-10-30 19:47:41 +0000] [1] [INFO] Using worker: sync
[2026-10-30 19:47:41 +0000] [7] [INFO] Booting worker with pid: 7
[2026-10-30 19:47:41 +0000] [8] [INFO] Booting worker with pid: 8
$ docker stop flaskapp
flaskapp

MariaDB container

Databases are often run in containers during development. In the following example, we start a MariaDB server from the official mariadb image and connect to it from Python on the host.

cities/schema.sql
CREATE TABLE cities(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), population INT);
INSERT INTO cities(name, population) VALUES('Bratislava', 432000);
INSERT INTO cities(name, population) VALUES('Budapest', 1759000);
INSERT INTO cities(name, population) VALUES('Prague', 1280000);
INSERT INTO cities(name, population) VALUES('Warsaw', 1748000);
INSERT INTO cities(name, population) VALUES('Los Angeles', 3971000);
INSERT INTO cities(name, population) VALUES('New York', 8550000);
INSERT INTO cities(name, population) VALUES('Edinburgh', 464000);
INSERT INTO cities(name, population) VALUES('Berlin', 3671000);

This is the SQL code for the cities table.

cities/app.py
import os

import pymysql

con = pymysql.connect(host=os.environ.get('DB_HOST', '127.0.0.1'),
                      user='user7', password='7user', database='testdb')

with con:
    with con.cursor() as cur:
        cur.execute('SELECT VERSION()')
        print(f'server: {cur.fetchone()[0]}')

        cur.execute('SELECT * FROM cities')

        for row in cur.fetchall():
            print(f'{row[0]}, {row[1]}, {row[2]}')

The program connects to the testdb database with the PyMySQL driver and prints the server version and all rows of the cities table. The host is taken from the DB_HOST environment variable.

The MARIADB_* environment variables create a database, a user with access to it, and set the root password. When the container starts with an empty data directory, it runs the SQL scripts in the /docker-entrypoint-initdb.d directory; we mount our schema file there read-only. The server needs a few seconds to initialize before it accepts connections.

The data lives in the container and is lost when it is removed. To keep it, we would mount a named volume, for instance -v citydata:/var/lib/mysql.

$ docker run -d --rm --name pydb -p 3306:3306 -e MARIADB_USER=user7 -e MARIADB_PASSWORD=7user -e MARIADB_DATABASE=testdb -e MARIADB_ROOT_PASSWORD=s3cret -v "$PWD/cities/schema.sql":/docker-entrypoint-initdb.d/schema.sql:ro mariadb:12
b564ef080ac1aa732c035ab70702dfac186eb13e81c8fac5b209293c027e03b9
$ python cities/app.py
server: 12.3.3-MariaDB-ubu2404
1, Bratislava, 432000
2, Budapest, 1759000
3, Prague, 1280000
4, Warsaw, 1748000
5, Los Angeles, 3971000
6, New York, 8550000
7, Edinburgh, 464000
8, Berlin, 3671000

Connecting containers

Containers on the same user-defined network can reach each other by their names. We run the same program in a container and connect it to the database container.

cities/Dockerfile
FROM python:3.15-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]

The image installs PyMySQL and runs the app.py program.

We create the pynet network and connect the running database container to it. The application container joins the same network, and the DB_HOST=pydb variable makes it connect to the database by the container name. Docker's embedded DNS server resolves the name. Finally, we stop the database and remove the network.

$ docker network create pynet
a1f23d5dba9efc850e643ae6d717e5bf1536e70136ec76298db85a9a613cb444
$ docker network connect pynet pydb
$ docker build -q -t pycities cities
sha256:29940c4127c440396bdc8ee5f43f3501e0755cd44b14512e7dc9257c256628ee
$ docker run --rm --network pynet -e DB_HOST=pydb pycities
server: 12.3.3-MariaDB-ubu2404
1, Bratislava, 432000
2, Budapest, 1759000
3, Prague, 1280000
4, Warsaw, 1748000
5, Los Angeles, 3971000
6, New York, 8550000
7, Edinburgh, 464000
8, Berlin, 3671000
$ docker stop pydb
pydb
$ docker network rm pynet
pynet

Docker SDK for Python

The Docker SDK for Python controls Docker Engine from Python programs.

$ pip install docker

Running containers from Python

The following program runs containers and inspects an image with the SDK.

docker_sdk.py
import docker

client = docker.from_env()

output = client.containers.run(
    'python:3.15-slim',
    ['python', '-c', 'import platform; print(platform.python_version())'],
    remove=True,
)
print(f'container output: {output.decode().strip()}')

image = client.images.get('python:3.15-slim')
print(image.tags, f'{image.attrs["Size"] / 1e6:.0f} MB')

container = client.containers.run('python:3.15-slim', ['sleep', '30'],
                                  detach=True, name='pysleeper')
print(container.name, container.status)

container.reload()
print(container.name, container.status)

container.stop(timeout=1)
container.remove()
print(len(client.containers.list(filters={'name': 'pysleeper'})))

The from_env function creates a client configured from the environment, by default connected to the local Docker socket.

output = client.containers.run(
    'python:3.15-slim',
    ['python', '-c', 'import platform; print(platform.python_version())'],
    remove=True,
)

The containers.run method runs a container, waits for it to finish, and returns its output as bytes, like docker run --rm.

container = client.containers.run('python:3.15-slim', ['sleep', '30'],
                                  detach=True, name='pysleeper')

With detach=True, the method returns a Container object immediately. Its attributes are a snapshot; the reload method refreshes them, so the status changes from created to running. We then stop and remove the container.

$ python docker_sdk.py
container output: 3.15.2
['python:3.15-slim'] 123 MB
pysleeper created
pysleeper running
0

Source

Python official images - Docker Hub

Docker SDK for Python

In this article we have worked with Python and Docker.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.