Skip to main content

Docker Compose Deployment

Docker Compose provides a simpler alternative to .NET Aspire for running CitadelMesh infrastructure. This guide covers when to use it and how to configure it effectively.

When to Use Docker Compose

Use Docker Compose If:

  • ✅ You don't need .NET development
  • ✅ You're building only Python agents or MCP adapters
  • ✅ You want simpler deployment without Aspire overhead
  • ✅ You're deploying to a server without .NET SDK
  • ✅ You prefer infrastructure-as-code in YAML

Use Aspire If:

  • ✅ You're developing .NET microservices
  • ✅ You want integrated dashboard and observability
  • ✅ You need hot reload for .NET code
  • ✅ You want resource dependency management
  • ✅ You're deploying to Azure Container Apps

SPIRE + Infrastructure Stack

CitadelMesh provides a complete Docker Compose setup with SPIRE for zero-trust identity.

Stack Components

The docker-compose-spire.yml file defines:

ServiceImagePurposePort
spire-serverghcr.io/spiffe/spire-server:1.9.6Certificate Authority8081
spire-agentghcr.io/spiffe/spire-agent:1.9.6Workload attestation9989
opaopenpolicyagent/opa:latest-staticPolicy engine8181

Starting the Stack

cd /path/to/CitadelMesh

# Start SPIRE + OPA
docker compose -f docker-compose-spire.yml up -d

# View logs
docker compose -f docker-compose-spire.yml logs -f

# Check status
docker compose -f docker-compose-spire.yml ps

Expected Output:

NAME                    STATUS   PORTS
citadel-spire-server healthy 0.0.0.0:8081->8081/tcp, 0.0.0.0:9988->9988/tcp
citadel-spire-agent running 0.0.0.0:9989->9989/tcp
citadel-opa running 0.0.0.0:8181->8181/tcp

Verifying Services

# Check SPIRE Server health
curl http://localhost:8081/healthz
# Expected: HTTP 200 OK

# Check OPA health
curl http://localhost:8181/health
# Expected: {"status": "ok"}

# List SPIRE entries
docker exec citadel-spire-server \
/opt/spire/bin/spire-server entry show

Full Stack with Additional Services

Create docker-compose.yml for complete infrastructure:

version: '3.9'

services:
# Extend SPIRE stack
spire-server:
extends:
file: docker-compose-spire.yml
service: spire-server

spire-agent:
extends:
file: docker-compose-spire.yml
service: spire-agent

opa:
extends:
file: docker-compose-spire.yml
service: opa

# Redis for caching
redis:
image: redis:7-alpine
container_name: citadel-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- citadel-mesh
command: redis-server --appendonly yes

# Redis Commander (Web UI)
redis-commander:
image: rediscommander/redis-commander:latest
container_name: citadel-redis-ui
environment:
- REDIS_HOSTS=local:redis:6379
ports:
- "8082:8081"
networks:
- citadel-mesh
depends_on:
- redis

# PostgreSQL for persistence
postgres:
image: postgres:16-alpine
container_name: citadel-postgres
environment:
POSTGRES_DB: citadel-db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: citadel123
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- citadel-mesh
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5

# pgAdmin (Web UI)
pgadmin:
image: dpage/pgadmin4:latest
container_name: citadel-pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: admin@citadel.local
PGADMIN_DEFAULT_PASSWORD: citadel123
ports:
- "5050:80"
networks:
- citadel-mesh
depends_on:
- postgres

# NATS for event bus
nats:
image: nats:2.10-alpine
container_name: citadel-nats
command: ["-js", "-m", "8222"]
ports:
- "4222:4222" # Client connections
- "8222:8222" # Monitoring
networks:
- citadel-mesh

# Jaeger for distributed tracing
jaeger:
image: jaegertracing/all-in-one:latest
container_name: citadel-jaeger
environment:
COLLECTOR_OTLP_ENABLED: true
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
networks:
- citadel-mesh

# Prometheus for metrics
prometheus:
image: prom/prometheus:latest
container_name: citadel-prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
ports:
- "9090:9090"
volumes:
- ./config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
networks:
- citadel-mesh

# Grafana for dashboards
grafana:
image: grafana/grafana:latest
container_name: citadel-grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: citadel123
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
- ./config/grafana/provisioning:/etc/grafana/provisioning:ro
networks:
- citadel-mesh
depends_on:
- prometheus

volumes:
redis-data:
postgres-data:
prometheus-data:
grafana-data:
spire-server-data:
spire-agent-data:
spire-sockets:

networks:
citadel-mesh:
name: citadel-mesh
driver: bridge

Starting Full Stack

# Start all services
docker compose up -d

# Start specific services
docker compose up -d redis postgres nats

# View all logs
docker compose logs -f

# View specific service logs
docker compose logs -f opa

Environment Configuration

Using .env File

Create .env in the same directory as docker-compose.yml:

# Database
POSTGRES_DB=citadel-db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=citadel123

# Redis
REDIS_PASSWORD=

# OPA
OPA_LOG_LEVEL=info
OPA_LOG_FORMAT=json

# SPIRE
SPIRE_TRUST_DOMAIN=citadel.local
SPIRE_SERVER_ADDRESS=spire-server:8081

# Observability
JAEGER_SAMPLING_RATE=1.0
PROMETHEUS_RETENTION=15d

# Development
ENABLE_DEBUG_ENDPOINTS=true

Docker Compose automatically loads .env file.

Overriding Defaults

Create docker-compose.override.yml for local customization:

version: '3.9'

services:
opa:
environment:
- OPA_LOG_LEVEL=debug # More verbose logging
volumes:
- ./policies:/policies:rw # Read-write for policy editing

postgres:
ports:
- "5433:5432" # Avoid conflict with local PostgreSQL

redis:
command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru

Override file is automatically merged with docker-compose.yml.

Development Workflow

1. Start Infrastructure

# Start with logs
docker compose up

# Or detached mode
docker compose up -d && docker compose logs -f

2. Develop Agents

Agents connect to Docker services:

cd src/agents
source .venv/bin/activate

# Configure to use Docker services
export NATS_URL=nats://localhost:4222
export OPA_URL=http://localhost:8181
export REDIS_URL=redis://localhost:6379

# Run agent
python security/security_agent.py

3. Edit and Test Policies

# Edit policy
vim policies/security.rego

# OPA auto-reloads the policy
# Test immediately
curl -X POST http://localhost:8181/v1/data/citadel/security/allow \
-H 'Content-Type: application/json' \
-d '{
"input": {
"action": "door_unlock",
"duration_seconds": 300
}
}'

4. View Observability

Adding MCP Servers

Add MCP adapters to docker-compose.yml:

services:
# EcoStruxure EBO Adapter
ecostruxure-mcp:
build: ./mcp-servers/ecostruxure-ebo
container_name: citadel-mcp-ecostruxure
environment:
- OPA_URL=http://opa:8181
- NATS_URL=nats://nats:4222
- ENABLE_MOCK_MODE=true
- LOG_LEVEL=debug
ports:
- "3001:3000"
networks:
- citadel-mesh
depends_on:
- opa
- nats
volumes:
- ./mcp-servers/ecostruxure-ebo:/app
- /app/node_modules # Prevent overwriting

# Security Expert Adapter
security-expert-mcp:
build: ./mcp-servers/security-expert
container_name: citadel-mcp-security
environment:
- OPA_URL=http://opa:8181
- NATS_URL=nats://nats:4222
ports:
- "3002:3000"
networks:
- citadel-mesh
depends_on:
- opa
- nats

Dockerfile for MCP server (mcp-servers/ecostruxure-ebo/Dockerfile):

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["node", "dist/index.js"]

Production Considerations

Resource Limits

Add resource constraints:

services:
postgres:
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 1G

redis:
deploy:
resources:
limits:
cpus: '1'
memory: 512M

Health Checks

Add health checks for reliability:

services:
nats:
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8222/healthz"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s

redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3

Logging

Configure JSON logging for production:

services:
opa:
environment:
- OPA_LOG_FORMAT=json
- OPA_LOG_LEVEL=info
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

Secrets Management

Use Docker secrets instead of environment variables:

services:
postgres:
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
secrets:
- postgres_password

secrets:
postgres_password:
file: ./secrets/postgres_password.txt

Common Operations

View Service Status

# List all services
docker compose ps

# Detailed status
docker compose ps --format json | jq

Restart Services

# Restart single service
docker compose restart opa

# Restart all services
docker compose restart

# Recreate containers (apply changes)
docker compose up -d --force-recreate

Update Images

# Pull latest images
docker compose pull

# Rebuild and restart
docker compose up -d --build

Clean Up

# Stop all services
docker compose down

# Stop and remove volumes (⚠️ deletes data)
docker compose down -v

# Remove everything including images
docker compose down -v --rmi all

Backup Data

# Backup PostgreSQL
docker exec citadel-postgres pg_dump -U postgres citadel-db > backup.sql

# Backup Redis
docker exec citadel-redis redis-cli SAVE
docker cp citadel-redis:/data/dump.rdb ./redis-backup.rdb

Troubleshooting

Service won't start

Error: Error response from daemon: Conflict

Solution:

# Remove conflicting containers
docker compose down
docker compose up -d

Port already in use

Error: Bind for 0.0.0.0:8181 failed: port is already allocated

Solution:

# Find process using port
lsof -ti:8181 | xargs kill -9 # macOS/Linux

# Or change port in docker-compose.yml
ports:
- "8182:8181" # Use 8182 externally

OPA policies not loading

Error: Policy bundle not found

Solution:

# Check volume mount
docker compose config | grep -A5 opa

# Verify policies directory
ls -la policies/

# Manually load policy
docker exec citadel-opa \
/opa run --server --addr 0.0.0.0:8181 /policies

Networking issues

Error: Services can't communicate

Solution:

# Verify network
docker network ls | grep citadel-mesh

# Inspect network
docker network inspect citadel-mesh

# Test connectivity
docker exec citadel-opa ping postgres

Persistent data lost

Issue: Data disappears on restart

Solution:

# Verify volumes
docker volume ls | grep citadel

# Use named volumes in docker-compose.yml
volumes:
postgres-data:
name: citadel-postgres-data
driver: local

Docker Compose vs Aspire Comparison

FeatureDocker Compose.NET Aspire
Setup ComplexitySimple YAMLRequires .NET SDK
DashboardNo built-inIntegrated
Hot ReloadManual restartAutomatic (.NET)
ObservabilitySeparate toolsUnified dashboard
Dependency Managementdepends_onSmart orchestration
Resource DiscoveryManual configAutomatic
Best ForAgents, MCP servers.NET microservices
Production ReadyYesAzure-focused

Next Steps

Docker Compose Best Practices

  1. Use named volumes for data persistence
  2. Add health checks to all services
  3. Set resource limits in production
  4. Use secrets for sensitive data
  5. Enable logging with rotation
  6. Pin image versions (avoid :latest)
  7. Use .env files for configuration
  8. Create overrides for local dev

Simple, powerful, production-ready. Continue to OPA Basics.