KAHIBARO
Discord Login Register

32.10. Dockerizing the Application

Why Dockerize Your Final Project

For your final project, you want your backend to run the same way:

Docker gives you this consistency by packaging:

into a single image that you can run as a container anywhere Docker is installed.

In this chapter you will:

You already learned Docker earlier, so here you focus on applying it to a real, multi-service backend.

Designing the Container Layout

What will run in containers?

In previous chapters, your final project got these pieces:

A simple layout for now:

ContainerPurpose
appFastAPI backend
workerBackground jobs
dbPostgreSQL database
redisRedis for cache / tasks
nginx (later)Reverse proxy, HTTPS, static

In this chapter you focus on app and worker images, and wiring them to db and redis using Docker Compose.

Creating a Production-Friendly Dockerfile

Basic Dockerfile structure

Here is a typical Dockerfile for a FastAPI / Uvicorn app in your project:

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_DEFAULT_TIMEOUT=100
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
 && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --upgrade pip \
 && pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "project.main:app", "--host", "0.0.0.0", "--port", "8000"]

Important parts:

RULE: Always use a specific Python version tag like python:3.12-slim, not just python:latest. This keeps your builds reproducible.

Aligning the Dockerfile with your project structure

Assume your final project has a structure like:

text
/app
  β”œβ”€β”€ project
  β”‚     β”œβ”€β”€ __init__.py
  β”‚     β”œβ”€β”€ main.py          # FastAPI app
  β”‚     β”œβ”€β”€ config.py
  β”‚     β”œβ”€β”€ db.py
  β”‚     └── ...
  β”œβ”€β”€ requirements.txt
  β”œβ”€β”€ alembic.ini
  β”œβ”€β”€ alembic
  └── scripts
        └── worker.py

Then your CMD should reference the correct module path:

You must match the module path exactly:

File pathFastAPI instanceCorrect Uvicorn target
project/main.pyapp = FastAPI()project.main:app
backend/app.pyapi = FastAPI()backend.app:api
src/api/main.pyapi = FastAPI()src.api.main:api

If you get an error like ImportError: No module named project, your module path or WORKDIR is probably wrong.

Using `.dockerignore`

Without a .dockerignore file, Docker sends everything in your project directory into the build context. This is slow and may copy secrets or build artifacts.

Create .dockerignore next to your Dockerfile:

gitignore
.git
.gitignore
__pycache__
*.pyc
*.pyo
*.pyd
.env
.env.*
.vscode
.idea
.mypy_cache
.pytest_cache
.dist
build
htmlcov
.coverage
node_modules

RULE: Never copy .env or secret files into the image. Keep secrets outside images and pass them as environment variables at runtime.

Building and Running Locally

Building the image

From your project root, build the image:

bash
docker build -t my-final-backend:dev .

Explanation:

You can list images:

bash
docker images

Running the container

Run the application:

bash
docker run --rm -p 8000:8000 my-final-backend:dev

Flags:

Open in a browser:

If your app uses environment variables for DB or Redis, the container might fail because those services are not running yet. That is where Docker Compose helps.

Environment-Based Configuration

Your app should already read configuration from environment variables, for example in project/config.py:

python
from pydantic import BaseSettings, AnyUrl
class Settings(BaseSettings):
    env: str = "development"
    database_url: AnyUrl
    redis_url: AnyUrl
    secret_key: str
    debug: bool = False
    class Config:
        env_file = ".env"  # for local dev only
settings = Settings()

In Docker, do not rely on .env inside the image. Instead pass variables from the host or compose file.

Example environment variables

text
ENV=development
DATABASE_URL=postgresql+psycopg2://user:password@db:5432/app_db
REDIS_URL=redis://redis:6379/0
SECRET_KEY=super-secret-key-change-me
DEBUG=true

Notice db and redis in the URLs. Those will be the service names in Docker Compose.

Using Docker Compose for the Full Stack

Basic `docker-compose.yml` for the final project

Create docker-compose.yml in your project root:

yaml
version: "3.9"
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: final-backend-app
    command: >
      uvicorn project.main:app
      --host 0.0.0.0
      --port 8000
      --reload
    ports:
      - "8000:8000"
    env_file:
      - .env.docker
    depends_on:
      - db
      - redis
  worker:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: final-backend-worker
    command: ["python", "-m", "scripts.worker"]
    env_file:
      - .env.docker
    depends_on:
      - db
      - redis
  db:
    image: postgres:16-alpine
    container_name: final-backend-db
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: app_password
      POSTGRES_DB: app_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
  redis:
    image: redis:7-alpine
    container_name: final-backend-redis
    ports:
      - "6379:6379"
volumes:
  postgres_data:

Key points:

`.env.docker` for compose

Create .env.docker:

env
ENV=development
DATABASE_URL=postgresql+psycopg2://app_user:app_password@db:5432/app_db
REDIS_URL=redis://redis:6379/0
SECRET_KEY=dev-secret-key-change-me
DEBUG=true

Run the stack:

bash
docker compose up --build

Use docker compose or docker-compose depending on your system.

Visit:

To stop:

bash
docker compose down

Database Migrations Inside Containers

Your project likely uses Alembic. You must decide when and where to run migrations.

Option 1: run migrations manually inside app container

After docker compose up -d, run:

bash
docker compose exec app alembic upgrade head

This uses the app container, your DATABASE_URL from .env.docker, and your Alembic config.

Option 2: run migrations at startup

You can wrap migration logic in a small script, for example:

python
# scripts/run_with_migrations.py
import subprocess
from project.main import app  # ensure app imports correctly
def run_migrations():
    subprocess.check_call(["alembic", "upgrade", "head"])
if __name__ == "__main__":
    run_migrations()

Then change command in compose for app:

yaml
command: >
  sh -c "alembic upgrade head &&
         uvicorn project.main:app --host 0.0.0.0 --port 8000"

RULE: Never run destructive migrations (like dropping tables) automatically in production startup without backups or approvals. Prefer safe, forward-only migrations.

Developing With Hot Reload in Docker

For development you may want Uvicorn reload and local code changes reflected without rebuilding the image.

Update docker-compose.yml:

yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/app
    command: >
      uvicorn project.main:app
      --host 0.0.0.0
      --port 8000
      --reload
    # ...

Now the code on your host is mounted into the container at /app. Changes trigger --reload.

Use this only for development. For production, do not mount source code from the host.

Multi-Stage Builds for Lean Production Images

Development images are often bigger, because they include build tools and caches. For production you want smaller and safer images.

Use a multi-stage Dockerfile:

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
 && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --upgrade pip \
 && pip install --prefix=/install -r requirements.txt
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
EXPOSE 8000
CMD ["uvicorn", "project.main:app", "--host", "0.0.0.0", "--port", "8000"]

Explanation:

Result is a smaller, more secure image.

Separate Images for App and Worker (Optional)

You can use one image for both app and worker, as shown. Sometimes you want different Python dependencies for your worker or a different base image.

Example Dockerfile.worker:

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements-worker.txt requirements.txt
RUN pip install --upgrade pip \
 && pip install -r requirements.txt
COPY . .
CMD ["python", "-m", "scripts.worker"]

Then in docker-compose.yml:

yaml
  worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    env_file:
      - .env.docker
    depends_on:
      - db
      - redis

If your worker needs image libraries or other system packages, you add them only to the worker image.

Logging and Observability in Containers

Your application should log to stdout and stderr. Docker captures these streams and you can view logs with:

bash
docker compose logs app
docker compose logs worker
docker compose logs db

To follow logs:

bash
docker compose logs -f app worker

For production, structured logs (JSON) are easier for log aggregation tools.

Example FastAPI logging config snippet:

python
import logging
import sys
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
    stream=sys.stdout,
)

Security Considerations for Dockerizing a Backend

A few important rules for your final project:

RULES FOR SECURE DOCKERIZED BACKENDS

  1. Do not bake secrets into Docker images. Use environment variables or secret managers.
  2. Run your app as a non-root user in the container when possible.
  3. Expose only needed ports.
  4. Use official base images and keep them updated.
  5. Do not mount host directories in production, except where absolutely necessary.
  6. Keep your Dockerfiles small and clear, no unused packages.

Example of running as a non-root user:

dockerfile
RUN adduser --disabled-password --gecos "" appuser
USER appuser

Place USER appuser near the end of your Dockerfile, before CMD.

Building Images for Production and Pushing to a Registry

For production you will:

  1. Build a production-tagged image
  2. Push to a registry (like Docker Hub or GitHub Container Registry)
  3. Pull and run on your production server

Example:

bash
# build with a version tag
docker build -t myorg/final-backend:1.0.0 .
# login to Docker Hub
docker login
# push
docker push myorg/final-backend:1.0.0

In your production deployment chapter you will wire this image into your CI/CD pipeline and server setup.

Checklist for Your Final Project Dockerization

Use this checklist to verify your project is ready:

ItemDone?
Dockerfile builds and runs the FastAPI app[ ]
.dockerignore is present and excludes secrets[ ]
docker-compose.yml runs app, db, and Redis[ ]
worker container is configured and running[ ]
Env vars in .env.docker match your config module[ ]
App can connect to Postgres (db) and Redis[ ]
Database migrations run correctly in containers[ ]
Dev setup supports hot reload (--reload)[ ]
Production image uses a fixed Python version[ ]
No secrets are baked into images[ ]

Once you complete this, your final project backend can be started with a single command:

bash
docker compose up --build

and is ready to be deployed in production using the same images.

Views: 9

Comments

Please login to add a comment.

Don't have an account? Register now!