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:
| Service | Image | Purpose | Port |
|---|---|---|---|
| spire-server | ghcr.io/spiffe/spire-server:1.9.6 | Certificate Authority | 8081 |
| spire-agent | ghcr.io/spiffe/spire-agent:1.9.6 | Workload attestation | 9989 |
| opa | openpolicyagent/opa:latest-static | Policy engine | 8181 |
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
- Jaeger UI: http://localhost:16686 (traces)
- Prometheus: http://localhost:9090 (metrics)
- Grafana: http://localhost:3000 (dashboards)
- Username:
admin - Password:
citadel123
- Username:
- NATS Monitoring: http://localhost:8222
- Redis Commander: http://localhost:8082
- pgAdmin: http://localhost:5050
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
| Feature | Docker Compose | .NET Aspire |
|---|---|---|
| Setup Complexity | Simple YAML | Requires .NET SDK |
| Dashboard | No built-in | Integrated |
| Hot Reload | Manual restart | Automatic (.NET) |
| Observability | Separate tools | Unified dashboard |
| Dependency Management | depends_on | Smart orchestration |
| Resource Discovery | Manual config | Automatic |
| Best For | Agents, MCP servers | .NET microservices |
| Production Ready | Yes | Azure-focused |
Next Steps
- OPA Policy Basics - Write safety policies
- Agent Template - Create custom agents
- Creating MCP Adapters - Build vendor adapters
- Production Deployment - Deploy to K3s
Docker Compose Best Practices
- Use named volumes for data persistence
- Add health checks to all services
- Set resource limits in production
- Use secrets for sensitive data
- Enable logging with rotation
- Pin image versions (avoid
:latest) - Use
.envfiles for configuration - Create overrides for local dev
Simple, powerful, production-ready. Continue to OPA Basics.