Docker Deployment

Deploy GritCMS with Docker Compose using PostgreSQL, Redis, and MinIO, with all services containerized.

Docker Compose is the recommended way to deploy GritCMS in a production or staging environment. It packages the API, admin dashboard, public site, PostgreSQL, Redis, and MinIO into containers.

Prerequisites

  • Docker 20.10+ installed on your server or local machine
  • Docker Compose v2 (included with Docker Desktop, or install separately on Linux)
  • At least 2 GB RAM available for the containers (4 GB recommended)

Architecture

The Docker Compose setup runs six containers:

ServiceImageInternal PortDescription
apiBuilt from apps/api8080Go backend (Gin + GORM)
adminBuilt from apps/admin3000Next.js admin dashboard
webBuilt from apps/web3000Next.js public site
postgrespostgres:16-alpine5432PostgreSQL database
redisredis:7-alpine6379Redis cache and sessions
miniominio/minio9000S3-compatible file storage

docker-compose.prod.yml

The project includes a production-ready Compose file. Here is the full configuration:

services:
  api:
    build:
      context: ./apps/api
      dockerfile: Dockerfile
    container_name: gritcms-api
    restart: unless-stopped
    expose:
      - "8080"
    env_file:
      - .env
    environment:
      APP_ENV: production
      DATABASE_URL: postgres://${POSTGRES_USER:-grit}:${POSTGRES_PASSWORD:-grit}@postgres:5432/${POSTGRES_DB:-gritcms}?sslmode=disable
      REDIS_URL: redis://redis:6379
      JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
      APP_URL: ${API_URL:-http://localhost:8080}
      CORS_ORIGINS: ${WEB_URL:-http://localhost:3000},${ADMIN_URL:-http://localhost:3001}
      STORAGE_DRIVER: ${STORAGE_DRIVER:-minio}
      MINIO_ENDPOINT: http://minio:9000
      MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin}
      MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}
      MINIO_BUCKET: ${MINIO_BUCKET:-uploads}
      RESEND_API_KEY: ${RESEND_API_KEY:-}
      MAIL_FROM: ${MAIL_FROM:-noreply@localhost}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
 
  web:
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    container_name: gritcms-web
    restart: unless-stopped
    expose:
      - "3000"
    environment:
      NEXT_PUBLIC_API_URL: ${API_URL:-http://api:8080}
 
  admin:
    build:
      context: .
      dockerfile: apps/admin/Dockerfile
    container_name: gritcms-admin
    restart: unless-stopped
    expose:
      - "3000"
    environment:
      NEXT_PUBLIC_API_URL: ${API_URL:-http://api:8080}
 
  postgres:
    image: postgres:16-alpine
    container_name: gritcms-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-grit}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-grit}
      POSTGRES_DB: ${POSTGRES_DB:-gritcms}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-grit}"]
      interval: 5s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    container_name: gritcms-redis
    restart: unless-stopped
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
 
  minio:
    image: minio/minio
    container_name: gritcms-minio
    restart: unless-stopped
    environment:
      MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
      MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
    volumes:
      - minio-data:/data
    command: server /data --console-address ":9001"
 
volumes:
  postgres-data:
  redis-data:
  minio-data:

Why expose instead of ports?

The Compose file uses expose rather than ports for the application services. This makes ports available only on the internal Docker network -- not bound to the host machine. This is the correct approach when running behind a reverse proxy (Traefik, Nginx, Caddy) which routes traffic to containers internally.

If you need direct host access (e.g., no reverse proxy), change expose to ports:

# Direct host access (no reverse proxy)
ports:
  - "8080:8080"  # api
  - "3000:3000"  # web
  - "3001:3000"  # admin (maps host 3001 to container 3000)

Environment Variables

Create a .env file in the project root alongside the Compose file:

# Required
JWT_SECRET=generate-a-64-char-random-string
POSTGRES_USER=grit
POSTGRES_PASSWORD=use-a-strong-password
POSTGRES_DB=gritcms
 
# Domains (update to your actual URLs)
API_URL=https://api.yourdomain.com
WEB_URL=https://yourdomain.com
ADMIN_URL=https://admin.yourdomain.com
 
# Storage
STORAGE_DRIVER=minio
MINIO_ACCESS_KEY=your-minio-key
MINIO_SECRET_KEY=your-minio-secret
MINIO_BUCKET=uploads
 
# Email (Resend)
RESEND_API_KEY=re_your_api_key
MAIL_FROM=hello@yourdomain.com

Generate secure secrets with:

openssl rand -hex 32

Full Variable Reference

VariableRequiredDefaultDescription
JWT_SECRETYes--Secret key for JWT authentication
POSTGRES_USERNogritPostgreSQL username
POSTGRES_PASSWORDNogritPostgreSQL password
POSTGRES_DBNogritcmsPostgreSQL database name
API_URLNohttp://localhost:8080Public API URL
WEB_URLNohttp://localhost:3000Public web URL
ADMIN_URLNohttp://localhost:3001Admin dashboard URL
STORAGE_DRIVERNominiominio, r2, or local
MINIO_ACCESS_KEYNominioadminMinIO access key
MINIO_SECRET_KEYNominioadminMinIO secret key
MINIO_BUCKETNouploadsMinIO bucket name
RESEND_API_KEYNo--Resend email API key
MAIL_FROMNonoreply@localhostSender email address
SENTINEL_ENABLEDNotrueEnable Sentinel error tracker
PULSE_ENABLEDNotrueEnable Pulse request monitor

Building and Running

From the project root, build and start all services:

docker compose -f docker-compose.prod.yml up -d --build

Check that all containers are running:

docker compose -f docker-compose.prod.yml ps

View logs:

docker compose -f docker-compose.prod.yml logs -f

View logs for a specific service:

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

Initial Setup

After the first deployment:

  1. Visit the admin dashboard and register your admin account (first user becomes admin)
  2. Complete the Setup Wizard -- it saves settings, creates your email list, and seeds your Home page
  3. Visit the public site to see your live site

Persistent Volumes

The Compose file defines three named volumes:

  • postgres-data -- PostgreSQL database files
  • redis-data -- Redis persistence data
  • minio-data -- Uploaded files and media

Never delete these volumes unless you intentionally want to wipe data.

Updating the Deployment

To deploy a new version:

git pull
docker compose -f docker-compose.prod.yml up -d --build

Docker Compose rebuilds only the images that have changed. Database volumes are preserved.

Production Checklist

  • Replace default secrets. Always set strong, unique JWT_SECRET, POSTGRES_PASSWORD, MINIO_ACCESS_KEY, and MINIO_SECRET_KEY values.
  • Use a reverse proxy. Place Nginx, Caddy, or Traefik in front of the services to handle SSL termination and domain routing.
  • Back up the database. Schedule regular backups using pg_dump:
    docker exec gritcms-postgres pg_dump -U grit gritcms > backup_$(date +%Y%m%d).sql
  • Resource limits. Consider setting memory and CPU limits on each service for predictable resource usage.
  • Configure CORS. The CORS_ORIGINS variable is built automatically from WEB_URL and ADMIN_URL.