Skip to content

Deploying with Docker Compose

Using Docker Compose is the officially recommended path for production deployments not running on a Kubernetes cluster. Keystone provides a starter compose recipe for launching the complete application stack from a single command.

Danger

Keystone deploys with insecure defaults that are not suitable for production. See the Settings page for a complete overview of configurable options and recommended settings.

Deployment Overview

An official compose recipe is provided below along with a table summerizing the deployed services. The recipe assumes application settings are defined in various .env files located in the same directory as the compose file. Example content for these files is also provided. For a full list of available API settings, see the Settings page.

Service Exposed Ports Description
proxy 80 Nginx reverse proxy for routing incoming requests and terminating TLS.
web Frontend service running the Keystone web interface.
api 8000 Backend API responsible for business logic and database access.
db PostgreSQL database used for persistent application data.
cache Redis instance used as a caching and messaging backend for Celery components.
smtp 8025 A mock SMTP server with a web frontend for viewing issued emails.
scheduler Celery Beat scheduler for executing periodic tasks.
worker Celery worker processes for background task execution (4 replicas by default).
docker-compose.yml
name: keystone

services:
  proxy:
    image: nginx:alpine3.22
    container_name: keystone-proxy
    restart: unless-stopped
    depends_on:
      api:
        condition: service_started
      web:
        condition: service_started
    ports:
      - "80:80"
    networks:
      - frontend
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - uploaded_files:/app/media
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost" ]
      interval: 10s
      timeout: 10s
      retries: 5

  web:
    image: docker.cloudsmith.io/better-hpc/keystone/keystone-web:${API_VERSION:-latest}
    container_name: keystone-web
    restart: unless-stopped
    depends_on:
      api:
        condition: service_started
    networks:
      - backend
      - frontend
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost" ]
      interval: 10s
      timeout: 10s
      retries: 5

  api:
    image: docker.cloudsmith.io/better-hpc/keystone/keystone-api:${API_VERSION:-latest}
    container_name: keystone-api
    restart: unless-stopped
    entrypoint: |
      sh -c "
        keystone-api migrate --no-input  # Apply latest DB schema
        uvicorn --host 0.0.0.0 --port 8000 --workers 4 keystone_api.main.asgi:application
      "
    env_file:
      - api.env
    depends_on:
      cache:
        condition: service_healthy
      db:
        condition: service_healthy
      smtp:
        condition: service_healthy
      scheduler:
        condition: service_started
    ports:
      # API Server
      - "8000:8000"
      # Prometheus Metrics
      - "9101:9101"
      - "9102:9102"
      - "9103:9103"
      - "9104:9104"
    networks:
      - backend
    volumes:
      - uploaded_files:/app/upload_files

  scheduler:
    image: docker.cloudsmith.io/better-hpc/keystone/keystone-api:${API_VERSION:-latest}
    container_name: keystone-scheduler
    restart: unless-stopped
    entrypoint: celery -A keystone_api.apps.scheduler beat --scheduler django_celery_beat.schedulers:DatabaseScheduler
    env_file:
      - api.env
    depends_on:
      cache:
        condition: service_healthy
      db:
        condition: service_healthy
      worker:
        condition: service_healthy
    networks:
      - backend
    healthcheck:
      disable: true

  worker:
    image: docker.cloudsmith.io/better-hpc/keystone/keystone-api:${API_VERSION:-latest}
    restart: unless-stopped
    entrypoint: celery -A keystone_api.apps.scheduler worker
    env_file:
      - api.env
    depends_on:
      cache:
        condition: service_healthy
      db:
        condition: service_healthy
      smtp:
        condition: service_healthy
    networks:
      - backend
    deploy:
      mode: replicated
      replicas: 4
    healthcheck:
      test: [ "CMD-SHELL", "celery -A keystone_api.apps.scheduler inspect ping --destination celery@$$HOSTNAME" ]
      interval: 10s
      timeout: 10s
      retries: 5

  smtp:
    image: axllent/mailpit
    container_name: keystone-smtp
    restart: unless-stopped
    environment:
      MP_MAX_MESSAGES: 5000
      MP_DATABASE: /data/mailpit.db
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1
    networks:
      - backend
    ports:
      - "8025:8025"
      - "1025:1025"
    volumes:
      - smtp_data:/data
    healthcheck:
      test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8025/readyz" ]
      interval: 10s
      timeout: 10s
      retries: 5

  cache:
    image: redis:8.0.2
    container_name: keystone-cache
    restart: unless-stopped
    command: redis-server
    networks:
      - backend
    volumes:
      - cache_data:/data
    healthcheck:
      test: [ "CMD", "redis-cli", "ping" ]
      interval: 10s
      timeout: 3s
      retries: 5

  db:
    image: postgres:17.4
    container_name: keystone-db
    restart: unless-stopped
    env_file:
      - db.env
    networks:
      - backend
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
      interval: 10s
      timeout: 10s
      retries: 5

volumes:
  uploaded_files:
    name: keystone_uploaded_files
  postgres_data:
    name: keystone_postgres_data
  cache_data:
    name: keystone_cache_data
  smtp_data:
    name: keystone_smtp_data

networks:
  frontend:
    name: keystone_frontend
  backend:
    name: keystone_backend
api.env
DJANGO_SETTINGS_MODULE="keystone_api.main.settings"
STORAGE_STATIC_DIR="/app/static"
STORAGE_UPLOAD_DIR="/app/upload_files"

# Security Settings
SECURE_SECRET_KEY="e5D+WcNeFEx363j3K9YUgypEGWgfK+vS5pgYsAX71ZcSDAAewL"
SECURE_ALLOWED_HOSTS="127.0.0.1,localhost,keystone-api"
SECURE_ALLOWED_ORIGINS="http://127.0.0.1,http://localhost,http://keystone-web,http://keystone-proxy"
SECURE_CSRF_ORIGINS="http://127.0.0.1,http://localhost,http://keystone-web,http://keystone-proxy"

# Redis settings
REDIS_HOST="cache"

# Database settings
DB_POSTGRES_ENABLE="true"
DB_NAME="keystone"
DB_USER="db_user"
DB_PASSWORD="foobar123"
DB_HOST="db"

# SMTP settings
EMAIL_HOST="smtp"
EMAIL_PORT="1025"
db.env
# Values formatted for use by the Postgres container
POSTGRES_DB="keystone"
POSTGRES_USER="db_user"
POSTGRES_PASSWORD="foobar123"

# Values formatted for use by the Postgres exporter
DATA_SOURCE_URI="db:5432/keystone?sslmode=disable"
DATA_SOURCE_USER="db_user"
DATA_SOURCE_PASS="foobar123"

Deploying the Stack

The recipe reads settings from two environment files. The api.env file is used to configure settings for the Keystone API. The db.env file is used to define settings for the backend postgres database.

Matching credentials

The database credentials in api.env and db.env must match exactly, or the API will fail to connect to the database. The REDIS_HOST and DB_HOST values must also match the corresponding service names in the compose file.

To launch the deployment, run the docker compose up command from the directory same containing the compose recipe and .env files:

docker compose up -d  # Launch a new instance

After the application launches, create a new administrative user by calling the keystone-api createsuperuser command from within the API container:

docker exec keystone-api keystone-api createsuperuser

The web interface is now available at http://localhost:80. Confirm the deployment by navigating to the site and authenticating with the newly created credentials.