KAHIBARO
Discord Login Register

23.4. Deploying with Docker

Overview

Deploying with Docker means packaging your backend application and everything it needs into a reusable, portable unit called an image, then running it as a container on any machine that has Docker installed. This chapter focuses on how to take a backend app that already runs locally and ship it using Docker in a simple, reliable way.

You will not learn Docker basics here, like what an image or container is in theory, because that is covered in the Docker section. Instead, you will see concrete deployment flows and patterns that are specific to running real backend services in production-like environments.


Typical Docker Deployment Flow

A simple backend deployment workflow with Docker often looks like this:

  1. Write a Dockerfile for your backend.
  2. Build a Docker image on your machine or in CI.
  3. Run and test the container locally.
  4. Push the image to a container registry.
  5. Pull and run the image on a server or cloud platform.

Let us go step by step with a concrete example using a FastAPI app, but the pattern is similar for other frameworks and languages.

Example project layout

Assume a small FastAPI project:

text
app/
  main.py
  requirements.txt
Dockerfile

app/main.py:

python
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
    return {"status": "ok"}

app/requirements.txt:

text
fastapi
uvicorn[standard]

Writing a Production-Friendly Dockerfile

You probably saw simple Dockerfiles earlier. For deployment, you want them to be:

A common pattern is:

Here is a typical production-style Dockerfile for a FastAPI app:

dockerfile
# 1. Base image
FROM python:3.12-slim AS base
# 2. Environment configuration
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1
# 3. System dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*
# 4. Work directory
WORKDIR /app
# 5. Install Python dependencies
COPY app/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 6. Copy the application code
COPY app/ .
# 7. Create a non-root user
RUN useradd -m appuser
USER appuser
# 8. Expose port and set default command
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Key ideas:

Important: Never run production containers as root unless absolutely necessary. Use USER to switch to a non-root user for better security.

For a different backend language, the pattern is similar:

Building and Running the Image Locally

Once the Dockerfile is ready, you can build the image.

From the project root:

bash
docker build -t myfastapi-app:latest .

You can list images:

bash
docker images

Then run a container:

bash
docker run -d \
  --name myfastapi-container \
  -p 8000:8000 \
  myfastapi-app:latest

Explanation:

Check logs:

bash
docker logs myfastapi-container

Test the health endpoint:

bash
curl http://localhost:8000/health
# {"status": "ok"}

Stop and remove the container when done:

bash
docker stop myfastapi-container
docker rm myfastapi-container

Rule: Always test the container locally before pushing the image or deploying to a server. If it does not work locally, it will not work in production.


Using Environment Variables in Docker Deployment

In development you might store settings in .env files or environment variables. For deployment, you must avoid hard coding secrets or environment-specific values in the image.

Common configuration values:

Modify your app to read from environment variables. Example main.py:

python
import os
from fastapi import FastAPI
app = FastAPI()
ENV_NAME = os.getenv("ENV_NAME", "development")
@app.get("/env")
def env():
    return {"env": ENV_NAME}

Run the container with environment variables:

bash
docker run -d \
  --name myfastapi-container \
  -p 8000:8000 \
  -e ENV_NAME=production \
  myfastapi-app:latest

You can also use an env file:

.env:

text
ENV_NAME=staging
DATABASE_URL=postgresql://user:pass@db:5432/app
DEBUG=false

Run with --env-file:

bash
docker run -d \
  --name myfastapi-container \
  -p 8000:8000 \
  --env-file .env \
  myfastapi-app:latest

Security rule: Do not bake secrets into your Docker image. Use environment variables, secret managers, or encrypted files mounted at runtime.


Handling Static Files and File Storage

In deployment, your backend might serve static assets or handle user uploads. Docker images are immutable, and containers have ephemeral filesystems, so you must plan where data lives.

Static files bundled in the image

For versioned static assets built at build time, you can copy them into the image.

Example project layout:

text
app/
  main.py
  static/
    logo.png
    styles.css

Dockerfile snippet:

dockerfile
COPY app/ /app

The static files travel with the image. When containers are replaced, static files are still available because they are part of the image.

User uploads and persistent data

User uploads must not be stored only inside the container filesystem. When the container is recreated or scaled, files would be lost.

Solution: use volumes to persist files outside the container.

Example: mount a host directory into the container:

bash
docker run -d \
  --name myfastapi-container \
  -p 8000:8000 \
  -v /srv/uploads:/app/uploads \
  myfastapi-app:latest

Table of storage patterns:


Type of dataWhere to store
Static assetsInside the image, or dedicated static server
User uploadsVolumes or object storage (S3, etc.)
LogsStdout/stderr for Docker, or volumes/log services
DatabasesExternal DB service, or dedicated DB container + volume

Multi-Container Deployments with Docker Compose

In production, your backend rarely runs alone. You probably need:

docker-compose lets you define and run multi-container applications using a single YAML file.

Example docker-compose.yml for a basic stack:

yaml
version: "3.9"
services:
  app:
    build: .
    container_name: myfastapi-app
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/app
      - ENV_NAME=production
    depends_on:
      - db
  db:
    image: postgres:16
    container_name: myfastapi-db
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=app
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:

Run everything:

bash
docker compose up -d

Stop everything:

bash
docker compose down

This is still deployment, even if it is on your local machine. The same pattern can be used on a remote server.


Pushing Images to a Container Registry

To deploy to a remote server or cloud, you must make your image available from somewhere that server can access. This is what a container registry is for.

Common registries:

Example with Docker Hub

  1. Create a Docker Hub account.
  2. Login from your terminal:
bash
   docker login
  1. Tag your image with your Docker Hub username:
bash
   docker tag myfastapi-app:latest yourname/myfastapi-app:1.0.0
   docker tag myfastapi-app:latest yourname/myfastapi-app:latest
  1. Push the image:
bash
   docker push yourname/myfastapi-app:1.0.0
   docker push yourname/myfastapi-app:latest

On your remote server:

bash
docker pull yourname/myfastapi-app:1.0.0
docker run -d \
  --name myfastapi-app \
  -p 80:8000 \
  --env-file /etc/myfastapi.env \
  yourname/myfastapi-app:1.0.0

Rule: Always tag images with a version, not only latest. This makes rollbacks and reproducible deployments possible.


Deploying to a Linux Server with Docker

Here is a concrete end-to-end scenario.

You have:

Contents of /etc/myfastapi.env:

text
ENV_NAME=production
DATABASE_URL=postgresql://prod_user:secret@prod-db:5432/app
SECRET_KEY=super-secret-production-key

Steps:

  1. Pull the image:
bash
   docker pull yourname/myfastapi-app:1.0.0
  1. Run the container:
bash
   docker run -d \
     --name myfastapi-app \
     --restart unless-stopped \
     -p 80:8000 \
     --env-file /etc/myfastapi.env \
     yourname/myfastapi-app:1.0.0

Explanation:

  1. Check logs:
bash
   docker logs -f myfastapi-app
  1. Test from outside:

From your local machine:

bash
   curl http://your-server-ip/health

If you see {"status":"ok"}, your app is deployed with Docker.


Zero-Downtime Style Updates with Docker

To update your app without noticeable downtime, you can deploy a new version beside the old one and then switch traffic.

Simple version for a single server and no reverse proxy:

  1. Build and push a new image: yourname/myfastapi-app:1.1.0.
  2. On the server, start a new container on another port:
bash
   docker run -d \
     --name myfastapi-app-v2 \
     -p 8001:8000 \
     --env-file /etc/myfastapi.env \
     yourname/myfastapi-app:1.1.0
  1. Test the new version:
bash
   curl http://your-server-ip:8001/health
  1. If OK, change your reverse proxy (Nginx, etc.) or load balancer configuration to forward traffic to port 8001 instead of 8000.
  2. Reload the proxy configuration.
  3. Stop the old container:
bash
   docker stop myfastapi-app
   docker rm myfastapi-app

In a more advanced setup, a load balancer can keep both versions running for a while and drain connections from the old one.


Log Management in Deployed Containers

In production you need logs for debugging and monitoring. With Docker, the recommended pattern is:

You do not usually write logs to files inside the container for deployment.

FastAPI example with Python logging:

python
import logging
logger = logging.getLogger("myapp")
logger.setLevel(logging.INFO)
app = FastAPI()
@app.get("/health")
def health():
    logger.info("Health check called")
    return {"status": "ok"}

In the container:

bash
docker logs -f myfastapi-app

In production, you might connect Docker logs to tools like:

Common Pitfalls When Deploying with Docker

Here are frequent mistakes and how to avoid them:

PitfallProblemFix
Hard coded config in imageCannot change settings without rebuildUse environment variables, config files mounted at runtime
Storing uploads in containerData lost on container replacementUse Docker volumes or external storage
Running as rootHigher security riskUse USER in Dockerfile
Only using latest tagHard to know which version is running, difficult rollbackUse versioned tags (1.0.0, 2024-08-01, etc.)
Huge imagesSlow deploy, big network transferUse slim base images, multi-stage builds, remove build tools
No health checksNo automatic detection of failing containersAdd health endpoints and configure health checks (proxy/orchestration)
Debug mode in productionSecurity issues, performance overheadMake debug mode configurable and disabled by default

Checklist: Before calling a Docker deployment "production ready", verify:

  • No secrets in the image.
  • Non-root user is used.
  • Configurable via environment variables.
  • Logs go to stdout/stderr.
  • Persistent data is not stored only inside containers.

Summary

In this chapter you saw how to:

These patterns form the foundation for more advanced deployment tools and orchestrators, such as Kubernetes or managed container services. Once you are comfortable deploying with plain Docker on a single server, those tools will be easier to understand.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!