Skip to main content
Version: 1.0.1

Deployment Guide

This guide covers deploying Milvaion to production environments using Docker, Kubernetes, and traditional VMs.

Deployment Checklist

Before going to production, ensure you have:

  • PostgreSQL with proper backup strategy
  • Redis with persistence enabled (RDB or AOF)
  • RabbitMQ with durable queues
  • Health checks configured
  • Resource limits defined

Docker Compose (Simple Production)

For single-server or small deployments:

docker-compose.prod.yml

version: '3.8'

services:
postgres:
image: postgres:16-alpine
container_name: milvaion-postgres
restart: always
environment:
POSTGRES_DB: MilvaionDb
POSTGRES_USER: milvaion
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backups:/backups
healthcheck:
test: ["CMD-SHELL", "pg_isready -U milvaion -d MilvaionDb"]
interval: 10s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
container_name: milvaion-redis
restart: always
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5

rabbitmq:
image: rabbitmq:3-management-alpine
container_name: milvaion-rabbitmq
restart: always
environment:
RABBITMQ_DEFAULT_USER: milvaion
RABBITMQ_DEFAULT_PASS_FILE: /run/secrets/rabbitmq_password
secrets:
- rabbitmq_password
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmqctl", "status"]
interval: 30s
timeout: 10s
retries: 5

milvaion-api:
image: milvasoft/milvaion-api:1.0.0
container_name: milvaion-api
restart: always
ports:
- "5000:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ConnectionStrings__DefaultConnectionString=Host=postgres;Port=5432;Database=MilvaionDb;Username=milvaion;Password=${DB_PASSWORD}
- MilvaionConfig__Redis__ConnectionString=redis:6379
- MilvaionConfig__Redis__Password=${REDIS_PASSWORD}
- MilvaionConfig__RabbitMQ__Host=rabbitmq
- MilvaionConfig__RabbitMQ__Username=milvaion
- MilvaionConfig__RabbitMQ__Password=${RABBITMQ_PASSWORD}
- MILVAION_ROOT_PASSWORD=admin
- MILVA_ENV=prod
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3

email-worker:
image: my-company/email-worker:1.0.0
restart: always
deploy:
replicas: 3
environment:
- Worker__WorkerId=email-worker
- Worker__MaxParallelJobs=20
- Worker__RabbitMQ__Host=rabbitmq
- Worker__RabbitMQ__Username=milvaion
- Worker__RabbitMQ__Password=${RABBITMQ_PASSWORD}
- Worker__Redis__ConnectionString=redis:6379,password=${REDIS_PASSWORD}
depends_on:
- milvaion-api

secrets:
db_password:
file: ./secrets/db_password.txt
rabbitmq_password:
file: ./secrets/rabbitmq_password.txt

volumes:
postgres_data:
redis_data:
rabbitmq_data:

Deploy Commands

# Create secrets
mkdir -p secrets
echo "your-secure-db-password" > secrets/db_password.txt
echo "your-secure-rabbitmq-password" > secrets/rabbitmq_password.txt

# Create .env file
cat > .env << EOF
DB_PASSWORD=your-secure-db-password
REDIS_PASSWORD=your-secure-redis-password
RABBITMQ_PASSWORD=your-secure-rabbitmq-password
EOF

# Deploy
docker compose -f docker-compose.prod.yml up -d

# Scale workers
docker compose -f docker-compose.prod.yml up -d --scale email-worker=5

Kubernetes Deployment

For larger, scalable deployments:

Namespace and Secrets

# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: milvaion

---
# secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: milvaion-secrets
namespace: milvaion
type: Opaque
stringData:
db-password: "your-secure-db-password"
redis-password: "your-secure-redis-password"
rabbitmq-password: "your-secure-rabbitmq-password"

API Deployment

# api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: milvaion-api
namespace: milvaion
spec:
replicas: 2
selector:
matchLabels:
app: milvaion-api
template:
metadata:
labels:
app: milvaion-api
spec:
containers:
- name: api
image: milvasoft/milvaion-api:1.0.0
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: ConnectionStrings__DefaultConnectionString
valueFrom:
secretKeyRef:
name: milvaion-secrets
key: db-connection-string
- name: MilvaionConfig__Redis__ConnectionString
value: "redis.milvaion.svc:6379"
- name: MilvaionConfig__Redis__Password
valueFrom:
secretKeyRef:
name: milvaion-secrets
key: redis-password
- name: MilvaionConfig__RabbitMQ__Host
value: "rabbitmq.milvaion.svc"
- name: MilvaionConfig__RabbitMQ__Password
valueFrom:
secretKeyRef:
name: milvaion-secrets
key: rabbitmq-password
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5

---
apiVersion: v1
kind: Service
metadata:
name: milvaion-api
namespace: milvaion
spec:
selector:
app: milvaion-api
ports:
- port: 80
targetPort: 8080
type: ClusterIP

Worker Deployment

# worker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: email-worker
namespace: milvaion
spec:
replicas: 3
selector:
matchLabels:
app: email-worker
template:
metadata:
labels:
app: email-worker
spec:
containers:
- name: worker
image: my-company/email-worker:1.0.0
env:
- name: Worker__WorkerId
value: "email-worker"
- name: Worker__MaxParallelJobs
value: "20"
- name: Worker__RabbitMQ__Host
value: "rabbitmq.milvaion.svc"
- name: Worker__RabbitMQ__Password
valueFrom:
secretKeyRef:
name: milvaion-secrets
key: rabbitmq-password
- name: Worker__Redis__ConnectionString
value: "redis.milvaion.svc:6379"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
exec:
command: ["cat", "/tmp/healthy"]
initialDelaySeconds: 30
periodSeconds: 10

Horizontal Pod Autoscaler

# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: email-worker-hpa
namespace: milvaion
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: email-worker
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70

Traditional Deployment (VM/Bare Metal)

For deployments on VMs, bare metal servers, or when not using containers.

Prerequisites:

  • PostgreSQL, Redis, and RabbitMQ are already installed and running
  • .NET 10 Runtime is installed (dotnet --version shows 10.x)
  • Appropriate network access between components

1. Deploy Milvaion API

Step 1: Build the API

On your build server or development machine:

# Clone repository
git clone https://github.com/Milvasoft/milvaion.git
cd milvaion

# Build in Release mode
dotnet publish src/Milvaion.Api/Milvaion.Api.csproj \
-c Release \
-o ./publish/api \
--self-contained false

# Create deployment package
cd publish/api
tar -czf milvaion-api.tar.gz *

Step 2: Transfer to Production Server

# Transfer to server
scp milvaion-api.tar.gz user@production-server:/opt/milvaion/

# On production server
cd /opt/milvaion
tar -xzf milvaion-api.tar.gz
rm milvaion-api.tar.gz

Step 3: Configure appsettings.Production.json

Create /opt/milvaion/appsettings.Production.json:

{
"ConnectionStrings": {
"DefaultConnectionString": "Host=postgres;Port=5432;Database=MilvaionDb;Username=postgres;Password=N4SQp.qW>6?xwWzg;Pooling=true;Minimum Pool Size=20;Maximum Pool Size=100;Connection Lifetime=900;Connection Idle Lifetime=180;Command Timeout=30;Include Error Detail=true"
},
"Cors": {
"DefaultPolicy": "AllowAll",
"Policies": {
"AllowAll": {
"AllowAnyOriginWithCredentials": true,
"Origins": [ "All" ],
"Methods": [ "All" ],
"Headers": [ "All" ],
"ExposedHeaders": [ "Content-Disposition" ],
"AllowCredentials": false
}
}
},
"MilvaionConfig": {
"Logging": {
"Seq": {
"Enabled": true,
"Uri": "http://seq:5341"
}
},
"OpenTelemetry": {
"Enabled": true,
"ExportPath": "/api/metrics",
"Service": "milvaion-backend",
"Environment": "milvaion-test",
"Job": "app-metrics",
"Instance": "milvaion-test"
},
"Redis": {
"ConnectionString": "redis:6379",
"Password": "",
"Database": 0,
"ConnectTimeout": 5000,
"SyncTimeout": 5000,
"KeyPrefix": "Milvaion:JobScheduler:",
"DefaultLockTtlSeconds": 600
},
"RabbitMQ": {
"Host": "rabbitmq",
"Port": 5672,
"Username": "guest",
"Password": "guest",
"VirtualHost": "/",
"Durable": true,
"AutoDelete": false,
"ConnectionTimeout": 30,
"Heartbeat": 60,
"AutomaticRecoveryEnabled": true,
"NetworkRecoveryInterval": 10,
"QueueDepthWarningThreshold": 100,
"QueueDepthCriticalThreshold": 500
},
"JobDispatcher": {
"Enabled": true,
"PollingIntervalSeconds": 1,
"BatchSize": 100,
"LockTtlSeconds": 600,
"EnableStartupRecovery": true
},
"WorkerAutoDiscovery": {
"Enabled": true
},
"ZombieOccurrenceDetector": {
"Enabled": true,
"CheckIntervalSeconds": 300,
"ZombieTimeoutMinutes": 10
},
"LogCollector": {
"Enabled": true,
"BatchSize": 100,
"BatchIntervalMs": 1000
},
"StatusTracker": {
"Enabled": true,
"BatchSize": 50,
"BatchIntervalMs": 100,
"ExecutionLogMaxCount": 100
},
"FailedOccurrenceHandler": {
"Enabled": true
},
"JobAutoDisable": {
"Enabled": true,
"ConsecutiveFailureThreshold": 5,
"FailureWindowMinutes": 60,
"AutoReEnableAfterCooldown": false,
"AutoReEnableCooldownMinutes": 30
}
},
"Serilog": {
"Using": [ "Serilog.Expressions", "Serilog.Sinks.Console" ],
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft.AspNetCore": "Information",
"System": "Warning",
"Microsoft.AspNetCore.Mvc": "Warning",
"Microsoft.AspNetCore.Cors": "Warning",
"Microsoft.AspNetCore.Routing": "Warning",
"Microsoft.AspNetCore.Hosting.Diagnostics": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.EntityFrameworkCore.Update": "Warning",
"Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler": "Warning"
}
},
"WriteTo": [
{
"Name": "Logger",
"Args": {
"configureLogger": {
"WriteTo": [
{
"Name": "Console"
}
]
}
}
}
]
}
}

Step 4: Apply Database Migrations

cd /opt/milvaion

# Apply migrations
dotnet Milvaion.Api.dll --migrate

# Or manually with EF Core tools
dotnet ef database update --project /path/to/Milvaion.Api.csproj

Step 5: Create systemd Service

Create /etc/systemd/system/milvaion-api.service:

[Unit]
Description=Milvaion Scheduler API
After=network.target postgresql.service redis.service rabbitmq-server.service

[Service]
Type=notify
User=milvaion
Group=milvaion
WorkingDirectory=/opt/milvaion
ExecStart=/usr/bin/dotnet /opt/milvaion/Milvaion.Api.dll
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=milvaion-api
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://0.0.0.0:5000
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

# Resource limits
LimitNOFILE=65536
MemoryLimit=2G

[Install]
WantedBy=multi-user.target

Step 6: Start the Service

# Create user for running service
sudo useradd -r -s /bin/false milvaion
sudo chown -R milvaion:milvaion /opt/milvaion

# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable milvaion-api
sudo systemctl start milvaion-api

# Check status
sudo systemctl status milvaion-api

# View logs
sudo journalctl -u milvaion-api -f

Step 7: Configure Reverse Proxy (Nginx)

Create /etc/nginx/sites-available/milvaion:

upstream milvaion_api {
server localhost:5000;
}

server {
listen 80;
server_name api.your-domain.com;

# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl http2;
server_name api.your-domain.com;

ssl_certificate /etc/ssl/certs/your-cert.crt;
ssl_certificate_key /etc/ssl/private/your-key.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;

client_max_body_size 10M;

location / {
proxy_pass http://milvaion_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}

# Health check endpoint (no auth required)
location /api/v1/healthcheck {
proxy_pass http://milvaion_api;
access_log off;
}
}

Enable and reload Nginx:

sudo ln -s /etc/nginx/sites-available/milvaion /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Step 8: Verify Deployment

# Check health
curl http://localhost:5000/api/v1/healthcheck/ready

# Or via Nginx
curl https://api.your-domain.com/api/v1/healthcheck/ready

# Check logs
sudo journalctl -u milvaion-api --since "10 minutes ago"

2. Deploy Worker

Step 1: Build the Worker

# On build server
cd your-worker-project

# Build in Release mode
dotnet publish -c Release -o ./publish/worker --self-contained false

# Create deployment package
cd publish/worker
tar -czf my-worker.tar.gz *

Step 2: Transfer to Production Server

# Transfer to server (can be same or different server as API)
scp my-worker.tar.gz user@worker-server:/opt/milvaion-workers/my-worker/

# On worker server
cd /opt/milvaion-workers/my-worker
tar -xzf my-worker.tar.gz
rm my-worker.tar.gz

Step 3: Configure appsettings.Production.json

Create /opt/milvaion-workers/my-worker/appsettings.Production.json:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
},
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.Seq"],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog"
}
},
{
"Name": "Seq",
"Args": {
"serverUrl": "http://localhost:5341"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
},
"Worker": {
"WorkerId": "my-worker-prod-01",
"MaxParallelJobs": 20,
"ExecutionTimeoutSeconds": 300,
"RabbitMQ": {
"Host": "localhost",
"Port": 5672,
"Username": "milvaion",
"Password": "YOUR_RABBITMQ_PASSWORD",
"VirtualHost": "/"
},
"Redis": {
"ConnectionString": "localhost:6379",
"Password": "YOUR_REDIS_PASSWORD",
"Database": 0,
"CancellationChannel": "Milvaion:JobScheduler:cancellation_channel"
},
"Heartbeat": {
"Enabled": true,
"IntervalSeconds": 5,
"JobHeartbeatIntervalSeconds": 60
},
"HealthCheck": {
"Enabled": true,
"LiveFilePath": "/tmp/milvaion-worker-live",
"ReadyFilePath": "/tmp/milvaion-worker-ready",
"IntervalSeconds": 30
},
"OfflineResilience": {
"Enabled": true,
"LocalStoragePath": "/var/lib/milvaion-worker/outbox",
"SyncIntervalSeconds": 30,
"MaxSyncRetries": 3,
"CleanupIntervalHours": 1,
"RecordRetentionDays": 1
}
},
"JobConsumers": {
"MyJob": {
"ConsumerId": "my-job-consumer",
"MaxParallelJobs": 30,
"MaxParallelJobsPerWorker": 30,
"ExecutionTimeoutSeconds": 120,
"MaxRetries": 3,
"BaseRetryDelaySeconds": 10,
"LogUserFriendlyLogsViaLogger": true
}
}
}

Step 4: Create systemd Service

Create /etc/systemd/system/milvaion-worker-my-worker.service:

[Unit]
Description=Milvaion Worker - My Worker
After=network.target rabbitmq-server.service redis.service

[Service]
Type=simple
User=milvaion
Group=milvaion
WorkingDirectory=/opt/milvaion-workers/my-worker
ExecStart=/usr/bin/dotnet /opt/milvaion-workers/my-worker/MyWorker.dll
Restart=always
RestartSec=10
KillSignal=SIGTERM
TimeoutStopSec=30
SyslogIdentifier=milvaion-worker-my-worker
Environment=DOTNET_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

# Resource limits
LimitNOFILE=65536
MemoryLimit=1G

[Install]
WantedBy=multi-user.target

Step 5: Start the Worker

# Create directories
sudo mkdir -p /var/lib/milvaion-worker/outbox
sudo chown -R milvaion:milvaion /opt/milvaion-workers/my-worker
sudo chown -R milvaion:milvaion /var/lib/milvaion-worker

# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable milvaion-worker-my-worker
sudo systemctl start milvaion-worker-my-worker

# Check status
sudo systemctl status milvaion-worker-my-worker

# View logs
sudo journalctl -u milvaion-worker-my-worker -f

Step 6: Scale Workers (Optional)

For multiple worker instances:

# Copy worker to different directories
sudo cp -r /opt/milvaion-workers/my-worker /opt/milvaion-workers/my-worker-02
sudo cp -r /opt/milvaion-workers/my-worker /opt/milvaion-workers/my-worker-03

# Update WorkerId in each appsettings.Production.json
sudo nano /opt/milvaion-workers/my-worker-02/appsettings.Production.json
# Change: "WorkerId": "my-worker-prod-02"

# Create separate systemd services
sudo cp /etc/systemd/system/milvaion-worker-my-worker.service \
/etc/systemd/system/milvaion-worker-my-worker-02.service

# Update paths and start
sudo systemctl daemon-reload
sudo systemctl enable milvaion-worker-my-worker-02
sudo systemctl start milvaion-worker-my-worker-02

Step 7: Verify Worker Deployment

# Check worker is running
sudo systemctl status milvaion-worker-my-worker

# Check logs for connection
sudo journalctl -u milvaion-worker-my-worker | grep -E "Connected|Started"

# Check RabbitMQ Management UI
# http://localhost:15672 - should see worker connection

# Verify worker registration in API
curl https://api.your-domain.com/api/v1/workers \
-H "Authorization: Bearer YOUR_TOKEN"

# Check health file
ls -la /tmp/milvaion-worker-live
ls -la /tmp/milvaion-worker-ready

Monitoring and Maintenance

Log Rotation

Create /etc/logrotate.d/milvaion:

/var/log/milvaion/*.log {
daily
rotate 14
compress
delaycompress
notifempty
create 0640 milvaion milvaion
sharedscripts
postrotate
systemctl reload milvaion-api > /dev/null 2>&1 || true
endscript
}

Monitoring Script

Create /usr/local/bin/check-milvaion-health.sh:

#!/bin/bash

API_URL="http://localhost:5000/api/v1/healthcheck/ready"
WORKER_HEALTH="/tmp/milvaion-worker-live"

# Check API
if ! curl -f -s "$API_URL" > /dev/null; then
echo "API health check failed!"
systemctl restart milvaion-api
fi

# Check Worker
if [ ! -f "$WORKER_HEALTH" ]; then
echo "Worker health file missing!"
systemctl restart milvaion-worker-my-worker
fi

# Check file age (should be updated every 30s)
if [ $(( $(date +%s) - $(stat -c %Y "$WORKER_HEALTH") )) -gt 60 ]; then
echo "Worker health file is stale!"
systemctl restart milvaion-worker-my-worker
fi

Add to crontab:

sudo crontab -e
# Add line:
*/5 * * * * /usr/local/bin/check-milvaion-health.sh

Health Checks

See the health check section for detailed information.


Resource Recommendations

API Server

WorkloadCPUMemoryReplicas
Small (<1K jobs/day)250m512Mi1
Medium (1K-10K jobs/day)500m2Gi1
Large (>10K jobs/day)1000m4Gi1

Workers

Job TypeCPUMemoryConcurrency
I/O-bound (email, API calls)100m128Mi50-100
CPU-bound (reports, processing)500m512Mi2-5
Memory-intensive (data analysis)250m2Gi1-2

Infrastructure

ComponentProduction Recommendation
PostgreSQL2 CPU, 4GB RAM, SSD storage
Redis1 CPU, 2GB RAM, persistence enabled
RabbitMQ2 CPU, 2GB RAM, durable queues

Logging in Workers

Structured Logging

Configure JSON logging for production:

{
"Serilog": {
"Using": ["Serilog.Sinks.Console"],
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "Console",
"Args": {
"formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
}
}

Centralized Logging

Send logs to ELK, Seq, or cloud logging:

// Program.cs
builder.Host.UseSerilog((context, config) =>
{
config
.ReadFrom.Configuration(context.Configuration)
.WriteTo.Seq("http://seq.internal:5341")
.Enrich.WithProperty("Service", "email-worker")
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName);
});

Backup Strategy

PostgreSQL

# Daily backup script
#!/bin/bash
BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
pg_dump -h localhost -U milvaion MilvaionDb | gzip > $BACKUP_DIR/milvaion_$TIMESTAMP.sql.gz

# Keep last 7 days
find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete

Redis

Enable persistence in redis.conf:

# RDB snapshots
save 900 1
save 300 10
save 60 10000

# AOF for durability
appendonly yes
appendfsync everysec

Security Checklist

  • All passwords are in secrets, not config files
  • TLS enabled for all external connections
  • Database accessible only from API server
  • Redis/RabbitMQ not exposed publicly
  • API behind load balancer/reverse proxy
  • Rate limiting on API endpoints
  • Audit logging enabled
  • Regular security updates

What's Next?