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
- Docker 25+ with Docker Compose v2
- A valid Surface Security license key (see Licensing)
- Hardware meeting Tier 1 or Tier 2 specifications
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_KEYorSURFACE_PORTAL_API_TOKENin the.envfile. 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 theSURFACE_PUBLIC_KEYenvironment 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:
- Open
https://localhost:3000in your browser - Create the initial admin account and tenant
- Enter your Surface Security license key, portal API token, and any other configuration
- The license and portal credentials are stored in the database and loaded automatically on subsequent restarts
| Service | URL | Credentials |
|---|---|---|
| Dashboard | https://localhost:3000 | Set during setup wizard |
| Redpanda Console | http://localhost:8181 | (none) |
| MinIO Console | http://localhost:9501 | MINIO_ACCESS_KEY / MINIO_SECRET_KEY from .env |
| Grafana | http://localhost:3001 | admin / GRAFANA_ADMIN_PASSWORD from .env |
Service Ports (Docker Compose)
| Service | Host Port | Container Port | Purpose |
|---|---|---|---|
| PostgreSQL | 5432 | 5432 | Transactional database |
| ClickHouse (HTTP) | 8123 | 8123 | Analytics queries |
| ClickHouse (Native) | 9000 | 9000 | Native protocol |
| Redpanda | 9092 | 9092 | Event streaming |
| Redpanda Console | 8181 | 8080 | Streaming admin UI |
| Schema Registry | 18081 | 8081 | Schema management |
| MinIO (API) | 9500 | 9000 | Object storage |
| MinIO (Console) | 9501 | 9001 | Storage admin UI |
| Backend API | 8090 | 8090 | Application API (accessible through Envoy on port 3000 in prod; no direct host mapping in production) |
| Frontend | 3000 | 3000 | Admin dashboard |
| Envoy Gateway | 3000 | 3000 | mTLS gateway for extensions |
| Grafana | 3001 | 3000 | Observability 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:
- Reverse proxy (nginx, Caddy, or Traefik) in front of the Docker Compose stack
- Envoy gateway -- already included in the stack for extension mTLS
- 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:
- Update the
SURFACESEC_VERSIONin your.envfile - 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-----'
- Restart services:
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env up -d
- 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