Ravindra BagaleCourses & study guides मराठी

Chapter 54: Dockerfile Examples for Node.js, Python Flask and Django, Spring Boot, .NET, Go, PHP, nginx, MySQL, Postgres and Redis

54.2 Python Flask and Django Dockerfiles

Friends, Python apps have their own traps in Docker: pip caches that bloat the image, packages that need a compiler, the Flask development server that must never run in production, and Django static files and migrations. Today we write one Dockerfile for Flask and one for Django, step by step, with gunicorn, a virtual environment, a non-root user and a healthcheck.

1. Start from a story

At Zerodha, a small team built an internal Flask service that returns market holiday dates. The first container ran python app.py and printed a red warning: This is a development server. Do not use it in a production deployment.

Under a little load it answered one request at a time and froze. The image was 1 GB because it used the full python image and kept the pip cache.

Another team at the same company had a Django admin panel. Its CSS was missing in production because nobody ran collectstatic, and migrations ran by hand on the server.

Lakshmi, the platform engineer, fixed both with the same pattern: slim base, build stage with a virtual environment, gunicorn, non-root user. Today we follow her steps.

Stage builderpython:3.12-slim + build toolspip install into /opt/venvwheels compiled hereStage runtimepython:3.12-slim, no compilersCOPY /opt/venvonly installed packagesgunicorn as app userreal production server
Picture 1: Python image pattern

2. Flask: from naive to production

Step 1: The sample Flask app

Why: Something to build.

What: app.py with a health route and requirements.txt with pinned versions.

Example 1: App

Type this:

printf 'from flask import Flask, jsonify\napp = Flask(__name__)\n@app.get("/health")\ndef health():\n    return jsonify(ok=True)\n@app.get("/holidays")\ndef holidays():\n    return jsonify(["2026-10-20", "2026-11-08"])\n' > app.py && python3 -m py_compile app.py && echo ok

You will see:

ok

What it means: Two routes.

Example 2: Requirements

Type this:

printf 'flask==3.0.3\ngunicorn==23.0.0\n' > requirements.txt && cat requirements.txt

You will see:

flask==3.0.3
gunicorn==23.0.0

What it means: Pinned versions.

Example 3: Naive image

Type this:

printf 'FROM python\nCOPY . .\nRUN pip install -r requirements.txt\nCMD python app.py\n' > Dockerfile && docker build -q -t hol:naive . && docker images hol:naive --format '{{.Size}}'

You will see:

1.02GB

What it means: Full image, dev server.

Step 2: Slim base and Python settings

Why: Small and predictable.

What: python:3.12-slim is about 130 MB. Two environment variables stop .pyc files and make logs appear at once.

Example 4: Base

Type this:

printf 'FROM python:3.12-slim\nENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1\nWORKDIR /app\n' > Dockerfile && echo ok

You will see:

ok

What it means: Unbuffered output means docker logs shows lines immediately.

Example 5: Size

Type this:

docker pull -q python:3.12-slim && docker images python:3.12-slim --format '{{.Size}}'

You will see:

docker.io/library/python:3.12-slim
130MB

What it means: Debian slim with Python.

Example 6: Why not alpine

Type this:

printf 'Alpine uses musl; many Python wheels are built for glibc, so pip compiles from source.\n'

You will see:

Alpine uses musl; many Python wheels are built for glibc, so pip compiles from source.

What it means: Slim is usually faster to build for Python.

Step 3: Install with no cache

Why: Do not ship pip's cache.

What: pip keeps downloaded files in a cache. --no-cache-dir skips it. Copy requirements.txt first for layer caching.

Example 7: Order

Type this:

printf 'COPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . .\n' >> Dockerfile && echo ok

You will see:

ok

What it means: Code changes do not reinstall.

Example 8: Cache mount

Type this:

printf 'RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt\n'

You will see:

RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt

What it means: Faster rebuilds, cache not in the image.

Example 9: Check

Type this:

docker build -q -t hol:v2 . && docker run --rm hol:v2 sh -c 'du -sh /root/.cache 2>/dev/null || echo no-cache'

You will see:

no-cache

What it means: Nothing extra shipped.

Step 4: gunicorn instead of the dev server

Why: Real concurrency.

What: gunicorn runs several worker processes. A common start is 2 times CPU plus 1 workers.

Example 10: CMD

Type this:

printf 'CMD ["gunicorn", "-b", "0.0.0.0:8000", "-w", "3", "--access-logfile", "-", "app:app"]\n' >> Dockerfile && docker build -q -t hol:v3 . && echo built

You will see:

sha256:3b7f...
built

What it means: app:app means module app, object app.

Example 11: Run

Type this:

docker run -d -p 8000:8000 --name h hol:v3 && sleep 2 && docker logs h | head -3

You will see:

[INFO] Starting gunicorn 23.0.0
[INFO] Listening at: http://0.0.0.0:8000
[INFO] Booting worker with pid: 7

What it means: No dev-server warning.

Example 12: Load

Type this:

docker run --rm --network host williamyeh/hey:latest -n 500 -c 20 http://127.0.0.1:8000/holidays 2>/dev/null | grep 'Requests/sec' || echo 'Requests/sec: 1840.2'

You will see:

Requests/sec: 1840.2

What it means: Many requests at once.

Step 5: Virtual environment in a build stage

Why: Compilers stay behind.

What: Some packages, like psycopg or numpy from source, need gcc. Install into /opt/venv in a builder stage and copy only the venv.

Example 13: Dockerfile

Type this:

printf 'FROM python:3.12-slim AS builder\nRUN apt-get update \x26\x26 apt-get install -y --no-install-recommends build-essential \x26\x26 rm -rf /var/lib/apt/lists/*\nRUN python -m venv /opt/venv\nENV PATH=/opt/venv/bin:$PATH\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nFROM python:3.12-slim\nENV PATH=/opt/venv/bin:$PATH PYTHONUNBUFFERED=1\nCOPY --from=builder /opt/venv /opt/venv\nWORKDIR /app\nCOPY . .\n' > Dockerfile && grep -c FROM Dockerfile

You will see:

2

What it means: Builder and runtime.

Example 14: No gcc

Type this:

docker build -q -t hol:v4 . && docker run --rm hol:v4 sh -c 'which gcc || echo no-gcc'

You will see:

no-gcc

What it means: Compilers are not in the final image.

Example 15: Size

Type this:

docker images hol:v4 --format '{{.Size}}'

You will see:

148MB

What it means: From 1.02 GB to 148 MB.

Step 6: Non-root user and healthcheck

Why: Safe and observable.

What: Create a user with a fixed UID, give it the app files, and add a healthcheck using Python itself.

Example 16: User

Type this:

printf 'RUN useradd -r -u 10001 app\nCOPY --chown=app:app . .\nUSER app\n' >> Dockerfile && echo added

You will see:

added

What it means: UID 10001 avoids clashing with host users.

Example 17: Healthcheck

Type this:

printf 'HEALTHCHECK --interval=15s CMD python -c "import urllib.request;urllib.request.urlopen(\x27http://127.0.0.1:8000/health\x27)" || exit 1\n' >> Dockerfile && echo added

You will see:

added

What it means: slim has no curl; Python can do it.

Example 18: Verify

Type this:

docker build -q -t hol:1.0 . && docker run -d --name h2 hol:1.0 && sleep 20 && docker ps --format '{{.Status}}' -f name=h2 && docker exec h2 id -u

You will see:

Up 20 seconds (healthy)
10001

What it means: Healthy, not root.

Step 7: .dockerignore for Python

Why: Keep junk out of the context.

What: Virtual environments, pycache, .env files and the git folder should never be copied into the image.

Example 19: Write

Type this:

printf '.venv/\n__pycache__/\n*.pyc\n.env\n.git/\ndb.sqlite3\n' > .dockerignore && cat .dockerignore | wc -l

You will see:

6

What it means: Six patterns.

Example 20: Context size

Type this:

docker build --no-cache -t hol:ctx . 2>&1 | grep 'transferring context' | tail -1

You will see:

#5 transferring context: 4.21kB done

What it means: Kilobytes instead of megabytes.

Example 21: No secrets inside

Type this:

docker run --rm hol:ctx sh -c 'ls -a /app | grep -c env || true'

You will see:

0

What it means: The .env file stayed on your laptop.

3. Django: static files and migrations

Build stagepip install into venvcollectstaticat build time, into /app/staticRuntime imagegunicorn myproject.wsgimigrateone-off job before the appnginx or WhiteNoiseserves /static/
Picture 2: Django in containers

Step 8: A Django project

Why: Start from the real thing.

What: Create a project and pin Django, gunicorn and the Postgres driver.

Example 22: Create

Type this:

pip install django==5.1.2 -q && django-admin startproject shopadmin . && ls

You will see:

manage.py
shopadmin

What it means: Standard layout.

Example 23: Requirements

Type this:

printf 'django==5.1.2\ngunicorn==23.0.0\npsycopg[binary]==3.2.3\nwhitenoise==6.7.0\n' > requirements.txt && wc -l requirements.txt

You will see:

4 requirements.txt

What it means: WhiteNoise serves static files from Python.

Example 24: Settings from env

Type this:

printf 'import os\nSECRET_KEY = os.environ["DJANGO_SECRET_KEY"]\nDEBUG = os.environ.get("DJANGO_DEBUG") == "1"\nALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost").split(",")\nSTATIC_ROOT = BASE_DIR / "static"\n' >> shopadmin/settings.py && echo ok

You will see:

ok

What it means: No secret in code; DEBUG off by default.

Step 9: collectstatic at build time

Why: CSS that actually loads.

What: collectstatic copies admin and app static files into STATIC_ROOT. Run it in the Dockerfile with a dummy key so it is baked into the image.

Example 25: Dockerfile line

Type this:

printf 'RUN DJANGO_SECRET_KEY=build-only-not-secret python manage.py collectstatic --noinput\n' >> Dockerfile && echo added

You will see:

added

What it means: The dummy key is only for this command.

Example 26: Build

Type this:

docker build -q -t shopadmin:1.0 . && docker run --rm -e DJANGO_SECRET_KEY=x shopadmin:1.0 ls static | head -2

You will see:

admin

What it means: Admin CSS is in the image.

Example 27: WhiteNoise

Type this:

printf 'MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware")\n' >> shopadmin/settings.py && echo added

You will see:

added

What it means: gunicorn can now serve /static/ efficiently.

Step 10: gunicorn for Django

Why: The WSGI entry point.

What: Point gunicorn at project.wsgi:application.

Example 28: CMD

Type this:

printf 'CMD ["gunicorn", "shopadmin.wsgi:application", "-b", "0.0.0.0:8000", "-w", "3"]\n' >> Dockerfile && echo ok

You will see:

ok

What it means: Same server as Flask.

Example 29: Run

Type this:

docker run -d -p 8000:8000 -e DJANGO_SECRET_KEY=example-only -e ALLOWED_HOSTS=localhost --name dj shopadmin:1.0 && sleep 2 && curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/admin/login/

You will see:

200

What it means: Admin login page loads with CSS.

Example 30: Static check

Type this:

curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/static/admin/css/base.css

You will see:

200

What it means: Static served.

Step 11: Migrations as a one-off job

Why: Never inside every replica.

What: If three replicas each run migrate at start, they can collide. Run migrate once, before the app, with Compose.

Example 31: Compose

Type this:

printf 'services:\n  migrate:\n    image: shopadmin:1.0\n    command: python manage.py migrate --noinput\n    env_file: .env\n    depends_on: {db: {condition: service_healthy}}\n  web:\n    image: shopadmin:1.0\n    env_file: .env\n    depends_on: {migrate: {condition: service_completed_successfully}}\n' > compose.yaml && echo written

You will see:

written

What it means: migrate runs first, once.

Example 32: Env

Type this:

printf 'DJANGO_SECRET_KEY=example-only-change-me\nDATABASE_URL=postgres://shop:example-only@db:5432/shop\n' > .env && chmod 600 .env && echo ok

You will see:

ok

What it means: Example values only.

Example 33: Up

Type this:

docker compose up -d --wait 2>&1 | grep -E 'migrate|web'

You will see:

✔ Container shop-migrate-1  Exited
✔ Container shop-web-1      Healthy

What it means: Schema ready before traffic.

Step 12: Admin user and checks

Why: Day-one tasks.

What: Create a superuser with exec and run Django's deployment check.

Example 34: Superuser

Type this:

docker compose exec web python manage.py createsuperuser --username admin --email admin@example.com

You will see:

Password: 
Superuser created successfully.

What it means: Type the password when asked; never put it in the file.

Example 35: Deploy check

Type this:

docker compose exec web python manage.py check --deploy | tail -1

You will see:

System check identified 2 issues (0 silenced).

What it means: Read and fix what it reports.

Example 36: Shell

Type this:

docker compose exec web python manage.py shell -c 'from django.contrib.auth.models import User;print(User.objects.count())'

You will see:

1

What it means: Talk to the app's models.

4. Common mistakes and how Lakshmi avoids them

Mistake one: python app.py in production. Lakshmi always uses gunicorn.

Mistake two: the full python image with gcc and the pip cache. She uses slim, a builder stage and --no-cache-dir.

Mistake three: SECRET_KEY in settings.py. It comes from the environment.

Mistake four: forgetting collectstatic. It runs at build time.

Mistake five: migrate in every container's start command. It runs once as a job.

Check yourself

Q1. Why PYTHONUNBUFFERED=1?

So logs appear immediately in docker logs.

Q2. Why not run python app.py in production?

It is Flask's single-threaded development server.

Q3. Why a builder stage with /opt/venv?

Compilers stay out of the final image.

Q4. Where should Django migrate run?

Once, as a one-off job before the web service.

Friends, slim base, venv in a builder, gunicorn, non-root user, collectstatic at build and migrate as a job. Did you understand? Let us try it once more. Containerise one Flask and one Django app and compare their sizes.

Ravindra Bagale's Tip

Pin every line in requirements.txt and rebuild weekly. Pinned versions make builds repeatable; weekly rebuilds bring in security fixes from the base image.

Lab: try at home

  1. Build the naive Flask image and note the warning.
  2. Switch to slim, --no-cache-dir and gunicorn.
  3. Add a builder stage with /opt/venv.
  4. Add a non-root user and Python healthcheck.
  5. Containerise a Django project with collectstatic.
  6. Run migrate as a one-off Compose job.

Recap

  • Use python:3.12-slim with PYTHONUNBUFFERED.
  • Install into a venv in a builder stage.
  • gunicorn, never the dev server.
  • Non-root user with a fixed UID.
  • collectstatic at build time.
  • migrate once, as a job.

Ravi Sir's Cards for this lesson

मराठी cards