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.
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.
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 = [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.
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.
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.
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.
requests==2.34.2
The dependencies are listed in the requirements.txt file with
pinned versions, so that each build installs the same packages.
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.
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.
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.
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.
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.
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.
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.
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
In this article we have worked with Python and Docker.
Author
List all Python tutorials.