Skip to content
Laviyx Learn

Try

Docker and Kubernetes course menu

How to Dockerize a Python Flask App

Write a production-ready Dockerfile for a Python web app: a slim base image, cached dependencies, a non-root user and gunicorn, with a real Flask example.

Beginner6 min readUpdated
On this page

Let us package a small Flask web app, the way you would in a real project.

The app#

Terminal
mkdir flask-app
cd flask-app
cat > app.py <<'EOF'
import os
import socket

from flask import Flask, jsonify

app = Flask(__name__)


@app.get("/")
def home():
    return jsonify(message="Hello from Flask", host=socket.gethostname() != "")


@app.get("/health")
def health():
    return jsonify(status="ok", env=os.environ.get("APP_ENV", "development"))
EOF
cat > requirements.txt <<'EOF'
flask==3.1.*
gunicorn==23.*
EOF
cat > .dockerignore <<'EOF'
__pycache__
*.pyc
.venv
.env
.git
EOF

The Dockerfile#

Terminal
cat > Dockerfile <<'EOF'
FROM python:3.13-slim

ENV PYTHONUNBUFFERED=1
WORKDIR /app

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

COPY . .

RUN useradd --create-home appuser
USER appuser

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
EOF

Why each part is there:

  • python:3.13-slim is a small, specific base image.
  • PYTHONUNBUFFERED=1 makes logs appear at once.
  • requirements.txt is copied first, so that dependencies stay cached when your code changes.
  • --no-cache-dir keeps pip's download cache out of the image.
  • USER appuser runs the app as a normal user, not root.
  • gunicorn is a production server. Flask's built-in server is only for development.
  • 0.0.0.0 makes the server listen on all interfaces. If it listens on 127.0.0.1, nothing outside the container can reach it.

Build and run#

Terminal
docker build -q -t flask-demo:1.0 . > /dev/null
docker run -d --name api -p 8000:8000 -e APP_ENV=production flask-demo:1.0 > /dev/null
sleep 3
curl -s http://localhost:8000/
curl -s http://localhost:8000/health
Output
{"host":true,"message":"Hello from Flask"}
{"env":"production","status":"ok"}

Check the result#

Terminal
docker exec api whoami
docker images flask-demo --format '{{.Tag}} {{.Size}}'
docker logs api 2>&1 | grep -i 'listening' | sed 's/^\[[^]]*\] //' | cut -c1-50
docker rm -f api > /dev/null
Output
appuser
1.0 133MB
[1] [INFO] Listening at: http://0.0.0.0:8000 (1)

Rebuilds are fast#

Change the app, and only the last steps run:

Terminal
echo "# changed" >> app.py
docker build --progress=plain -t flask-demo:1.1 . 2>&1 | grep -E '^#[0-9]+ (CACHED|\[[0-9])' | cut -c1-60
Output
#4 [1/6] FROM docker.io/library/python:3.13-slim@sha256:ae14
#6 [2/6] WORKDIR /app
#6 CACHED
#7 [3/6] COPY requirements.txt .
#7 CACHED
#8 [4/6] RUN pip install --no-cache-dir -r requirements.txt
#8 CACHED
#9 [5/6] COPY . .
#10 [6/6] RUN useradd --create-home appuser

The pip install step was cached.

FastAPI#

For an async framework, the Dockerfile is nearly the same. Only the CMD changes:

DOCKERFILE
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Pin your versions#

flask==3.1.* still allows changes. For repeatable builds, use a lock file generated with pip freeze, pip-tools or uv.

Common mistakes#

Practice#

Practice exercise

Build a python:3.13-slim image that runs a script printing sum(range(10)), without any web server.

Show solution
Terminal
mkdir ~/calc && cd ~/calc
echo "print(sum(range(10)))" > calc.py
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY calc.py .
CMD ["python", "calc.py"]
EOF
docker build -q -t calc:1.0 . > /dev/null
docker run --rm calc:1.0
Output
45

Key takeaway#

Next: Containerising a Node.js App.

Finished this lesson?

Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.