Skip to main content

Docker Compose Deployment

Single-node Docker Compose deployment for Tier 1 (up to 1,000 endpoints) and Tier 2 (1,000--5,000 endpoints) deployments. For larger deployments, see Kubernetes Deployment.

Prerequisites

Production Deployment

1. Configure Environment

Copy the example environment file and generate all required secrets:

cd infra/docker
cp .env.prod.example .env

Generate passwords and keys (fill these into your .env file):

# Database passwords
PG_PASSWORD=$(openssl rand -base64 32)
CH_PASSWORD=$(openssl rand -base64 32)
MINIO_SECRET_KEY=$(openssl rand -base64 32)

# Cryptographic keys
CERT_ENCRYPTION_KEY=$(openssl rand -hex 32)
JWT_SECRET=$(openssl rand -hex 32)
EXTENSION_SIGNING_KEY=$(openssl rand -base64 32)

# Service admin passwords
KEYCLOAK_ADMIN_PASSWORD=$(openssl rand -base64 24)
GRAFANA_ADMIN_PASSWORD=$(openssl rand -base64 24)

Set your domain:

ALLOWED_ORIGINS=https://surfacesec.yourdomain.com
GRAFANA_ROOT_URL=https://grafana.yourdomain.com

Note: Do not set SURFACE_LICENSE_KEY or SURFACE_PORTAL_API_TOKEN in the .env file. These are configured through the setup wizard on first boot and stored in the database automatically.

2. Build Images

The API image must be built with your Surface Security public key embedded. This embeds the Ed25519 public key into the binary at compile time so that license signatures can be verified locally without any network calls.

cd infra/docker

# Build the API image with the public key embedded
# NOTE: --env-file must come before the subcommand (build, up, down, etc.)
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env build \
--no-cache --build-arg SURFACE_PUBLIC_KEY='-----BEGIN PUBLIC KEY-----
<your-ed25519-public-key-here>
-----END PUBLIC KEY-----'

Why is this needed? The public key is injected into the Go binary via linker flags (-ldflags) during compilation. This prevents operators from substituting a different key to forge license signatures. The embedded key always takes priority over the SURFACE_PUBLIC_KEY environment variable.

To rebuild the images after an update or configuration change, run the same build command again.

3. Start Services

cd infra/docker

# Start with production profile
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env up -d

4. Verify Services

# Check all containers are running
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env ps

# Verify API health
curl -sf http://localhost:8090/health

# Check that setup mode is active (first run)
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env logs api | grep -i "setup mode"

5. Complete the Setup Wizard

Navigate to the dashboard and complete the initial setup:

  1. Open https://localhost:3000 in your browser
  2. Create the initial admin account and tenant
  3. Enter your Surface Security license key, portal API token, and any other configuration
  4. The license and portal credentials are stored in the database and loaded automatically on subsequent restarts
ServiceURLCredentials
Dashboardhttps://localhost:3000Set during setup wizard
Redpanda Consolehttp://localhost:8181(none)
MinIO Consolehttp://localhost:9501MINIO_ACCESS_KEY / MINIO_SECRET_KEY from .env
Grafanahttp://localhost:3001admin / GRAFANA_ADMIN_PASSWORD from .env

Service Ports (Docker Compose)

ServiceHost PortContainer PortPurpose
PostgreSQL54325432Transactional database
ClickHouse (HTTP)81238123Analytics queries
ClickHouse (Native)90009000Native protocol
Redpanda90929092Event streaming
Redpanda Console81818080Streaming admin UI
Schema Registry180818081Schema management
MinIO (API)95009000Object storage
MinIO (Console)95019001Storage admin UI
Backend API80908090Application API (accessible through Envoy on port 3000 in prod; no direct host mapping in production)
Frontend30003000Admin dashboard
Envoy Gateway30003000mTLS gateway for extensions
Grafana30013000Observability dashboards

Offline / Air-Gapped Deployment

For environments without internet access:

# Copy and fill in the example environment file (see Step 1 above for required keys)
cp .env.prod.example .env

# Disable portal communication for offline mode
# Set in .env:
# SURFACE_PORTAL_URL=
#
# Leave SURFACE_LICENSE_KEY empty — it will be set during the setup wizard.

# Start services
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env up -d

# Complete the setup wizard at https://localhost:3000
# Enter your license key during the wizard — it is stored in the database.

# Verify
curl -sf http://localhost:8090/health

See Air-Gapped Deployment for the complete offline deployment procedure including image transfer.

TLS Configuration

For production deployments, configure TLS by placing your certificate and key in front of the services. Options include:

  1. Reverse proxy (nginx, Caddy, or Traefik) in front of the Docker Compose stack
  2. Envoy gateway -- already included in the stack for extension mTLS
  3. Let's Encrypt -- configure a reverse proxy with automatic certificate provisioning

Backup

Automated Backups

Backups run automatically via cron jobs:

  • PostgreSQL: Daily at 2 AM UTC
  • ClickHouse: Daily at 3 AM UTC

Manual Backup

# PostgreSQL full backup
docker compose exec postgres-backup /backup.sh full

Restore from Backup

# List available backups
ls -la /backup/

# Restore PostgreSQL
docker compose exec postgres pg_restore -U surfacesec -d surfacesec /backup/full_backup.sql.gz

Monitoring

Health Checks

# Liveness check
curl http://localhost:8090/health

# Readiness check (verifies database connectivity)
curl http://localhost:8090/ready

Logs

# All services
docker compose logs -f

# Specific service
docker compose logs -f api

# Filter errors
docker compose logs api 2>&1 | grep -i error

Metrics

Access Grafana at http://localhost:3001 for pre-configured dashboards. The OpenTelemetry Collector and Tempo provide distributed tracing for request diagnostics.

Upgrading

To upgrade Surface Security on a Docker Compose deployment:

  1. Update the SURFACESEC_VERSION in your .env file
  2. Rebuild the images with the public key embedded:
    docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env build \
    --build-arg SURFACE_PUBLIC_KEY='-----BEGIN PUBLIC KEY-----
    <your-ed25519-public-key-here>
    -----END PUBLIC KEY-----'
  3. Restart services:
    docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env up -d
  4. Verify the new version:
    curl http://localhost:8090/health

For air-gapped environments, see Update System for the offline update procedure.

Troubleshooting

Container won't start

# Check container logs
docker compose logs <service-name>

# Check resource usage
docker stats

Database connection issues

# Test PostgreSQL connection
docker compose exec postgres pg_isready -U surfacesec

# Test ClickHouse connection
docker compose exec clickhouse clickhouse-client --query "SELECT 1"

High memory usage

# Check container memory
docker stats

# Increase ClickHouse memory limits in docker-compose.yml:
# clickhouse:
# deploy:
# resources:
# limits:
# memory: 8G