Files
orion/docs/deployment/docker.md
Samir Boulahtit f631283286
Some checks failed
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / ruff (push) Successful in 11s
CI / pytest (push) Has been cancelled
docs(deployment): update memory limits and celery concurrency across all guides
Sync all deployment docs with actual docker-compose.yml values:
- celery-worker: 512→768MB, concurrency 4→2
- db: 512→256MB, celery-beat: 256→128MB, flower: 256→192MB
- Redis maxmemory: 256mb→100mb (matches container mem_limit 128m)
- Add redis-exporter to scaling guide memory budget

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 22:21:25 +01:00

12 KiB

Docker Deployment

This guide covers deploying Orion using Docker and Docker Compose.

Best for: Teams who want consistent environments and easy rollbacks.


Development vs Production

Aspect Development Production
Compose file docker-compose.yml docker-compose.prod.yml
App server Hot reload enabled Multiple workers
Database Local volume Persistent volume with backups
SSL Not needed Required (via Nginx)
Logging Console File + centralized

Development Setup

# Start all services
make docker-up

# Or manually
docker compose up -d

# View logs
docker compose logs -f

# Stop services
make docker-down

Current Services

Service Port Profile Purpose
db 5432 (default) PostgreSQL database
redis 6379 (default) Cache and queue broker
api 8000 full FastAPI application
celery-worker full Background task processing
celery-beat full Scheduled task scheduler
flower 5555 full Celery monitoring dashboard
prometheus 9090 full Metrics storage (15-day retention)
grafana 3001 full Dashboards (https://grafana.wizard.lu)
node-exporter 9100 full Host CPU/RAM/disk metrics
cadvisor 8080 full Per-container resource metrics

Use docker compose --profile full up -d to start all services, or docker compose up -d for just db + redis (local development).


Production Deployment

1. Create Production Compose File

# docker-compose.prod.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    restart: always
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      DATABASE_URL: postgresql://orion_user:${DB_PASSWORD}@db:5432/orion_db
      REDIS_URL: redis://redis:6379/0
      CELERY_BROKER_URL: redis://redis:6379/1
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - uploads:/app/uploads
      - logs:/app/logs
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 1G

  celery:
    build: .
    restart: always
    command: celery -A app.core.celery_config worker --loglevel=info -Q default,long_running,scheduled
    environment:
      DATABASE_URL: postgresql://orion_user:${DB_PASSWORD}@db:5432/orion_db
      REDIS_URL: redis://redis:6379/0
    env_file:
      - .env
    depends_on:
      - db
      - redis
    volumes:
      - logs:/app/logs
    deploy:
      resources:
        limits:
          memory: 768M

  celery-beat:
    build: .
    restart: always
    command: celery -A app.celery beat --loglevel=info
    environment:
      CELERY_BROKER_URL: redis://redis:6379/1
    env_file:
      - .env
    depends_on:
      - redis
    deploy:
      resources:
        limits:
          memory: 256M

  db:
    image: postgres:15-alpine
    restart: always
    environment:
      POSTGRES_DB: orion_db
      POSTGRES_USER: orion_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U orion_user -d orion_db"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 512M

  redis:
    image: redis:7-alpine
    restart: always
    command: redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 300M

  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./static:/app/static:ro
      - uploads:/app/uploads:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - api
    deploy:
      resources:
        limits:
          memory: 128M

volumes:
  postgres_data:
  redis_data:
  uploads:
  logs:

2. Create Dockerfile

# Dockerfile
FROM python:3.11-slim

# Install system dependencies
RUN apt-get update && apt-get install -y \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Install Tailwind CLI
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
    && chmod +x tailwindcss-linux-x64 \
    && mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss

WORKDIR /app

# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY . .

# Build Tailwind CSS
RUN tailwindcss -i ./static/admin/css/tailwind.css -o ./static/admin/css/tailwind.output.css --minify \
    && tailwindcss -i ./static/store/css/tailwind.css -o ./static/store/css/tailwind.output.css --minify \
    && tailwindcss -i ./static/storefront/css/tailwind.css -o ./static/storefront/css/tailwind.output.css --minify \
    && tailwindcss -i ./static/public/css/tailwind.css -o ./static/public/css/tailwind.output.css --minify

# Create non-root user
RUN useradd -m -u 1000 orion && chown -R orion:orion /app
USER orion

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

3. Nginx Configuration

mkdir -p nginx/conf.d
# nginx/conf.d/orion.conf
upstream api {
    server api:8000;
}

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    # Static files
    location /static {
        alias /app/static;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location /uploads {
        alias /app/uploads;
        expires 7d;
    }

    location / {
        proxy_pass http://api;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

4. Deploy

# Create .env file with production values
cp .env.example .env
nano .env

# Set database password
export DB_PASSWORD=$(openssl rand -hex 16)
echo "DB_PASSWORD=$DB_PASSWORD" >> .env

# Build and start
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d

# Run migrations
docker compose -f docker-compose.prod.yml exec api alembic upgrade head

# Initialize data
docker compose -f docker-compose.prod.yml exec api python scripts/seed/init_production.py

Daily Operations

View Logs

# All services
docker compose -f docker-compose.prod.yml logs -f

# Specific service
docker compose -f docker-compose.prod.yml logs -f api
docker compose -f docker-compose.prod.yml logs -f celery

# Last 100 lines
docker compose -f docker-compose.prod.yml logs --tail 100 api

Access Container Shell

# API container
docker compose -f docker-compose.prod.yml exec api bash

# Database
docker compose -f docker-compose.prod.yml exec db psql -U orion_user -d orion_db

# Redis
docker compose -f docker-compose.prod.yml exec redis redis-cli

Restart Services

# Single service
docker compose -f docker-compose.prod.yml restart api

# All services
docker compose -f docker-compose.prod.yml restart

Deploy Updates

# Pull latest code
git pull origin main

# Rebuild and restart
docker compose -f docker-compose.prod.yml build api celery
docker compose -f docker-compose.prod.yml up -d api celery

# Run migrations if needed
docker compose -f docker-compose.prod.yml exec api alembic upgrade head

Rollback

# View image history
docker images orion-api

# Tag current as backup
docker tag orion-api:latest orion-api:backup

# Rollback to previous
docker compose -f docker-compose.prod.yml down api
docker tag orion-api:previous orion-api:latest
docker compose -f docker-compose.prod.yml up -d api

Backups

Automated backup scripts handle daily pg_dump with rotation and optional Cloudflare R2 offsite sync:

# Run backup (Orion + Gitea databases)
bash scripts/backup.sh

# Run backup with R2 upload
bash scripts/backup.sh --upload

# Restore from backup
bash scripts/restore.sh orion ~/backups/orion/daily/orion_20260214_030000.sql.gz
bash scripts/restore.sh gitea ~/backups/gitea/daily/gitea_20260214_030000.sql.gz

Backups are stored in ~/backups/{orion,gitea}/{daily,weekly}/ with 7-day daily and 4-week weekly retention. A systemd timer runs the backup daily at 03:00.

See Hetzner Server Setup — Step 17 for full setup instructions.

Manual Database Backup

# One-off backup
docker compose exec db pg_dump -U orion_user orion_db | gzip > backup_$(date +%Y%m%d).sql.gz

# Restore
gunzip -c backup_20240115.sql.gz | docker compose exec -T db psql -U orion_user -d orion_db

Monitoring

The monitoring stack (Prometheus, Grafana, node-exporter, cAdvisor) runs under profiles: [full]. See Hetzner Server Setup — Step 18 for full setup and Observability Framework for the application-level metrics architecture.

Resource Usage

docker stats --no-stream

Health Checks

# Check service health
docker compose --profile full ps

# API health
curl -s http://localhost:8000/health | jq

# Prometheus metrics
curl -s http://localhost:8000/metrics | head -5

# Prometheus targets
curl -s http://localhost:9090/api/v1/targets | python3 -m json.tool | grep health

Troubleshooting

Container Won't Start

# Check logs
docker compose -f docker-compose.prod.yml logs api

# Check container status
docker compose -f docker-compose.prod.yml ps -a

# Inspect container
docker inspect <container_id>

Database Connection Issues

# Test from API container
docker compose -f docker-compose.prod.yml exec api python -c "
from app.core.database import engine
with engine.connect() as conn:
    print('Connected!')
"

Out of Disk Space

# Check disk usage
docker system df

# Clean up
docker system prune -a --volumes

Memory Issues

# Check memory usage
docker stats --no-stream

# Increase limits in docker-compose.prod.yml
deploy:
  resources:
    limits:
      memory: 2G

Security

Non-Root User

All containers run as non-root users. The Dockerfile creates a orion user.

Secret Management

# Use Docker secrets (Swarm mode)
echo "your-password" | docker secret create db_password -

# Or use environment files
# Never commit .env to git

Network Isolation

# Add to docker-compose.prod.yml
networks:
  frontend:
  backend:

services:
  nginx:
    networks:
      - frontend
  api:
    networks:
      - frontend
      - backend
  db:
    networks:
      - backend
  redis:
    networks:
      - backend

Scaling

Horizontal Scaling

# Scale API containers
docker compose -f docker-compose.prod.yml up -d --scale api=3

# Update nginx upstream
upstream api {
    server api_1:8000;
    server api_2:8000;
    server api_3:8000;
}

Moving to Kubernetes

When you outgrow Docker Compose, see our Kubernetes migration guide (coming soon).