KAHIBARO
Discord Login Register

32.12. Production Deployment

Understanding Production Deployment

Production deployment is the step where your backend stops being a local experiment and becomes a real service that users depend on. In the final project, this is where you take your already built and tested application and run it on real infrastructure, in a safe and repeatable way.

This chapter focuses on the practical aspects that are specific to deploying a production backend, not on building the application itself or configuring HTTPS and domains, which are covered elsewhere.

Goals of a Production Deployment

When you deploy to production, you are trying to achieve more than just “it runs.” A good deployment:

Key production rule:
A build that is deployed to production must be the same artifact that passed automated tests in CI, without manual changes.

Think of deployment as a pipeline: code → build artifact → tested artifact → deployed artifact. You should not rebuild or edit the app manually on the server.

Environments: Local, Staging, Production

You will usually have at least three environments:

EnvironmentPurposeWho uses it
LocalDaily development and experimentationIndividual developers
StagingFinal testing before productionDevs, QA, sometimes PMs
ProductionReal users and real dataEnd users and clients

The same application should run in all three, but with different configuration: database URLs, secrets, debug flags, logging levels, etc.

For your final project, you can often skip staging if you are alone, but it is better to create at least a “staging-like” environment where you can safely test migrations and deployments on a server that is not serving real users.

Where to Deploy: Server Choices

There are three common options:

OptionExampleProsCons
Single VPS (virtual server)DigitalOcean, Linode, HetznerCheap, simple to understandManual scaling, more manual setup
PaaS platformRender, Railway, Fly.ioLess ops work, easy deploymentCosts, more vendor lock-in
Container orchestratorKubernetes, ECS, NomadScales well, advanced featuresComplex to learn

For this course and your final project, a single Linux VPS with Docker is a realistic and educational choice, so we will describe deployment in that context.

Preparing the Production Server

Before you deploy, your server should be prepared. This usually happens once, then rarely changes.

Typical steps on a fresh Linux VPS:

  1. Create a non-root user
    • Add a user and give it sudo privileges.
    • Use this user for deployments, not root.
  2. Set up SSH authentication
    • Use SSH key pairs, not passwords.
    • Optionally disable password login for better security.
  3. Install required software

At minimum:

  1. Configure firewall
    • Allow SSH (port 22) and HTTP/HTTPS (ports 80 and 443).
    • Block everything else by default if possible.
  2. Create directories for persistent data

Example structure:

text
   /opt/yourapp/
     app/          # application code or built artifacts
     env/          # environment files (.env)
     data/         # DB volumes, uploads, etc.
     logs/         # application logs (if stored on disk)

This is infrastructure work, not deployment itself, but deployment will assume this base is ready.

Deployment Artifacts

In modern backend deployments you rarely copy raw source code and run pip install directly on the server. Instead you create an artifact:

In this course you will generally use Docker.

Building Images for Production

Your application likely already has a Dockerfile. For production, you want:

A simple multi-stage pattern (conceptual example):

Dockerfile
# Builder stage: install dev tools, build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN pip install --upgrade pip && pip install poetry \
    && poetry export -f requirements.txt --output requirements.txt
COPY . .
RUN pip install -r requirements.txt
# Runtime stage: minimal image
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12 /usr/local/lib/python3.12
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

In practice you should already have something similar from earlier Docker chapters. The important part for deployment: the same Docker image that you build in CI (or on your machine) is what you run in production.

A Minimal Production Stack

A typical production-like stack for your final project might include:

All of this can be described in a docker-compose.yml file.

A minimal example (simplified and generic):

yaml
version: "3.9"
services:
  app:
    image: your-docker-user/yourapp:1.0.0
    env_file:
      - ./env/production.env
    depends_on:
      - db
      - redis
    ports:
      - "8000:8000"
    command: >
      uvicorn app.main:app
      --host 0.0.0.0
      --port 8000
      --workers 4
    restart: always
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: yourapp
      POSTGRES_USER: yourapp
      POSTGRES_PASSWORD: change_me
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    restart: always
  redis:
    image: redis:7
    volumes:
      - ./data/redis:/data
    restart: always

In a real deployment, Nginx will sit in front of the app service, listen on ports 80 and 443 and forward requests to app:8000.

Environment-Specific Configuration

The same code will run in local, staging, and production, with different configuration.

Use environment variables to configure:

You might have:

On the server, you typically place them in a directory like /opt/yourapp/env/production.env.

Never commit production secrets to Git.
Secrets must only exist in secure storage, such as environment variables, secret managers, or encrypted files outside your repository.

Your application configuration layer should read values from environment variables. For example, in a Pydantic settings class.

A Simple Manual Deployment Flow

Here is a concrete example of a manual but structured deployment using Docker and a VPS. This is enough for a small project.

Step 1: Build and push the image

On your development machine or in CI:

bash
# Pull latest changes
git pull origin main
# Run tests
pytest
# Build image
docker build -t your-docker-user/yourapp:1.0.0 .
# Push to registry
docker push your-docker-user/yourapp:1.0.0

The tag 1.0.0 should match a Git tag or commit reference. Avoid using only latest because it is not explicit.

Step 2: Connect to the server

From your machine:

bash
ssh youruser@your-server-ip
cd /opt/yourapp

Step 3: Pull the new image

On the server:

bash
docker pull your-docker-user/yourapp:1.0.0

If you use docker-compose.yml with the image tag set, make sure it matches.

Step 4: Run migrations

Database migrations should be applied before switching traffic to the new version, but after you have the new code.

For example, if your app provides a migration command:

bash
docker compose run --rm app alembic upgrade head

This uses the new image to run migrations.

Step 5: Restart the application service

Using Docker Compose:

bash
docker compose up -d app

This recreates the app container with the new image while keeping the DB and Redis running.

Step 6: Verify the deployment

Check container status:

bash
docker compose ps

Inspect logs:

bash
docker compose logs -f app

Trigger a simple health check. If you have a /health endpoint:

bash
curl -f https://your-domain.com/health

If it returns a successful status, your deployment is live.

Zero-Downtime Upgrade Strategy (Basic)

For high-traffic applications, you usually aim for zero downtime.

You can get close to zero downtime even with a single server and Docker:

  1. Nginx listens on port 80/443 and forwards to the app container.
  2. When you run docker compose up -d app:
    • Docker creates a new container from the new image.
    • It stops and removes the old container after the new one is ready.
  3. Nginx holds the listening ports. It simply connects new requests to the new container.

During this transition, existing connections may be closed, but if your application is stateless and calls are short, users usually do not notice.

For more advanced setups you can:

These are beyond the scope of this course but follow the same idea: bring new version up, switch traffic, then take old version down.

Coordinating Migrations and Deployments

Database migrations are the part that often breaks deployments.

A safe pattern:

  1. Additive migrations first
    • Add new columns or tables that the new version needs.
    • Keep the old version compatible.
    • Deploy new code that uses both old and new structures if needed.
  2. Switch application version
    • Deploy new version of the app that can handle both old and new data format, or that uses the new structures.
  3. Cleanup migrations
    • After the new version runs safely in production and old data paths are not used, remove deprecated columns or tables.

For small projects you can often migrate and deploy in one step, but still:

Always run migrations before starting the new application version that depends on them.
Never deploy code that expects a column that does not exist yet.

If a migration fails:

Rolling Back a Deployment

No matter how careful you are, some deployments will introduce bugs.

You must have a plan to roll back.

Typical rollback options with Docker:

  1. Keep the previous image

You might have versions:

If 1.1.0 is bad, you can:

  1. Roll back database migrations

This is harder. It only works if your migration tooling supports down migrations and your changes are reversible.

Example:

bash
   docker compose run --rm app alembic downgrade -1

Some changes, such as dropping a column, cannot be easily reversed.

Because database rollback is risky, many teams prefer:

For your final project, a simple rollback strategy is:

Logging and Monitoring After Deployment

After deployment you need to check that the application is healthy.

Use the logging and monitoring tools from previous chapters, but pay attention to deployment-specific checks:

Right after a deployment you should:

  1. Tail logs for a few minutes:
bash
   docker compose logs -f app
  1. Watch the system:
    • Use tools like htop, docker stats, or your monitoring dashboard.
  2. Hit key API endpoints manually or with a smoke test script.

A smoke test is a very small set of automated checks that verifies the basic health of the service after deployment, for example:

If any of these fail, treat the deployment as failed and consider rolling back.

Common Production Deployment Pitfalls

Here are mistakes that cause trouble frequently:

PitfallSafer alternative
Building images directly on the serverBuild in CI or locally, then push and pull
Editing code manually on the serverOnly deploy from Git commits
Using latest tag everywhereUse versioned tags like 1.0.0 or commit hash
Running pip install in a running containerBuild dependencies into the image
Copying .env into the repoStore .env files only on the server, not in Git
Skipping migrationsAlways migrate before starting new app version
No health checksImplement a /health endpoint and test it
No logs after deployAlways look at logs after deployment

Keep this list in mind when you design your final project deployment process.

Documenting Your Deployment Process

For the final project, pretend you are handing this project to another engineer. They should be able to deploy without guessing.

Create a DEPLOYMENT.md file that includes:

For example, you might include:

markdown
# Deployment guide
1. Build and push image:
   ```bash
   docker build -t your-docker-user/yourapp:VERSION .
   docker push your-docker-user/yourapp:VERSION
  1. On server:
bash
   cd /opt/yourapp
   sed -i 's/yourapp:.*/yourapp:VERSION/' docker-compose.yml
   docker pull your-docker-user/yourapp:VERSION
   docker compose run --rm app alembic upgrade head
   docker compose up -d app
  1. Verify:
bash
   curl -f https://your-domain.com/health

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!