Bringing It All Together — Docker Packaging & Cloud Deployment [2026 Tutorial]

Written in

by

This is Part 5 · The Finale of the series.

  • Part 1: Why Python Still Dominates in 2026
  • Part 2: Build Your Own AI Chatbot — RAG From Scratch to Deployment
  • Part 3: One AI Is No Longer Enough — LangGraph Multi-Agent Systems
  • Part 4: AI That Finally Remembers — Complete LangGraph Memory Guide
  • Part 5: Bringing It All Together — Docker Packaging & Cloud Deployment ← You are here

📌 Level: Intermediate (Parts 1–4 are sufficient background) ⏱️ Reading time: ~13 min / Hands-on time: ~2–3 hours 🛠️ End result: A fully integrated AI service (RAG + multi-agent + memory) packaged with Docker and deployed to the cloud


We’ve made it to the finale.

In Part 1, you understood why Python dominates 2026. In Part 2, you built a RAG chatbot. In Part 3, you assembled a multi-agent team. In Part 4, you gave your AI a memory.

But everything built so far lives only on your local machine. You can’t send a friend a link. You can’t use it from your phone.

Today, that changes.

We’ll package everything with Docker, deploy it to the cloud, and ship an AI service accessible from anywhere in the world — the day everything from Parts 1–4 comes together as one.


📊 Table of Contents

  1. Why Docker — Eliminating “It Worked on My Machine” Forever
  2. The Full Architecture at a Glance
  3. Organizing the Project Structure
  4. Writing a Multi-Stage Dockerfile — Image Size 1 GB → 180 MB
  5. Running the Full Local Stack with Docker Compose
  6. Managing Environment Variables & Secrets Safely
  7. Building a CI/CD Pipeline with GitHub Actions
  8. Deploying to Railway — Going Live in Production
  9. Health Checks & Monitoring
  10. Post-Deployment Checklist

1. Why Docker

Every developer has heard this line at least once:

“But it worked on my machine…”

A teammate’s MacBook, an AWS EC2 server, a Railway container — each has a different Python version, different package versions, different environment variable setup. That’s why code that runs locally fails on the server.

Docker eliminates this problem at the root.

It bundles your code + runtime environment + dependencies into a single container image. That image runs identically everywhere — Mac, Linux server, or cloud.

[Without Docker]
Developer's Mac → "Works great!"
CI server → "Missing package error"
Production → "Wrong Python version"
Teammate's PC → "Dependency conflict"
[With Docker]
Runs identically everywhere → ✅

In 2026, the standard for deploying production Python AI apps is clear: Docker containers.


2. The Full Architecture at a Glance

The complete picture of what we’ll finish today.

┌─────────────────────────────────────────────────────┐
│ Client (Browser / App) │
└──────────────────────────┬──────────────────────────┘
│ HTTPS
┌──────────────────────────▼──────────────────────────┐
│ Railway Cloud Platform │
│ ┌───────────────────────────────────────────────┐ │
│ │ FastAPI Container (port 8000) │ │
│ │ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ RAG Pipeline │ │ LangGraph Agent Team │ │ │
│ │ │ (Part 2) │ │ + Memory (Parts 3-4)│ │ │
│ │ └──────┬───────┘ └──────────┬───────────┘ │ │
│ └─────────┼─────────────────────┼───────────────┘ │
│ │ │ │
│ ┌─────────▼─────────────────────▼───────────────┐ │
│ │ PostgreSQL Container (checkpointer + pgvec) │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

Services:

  • FastAPI app — handles all endpoints
  • PostgreSQL — conversation memory (checkpointer) + vector storage (pgvector)
  • Railway — container hosting, automatic HTTPS, domain provisioning

3. Organizing the Project Structure

Consolidate everything built across Parts 1–4 into one unified project.

ai-service/
├── .env # Local dev only (never commit!)
├── .env.example # Shared template for teammates
├── .gitignore
├── Dockerfile # Production image definition
├── docker-compose.yml # Local full-stack runner
├── requirements.txt
├── app/
│ ├── main.py # FastAPI entry point
│ ├── rag_pipeline.py # Part 2: RAG logic
│ ├── agents/
│ │ ├── state.py # Part 3: Shared state
│ │ ├── agents.py # Part 3: 3 agents
│ │ ├── supervisor.py # Part 3: Supervisor
│ │ └── graph.py # Part 3: Graph assembly
│ └── memory/
│ └── checkpointer.py # Part 4: PostgreSQL checkpointer
├── .github/
│ └── workflows/
│ └── deploy.yml # CI/CD pipeline
└── scripts/
└── healthcheck.sh # Health check script

4. Multi-Stage Dockerfile — Image Size 1 GB → 180 MB

The most important file. Multi-stage builds cut the final image size by over 80%.

# Dockerfile
# ─────────────────────────────────────
# Stage 1: Builder (includes build tools)
# ─────────────────────────────────────
FROM python:3.12-slim AS builder
# Python optimization env vars
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /build
# Install build tools (excluded from final image)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Create virtual environment & install dependencies
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --upgrade pip && \
pip install -r requirements.txt
# ─────────────────────────────────────
# Stage 2: Production (no build tools)
# ─────────────────────────────────────
FROM python:3.12-slim AS production
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH"
# Copy only the virtual env from builder — build tools stay behind
COPY --from=builder /opt/venv /opt/venv
# Security: run as a non-root dedicated user
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /home/appuser/app
# Copy application code
COPY --chown=appuser:appuser app/ .
USER appuser
EXPOSE 8000
# Production server: gunicorn + uvicorn workers
CMD ["gunicorn", "main:app", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--workers", "2", \
"--bind", "0.0.0.0:8000", \
"--timeout", "120", \
"--access-logfile", "-"]

Why multi-stage?

Single StageMulti-Stage
Image size~1.0 GB~180 MB
Build tools included✅ (security risk)❌ (removed)
Deploy speedSlowFast
Attack surfaceWideNarrow

5. Running the Full Local Stack with Docker Compose

One command to spin up the app + database locally.

# docker-compose.yml
version: "3.9"
services:
# ── Main Application ────────────────────────────
app:
build:
context: .
target: production # Use the production stage from Dockerfile
ports:
- "8000:8000"
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- DATABASE_URL=postgresql://aiuser:aipass@db:5432/aidb
- ENVIRONMENT=development
depends_on:
db:
condition: service_healthy # Wait until DB is ready
volumes:
- ./app:/home/appuser/app # Live code reload during development
- uploads_data:/home/appuser/app/uploads
restart: unless-stopped
# ── PostgreSQL Database ────────────────────────
db:
image: pgvector/pgvector:pg16 # Includes pgvector extension
environment:
POSTGRES_USER: aiuser
POSTGRES_PASSWORD: aipass
POSTGRES_DB: aidb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aiuser -d aidb"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres_data:
uploads_data:

Run commands:

# Start the full stack (background)
docker-compose up -d
# Tail logs
docker-compose logs -f app
# Stop
docker-compose down
# Full reset (including volumes)
docker-compose down -v

Visit http://localhost:8000/docs to test via Swagger UI immediately.


6. Managing Environment Variables & Secrets Safely

Hardcoding API keys in code — or pushing them to GitHub — is a serious mistake. Here’s the right way.

# .env.example — share this with teammates (key names only, no real values)
ANTHROPIC_API_KEY=your-api-key-here
DATABASE_URL=postgresql://user:password@localhost:5432/aidb
ENVIRONMENT=development
SECRET_KEY=generate-a-random-32-char-string
# .gitignore — must include these
.env
.env.local
*.db
uploads/
vector_store/
__pycache__/
*.pyc
.DS_Store
# app/config.py — centralized settings management
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
# Required (app won't start without these)
anthropic_api_key: str
database_url: str
# Optional (have defaults)
environment: str = "production"
secret_key: str = "change-this-in-production"
max_upload_size_mb: int = 10
max_conversation_turns: int = 50
class Config:
env_file = ".env"
case_sensitive = False
@lru_cache() # Load once, cache after that
def get_settings() -> Settings:
return Settings()
# Usage:
# from config import get_settings
# settings = get_settings()
# settings.anthropic_api_key

7. Building a CI/CD Pipeline with GitHub Actions

A pipeline that automatically tests and deploys every time you push code.

# .github/workflows/deploy.yml
name: Deploy AI Service
on:
push:
branches: [main] # Runs on every push to main
pull_request:
branches: [main] # Runs tests on PRs (no deploy)
jobs:
# ── Step 1: Test ────────────────────────────────
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Install dependencies
run: pip install -r requirements.txt pytest httpx
- name: Run tests
run: pytest app/tests/ -v --tb=short
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
DATABASE_URL: "sqlite:///./test.db"
# ── Step 2: Build & Verify Docker Image ─────────
build:
needs: test # Only runs if tests pass
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build --target production -t ai-service:${{ github.sha }} .
- name: Verify image starts
run: |
docker run --rm -d --name test-container \
-e ANTHROPIC_API_KEY=dummy \
-e DATABASE_URL=dummy \
-p 8000:8000 \
ai-service:${{ github.sha }}
sleep 5
curl -f http://localhost:8000/health || exit 1
docker stop test-container
# ── Step 3: Deploy to Railway ───────────────────
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Install Railway CLI
run: npm install -g @railway/cli
- name: Deploy to Railway
run: railway up --service ai-service
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

Setting up GitHub Secrets:

  • Repository → Settings → Secrets → Actions
  • Add ANTHROPIC_API_KEY
  • Add RAILWAY_TOKEN (generate from Railway dashboard)

8. Deploying to Railway — Going Live

Railway hosts Docker images directly. Link it to GitHub and every push auto-deploys.

First-time deployment:

# 1. Install Railway CLI
npm install -g @railway/cli
# 2. Log in
railway login
# 3. Create a new project
railway init
# 4. Add PostgreSQL service
railway add --plugin postgresql
# 5. Set environment variables (multiple at once)
railway variables set \
ANTHROPIC_API_KEY=your-key \
ENVIRONMENT=production \
SECRET_KEY=$(openssl rand -hex 32)
# 6. Deploy!
railway up

Post-deployment commands:

# Open the deployed URL
railway open
# Tail live logs
railway logs --tail
# List environment variables
railway variables

Railway automatically provisions HTTPS and a domain. Once deployed, your service is live at https://your-service.up.railway.app.


9. Health Checks & Monitoring

Endpoints that confirm the service is running correctly after deployment.

# Add to app/main.py
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from datetime import datetime
import os
app = FastAPI()
@app.get("/health")
async def health_check():
"""
Called periodically by load balancers, Railway, and monitoring tools.
200 = healthy | 5xx = unhealthy → triggers auto-restart
"""
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"environment": os.getenv("ENVIRONMENT", "unknown"),
"version": "1.0.0"
}
@app.get("/health/detailed")
async def detailed_health():
"""Full health check including DB connectivity"""
checks = {}
# Verify DB connection
try:
from memory.checkpointer import get_db_connection
conn = get_db_connection()
conn.close()
checks["database"] = "ok"
except Exception as e:
checks["database"] = f"error: {str(e)}"
all_ok = all(v == "ok" for v in checks.values())
status_code = 200 if all_ok else 503
return JSONResponse(
content={"status": "healthy" if all_ok else "degraded", "checks": checks},
status_code=status_code
)

10. Post-Deployment Checklist

Before making your service public, verify every item below.

Security

  • [ ] Is .env included in .gitignore?
  • [ ] Are API keys hardcoded nowhere in the source code?
  • [ ] Is the Docker container running as a non-root user?
  • [ ] Is HTTPS enabled? (Railway provides this automatically)

Performance

  • [ ] Is the Gunicorn worker count appropriate? (Recommended: CPU cores × 2 + 1)
  • [ ] Is response time under 3 seconds?
  • [ ] Is memory usage within acceptable limits?

Reliability

  • [ ] Does /health return 200?
  • [ ] Is the DB connection stable?
  • [ ] Are there no anomalies in the error logs?

Operations

  • [ ] Do you know how to check logs?
  • [ ] Do you know how to restart the service?
  • [ ] Are cost alerts configured?

Wrapping Up — This Is Not an Ending, It’s a Beginning

Let’s look back at everything we built together across 5 parts.

Part 1 — You understood why Python dominates the AI era. Part 2 — You built a document-grounded AI chatbot with RAG. Part 3 — You assembled a collaborating AI team with multi-agents. Part 4 — You gave your AI persistent memory. Part 5 — You shipped it to the world with Docker and the cloud.

This is not the end.

Running a deployed service, collecting real user feedback, finding bottlenecks, and iterating — that process is just beginning. And that process is what makes a developer grow.

What to build next: streaming responses, user authentication, usage-based billing, multimodal input (images + text) — the possibilities are open.

A sincere thank you to everyone who followed this series to the end. Drop a comment if you have questions 🙌


🔖 Full Series

  • Part 1: Why Python Still Dominates in 2026
  • Part 2: Build Your Own AI Chatbot — RAG From Scratch to Deployment
  • Part 3: One AI Is No Longer Enough — LangGraph Multi-Agent Systems
  • Part 4: AI That Finally Remembers — Complete LangGraph Memory Guide
  • Part 5: Bringing It All Together — Docker & Cloud Deployment ← You are here

Tags: #Python #Docker #FastAPI #LangGraph #CloudDeployment #Railway #CICD #AIService #DevTutorial #2026


Sources: Docker Multi-Stage Builds Guide · Railway Docs · FastAPI Deployment Guide 2026 · GitHub Actions Docs

Tags

Categories

Discover more from

Subscribe now to keep reading and get access to the full archive.

Continue reading