Production Guide
A checklist and guide for deploying ArchForge to a production environment.
Pre-Deployment Checklist
| # | Task | Priority | Notes |
|---|---|---|---|
| 1 | Set production secrets | Critical | No defaults — DB_PASSWORD, Redis, RSA, Sa-Token Redis, etc. |
| 2 | Change RSA private key | Critical | Generate a new RSA key pair; pass it as ARCH_FORGE_RSA_PRIVATE_KEY (the admin does not start without it) |
| 3 | Change PostgreSQL password | Critical | Use a strong password |
| 4 | Enable captcha | High | Set arch-forge.captcha.enabled: true |
| 5 | Configure PostgreSQL (master/slave) | High | Set up replication if needed |
| 6 | Configure Redis | High | Dedicated instance with a password — REDIS_PASSWORD (docker-compose.prod.yml and docker-compose.staging.yml start Redis with --requirepass and fail without it) |
| 7 | Enable Flyway | High | Set arch-forge.flyway.enabled: true |
| 8 | Set JPA DDL to validate | High | spring.jpa.hibernate.ddl-auto: validate |
| 9 | Configure HTTPS | High | Terminate SSL at Nginx or load balancer |
| 10 | Set up database backups | High | Automated daily backups |
| 11 | Check OTLP sampling | Medium | SAMPLING_PROBABILITY defaults to 10% in prod/staging |
| 12 | Review log levels | Medium | Avoid DEBUG in production |
| 13 | Set memory limits | Medium | JVM flags or Docker resource limits |
| 14 | Set CORS_ALLOWED_ORIGINS | Critical | Explicit origins, no * — every profile except dev/test refuses to start otherwise |
| 15 | Set ALERT_WEBHOOK_URL | High | Where Alertmanager delivers alerts (see Observability) |
| 16 | Set WEB_PUBLIC_URL | High | Public URL of the C-end site (links in mails and notifications) |
docker-compose.prod.yml refuses to start until DB_PASSWORD, REDIS_PASSWORD, ARCH_FORGE_RSA_PRIVATE_KEY, CORS_ALLOWED_ORIGINS and WEB_PUBLIC_URL are set (scripts/prod/.env.example lists them). A contract test keeps the compose files in step with every variable the prod/staging profiles require.
Generate Secrets
Production secrets
Auth is Sa-Token (UUID tokens in Redis), not a signed JWT. Production must inject secrets via environment variables; missing ARCH_FORGE_RSA_PRIVATE_KEY or DB_PASSWORD fails startup / compose. Do not rely on YAML defaults.
# Example: generate an RSA transport key and a Redis/DB password
openssl rand -base64 32
./archforge init --write --profile prodCopy values into .env / your secret manager. Do not commit application-prod.yaml secrets.
RSA Key Pair
Generate a new RSA private key:
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
openssl pkcs8 -topk8 -inform PEM -outform DER -in private.pem -out private.der -nocrypt
base64 private.derSet the Base64-encoded private key in arch-forge.rsa-private-key (ARCH_FORGE_RSA_PRIVATE_KEY). The server derives the public key and hands it to the admin login page through GET /admin/auth/getConfig; the page encrypts the password with RSA-OAEP / SHA-256. That needs a secure context — serve prod over HTTPS (see Authentication).
Production Configuration
Create archforge-server-admin/src/main/resources/application-prod.yaml from the example:
cp application-prod.yaml.example application-prod.yamlKey Settings
server:
port: 8080
spring:
jpa:
hibernate:
ddl-auto: validate # Flyway manages DDL
datasource:
dynamic:
primary: user_master
datasource:
user_master:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://master:5432/archforge
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
user_slave:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://slave:5432/archforge
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
data:
redis:
host: ${REDIS_HOST:redis}
port: 6379
password: ${REDIS_PASSWORD}
sa-token:
timeout: 86400 # 24 hours for production
arch-forge:
captcha:
enabled: true
flyway:
enabled: true
monitor:
enabled: true
embedded:
redis: false # Use real Redis
postgresql: false # Use real PostgreSQL
management:
server:
port: ${MANAGEMENT_SERVER_PORT:8089} # actuator off the business port (web: 8091)
endpoints:
web:
exposure:
include: health,info,prometheus
tracing:
sampling:
probability: 0.1 # 10% sampling in productionDatabase Setup
Install PostgreSQL 17
Option A: Docker (recommended for most deployments):
docker run -d --name archforge-pg \
--restart unless-stopped \
-e POSTGRES_USER=archforge \
-e POSTGRES_PASSWORD=${DB_PASSWORD} \
-e POSTGRES_INITDB_ARGS="--encoding=UTF8 --locale=en_US.UTF-8" \
-v postgres-data:/var/lib/postgresql/data \
-p 5432:5432 postgres:17-alpineOption B: Native install (Ubuntu):
sudo apt install -y postgresql-17
sudo -u postgres createuser --createdb archforge
sudo -u postgres psql -c "ALTER USER archforge PASSWORD '<your-password>';"Create Databases
psql -h <master-host> -U archforge -c \
"CREATE DATABASE archforge_user ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8';"Master/Slave Replication
For read/write split with dynamic-datasource, set up PostgreSQL streaming replication between master and slave instances. ArchForge routes queries automatically via the @DS annotation and datasource groups configured in application-prod.yaml.
Refer to the PostgreSQL Replication Documentation for detailed setup instructions.
First Deployment
On first startup, Flyway creates all tables and seeds initial data automatically:
# Start application — Flyway runs V1 (schema), V2 (data), V3 (menus)
SPRING_PROFILES_ACTIVE=prod java -jar archforge-server-admin.jarSubsequent Deployments
Flyway automatically detects and runs new migration scripts. Always:
- Backup the database before deploying
- Review migration scripts via code review
- Test migrations in the test environment first
See Database Migration for the full guide.
HTTPS Setup
Option A: Nginx SSL Termination
server {
listen 443 ssl http2;
server_name admin.yourdomain.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://archforge:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
}
server {
listen 80;
server_name admin.yourdomain.com;
return 301 https://$server_name$request_uri;
}Option B: Cloud Load Balancer
If using AWS ALB, GCP Load Balancer, or similar, terminate SSL at the load balancer and proxy HTTP to Nginx on port 80.
Backup Strategy
Database Backup
# Daily automated backup
pg_dump -h <master-host> -U archforge archforge_user | gzip > backup_$(date +%Y%m%d).sql.gzSchedule via cron for daily backups and retain 30 days:
# /etc/cron.d/archforge-backup
0 2 * * * archforge pg_dump -h localhost -U archforge archforge_user | gzip > /backups/archforge_$(date +\%Y\%m\%d).sql.gz && find /backups -name "archforge_*.sql.gz" -mtime +30 -deleteApplication Backup
- Docker volumes for PostgreSQL data:
postgres-data - Application logs:
docker/logs/ - Configuration:
application-prod.yaml(keep in a secure vault, not git)
Monitoring
Health Checks and Probes
curl http://localhost:8080/livez # liveness: the process only
curl http://localhost:8080/readyz # readiness: + database + RedisIn prod (and staging) the actuator itself listens on the management port — admin 8089, web 8091, override with MANAGEMENT_SERVER_PORT in the service's environment (the compose files do not set it). Prometheus scrapes 8089/8091 — change docker/observability/prometheus/prometheus.yml together with it. /actuator/health on the business port returns 404 there, so point orchestrator probes at /livez and /readyz. Never publish the management port.
Prometheus Metrics
Scrape /actuator/prometheus on the management port from inside the network: backend:8089 and backend-web:8091 in docker/docker-compose.prod.yml, which joins both services to the archforge-observability network. The bundled Prometheus config discovers them by DNS — see Observability.