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.
On this page
Let us package a small Flask web app, the way you would in a real project.
The app#
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
EOFThe Dockerfile#
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"]
EOFWhy each part is there:
python:3.13-slimis a small, specific base image.PYTHONUNBUFFERED=1makes logs appear at once.requirements.txtis copied first, so that dependencies stay cached when your code changes.--no-cache-dirkeeps pip's download cache out of the image.USER appuserruns 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.0makes the server listen on all interfaces. If it listens on127.0.0.1, nothing outside the container can reach it.
Build and run#
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{"host":true,"message":"Hello from Flask"}
{"env":"production","status":"ok"}Check the result#
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/nullappuser
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:
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#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 appuserThe pip install step was cached.
FastAPI#
For an async framework, the Dockerfile is nearly the same. Only the CMD changes:
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
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.045Key takeaway#
Next: Containerising a Node.js App.
Finished this lesson?
Related lessons
Recommended Docker and Kubernetes resources
- Docker Deep Dive by Nigel Poulton
A practical guide to how Docker works, for readers who have finished the basics.
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.