Production Deployment Guide
On this page
Overview
Canopy is a containerized microservices system. In development, it runs via Docker Compose (cargo xtask dev start). In production, it deploys to any container orchestration platform (Kubernetes, ECS, Docker Swarm) or can run as standalone Docker containers behind a load balancer.
This guide covers the production deployment architecture. For local development, see Local Development.
Prerequisites
| Component | Version | Notes |
|---|---|---|
Container runtime |
Docker 24+ or Kubernetes 1.28+ |
All services ship as Alpine-based containers |
PostgreSQL |
16+ |
6 isolated databases per ADR-001 (or shared instance with separate DBs) |
RabbitMQ |
3.13+ |
Durable queues, management plugin recommended |
Keycloak |
24+ |
OIDC identity provider with RS256 JWT |
Redis |
7+ |
Session LRU cache for BFF services (ADR-009) |
S3-compatible storage |
Any |
MinIO, AWS S3, Garage — for PDF notice storage |
TLS certificates |
— |
For all public-facing endpoints |
Infrastructure Requirements
Service Tier (19 application services for SNAP-only profile)
Each service is stateless — no local disk, no in-memory sessions, no sticky routing. Scale horizontally by adding replicas.
| Service Group | vCPU | Memory | Instances | Notes |
|---|---|---|---|---|
Program services (snap, tanf, medicaid, caps, wic) |
0.5 |
256 MB |
2+ |
One active per program; scale for throughput |
Infrastructure services (rules, persons, applications, eligibility, enrollment, renewals, appeals, reporting, security) |
0.5 |
256 MB |
2+ |
Scale persons/eligibility first under load |
canopy-notices |
1 |
512 MB |
2+ |
Handles Typst PDF rendering (CPU-bound) via canopy-typst (ADR-010); size CPU for notice render throughput |
BFF services (canopy-web, canopy-portal) |
0.5 |
512 MB |
2+ |
Server-rendered web layer; no PDF rendering (delegated to canopy-notices) |
Data Tier
| Component | vCPU | Memory | Disk | Notes |
|---|---|---|---|---|
PostgreSQL (per program DB) |
1 |
1 GB |
50 GB SSD |
Enable WAL archiving for PITR. One instance per program per ADR-001, or shared instance with separate databases. |
RabbitMQ |
1 |
1 GB |
20 GB |
Durable queues. 3-node cluster for HA. |
Keycloak |
1 |
1 GB |
10 GB |
External PostgreSQL backend recommended for HA |
Redis |
0.5 |
128 MB |
— |
LRU eviction (maxmemory 128mb), AOF persistence. Session cache only — PostgreSQL is authoritative. |
S3 / MinIO |
0.5 |
512 MB |
100 GB+ |
Versioning enabled for notice PDFs. Grows with notice volume. |
Deployment Profiles (ADR-005)
Canopy supports selective program deployment via Docker Compose profiles or equivalent Kubernetes label selectors:
| Profile | Services | Use Case |
|---|---|---|
|
19 |
SNAP UAT (September 2026 target) |
|
16 |
TANF-only deployment |
|
17 |
Medicaid + CHIP |
|
14 |
CAPS/CCDF only |
|
14 |
WIC only |
|
29 |
All programs (default) |
Services not included in a profile gracefully degrade — the eligibility orchestrator returns pending_verification for unavailable programs instead of failing.
Environment Configuration
All configuration is via environment variables following the CANOPY_{SERVICE}__{KEY} convention.
Required Variables (all services)
| Variable | Description | Example |
|---|---|---|
|
Service listen port |
|
|
PostgreSQL connection (use |
|
|
RabbitMQ AMQP URL (use |
|
|
Public JWT issuer URL |
|
|
Internal JWKS fetch URL (may differ from issuer in containerized deployments) |
|
|
Jurisdiction identifier for rulesets |
|
|
Runtime environment ( |
|
Secrets
| Variable | Description |
|---|---|
|
AES-256-GCM key for SSN encryption. Generate: |
|
ECDSA P-256 private key PEM for determination signing. Generate: |
|
Current public verification key PEM. Used by canopy-eligibility to verify determination signatures; retired keys are lazy-loaded from canopy-security’s |
|
Service-to-service API key for internal endpoints (IEVS, SAVE). Same value across all services. |
Store secrets in a vault (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault). Never commit secrets to the repository.
Optional Tuning
| Variable | Default | Description |
|---|---|---|
|
10 |
PostgreSQL connection pool size per service |
|
600 |
Idle connection timeout |
|
6000 |
Rate limit per IP per minute (0 = disabled) |
|
2097152 |
Max request body size in bytes (2 MiB) |
|
28800 / 1800 |
Session TTL (canopy-web: 8h, canopy-portal: 30min) |
|
true |
Set |
Database Setup
-
Create databases (one per program service per ADR-001):
CREATE DATABASE canopy_snap; CREATE DATABASE canopy_tanf; CREATE DATABASE canopy_medicaid; CREATE DATABASE canopy_caps; CREATE DATABASE canopy_wic; CREATE DATABASE canopy_rules; CREATE DATABASE canopy_persons; CREATE DATABASE canopy_applications; CREATE DATABASE canopy_eligibility; CREATE DATABASE canopy_enrollment; CREATE DATABASE canopy_renewals; CREATE DATABASE canopy_notices; CREATE DATABASE canopy_appeals; CREATE DATABASE canopy_reporting; CREATE DATABASE canopy_security; CREATE DATABASE canopy_web_sessions;
-
Migrations run automatically on service startup via
sqlx::migrate!(). No manual migration step required — except canopy-reporting (below). -
Enable SSL: add
?sslmode=requireto all DATABASE_URL values in production.
canopy-reporting role provisioning (#1456, ADR-004 A8b)
canopy-reporting runs under a least-privilege role split and will refuse to boot as a broad role outside development. Before its first start:
-
Provision the login carrier:
ALTER ROLE canopy_reporting_app LOGINand set its password from the secret manager (the role itself is created NOLOGIN/passwordless by migration20261111000000; the credential is never in source). -
Point
CANOPY_REPORTINGDATABASE_URLatcanopy_reporting_appandCANOPY_REPORTINGMIGRATION_DATABASE_URLat the elevated migrator identity (see Configuration Reference).
Full cutover sequencing, rollback, and the standing per-migration ownership-transfer convention: Reporting Credential-Cutover Runbook.
Monitoring
Health Checks
Every service exposes:
-
GET /healthz— returns JSON:{"status": "ok", "checks": {"database": "ok", "rabbitmq": "ok"}} -
GET /metrics— Prometheus text format (requiresotelcompile feature +OTEL_EXPORTER_OTLP_ENDPOINT)
Configure your load balancer to health-check /healthz on each service.
Recommended Alert Rules
| Metric | Threshold | Action |
|---|---|---|
Service health check failure |
> 30 seconds |
Restart container, investigate logs |
Database connection pool saturation |
active = max_connections |
Increase |
RabbitMQ queue depth |
> 10,000 messages |
Scale consumers, check for slow subscribers |
HTTP error rate |
> 1% of requests |
Check service logs, recent deployments |
P99 response latency |
> 5 seconds |
Profile slow queries, check resource limits |
Disk usage (PostgreSQL) |
> 80% |
Expand volume, review retention policies |
Log Aggregation
All services emit structured JSON logs to stdout via tracing_subscriber. Configure your container orchestrator to collect stdout and forward to your log aggregation platform (Loki, ELK, CloudWatch, Splunk).
Log level controlled by CANOPY_{SVC}__LOG_LEVEL (default: info). Set to debug for troubleshooting.
Backup & Disaster Recovery
PostgreSQL
-
Enable WAL archiving and continuous archiving (PITR)
-
Daily
pg_dumpto S3 for each program database -
Test restore procedures quarterly
-
RTO target: 1 hour (restore from PITR)
-
RPO target: 5 minutes (WAL segment interval)
RabbitMQ
-
Durable queues with disk persistence (default in Canopy)
-
3-node cluster with quorum queues for HA
-
Messages survive broker restart; unacked messages redelivered
TLS Configuration
-
All public endpoints: TLS 1.2+ enforced. rustls handles TLS in-process (no OpenSSL).
-
Database connections: Add
?sslmode=requireto DATABASE_URL for production. -
RabbitMQ: Use
amqps://protocol with TLS-enabled RabbitMQ. -
Internal service communication: HTTP by default within the container network. For defense-in-depth, enable mTLS via service mesh (Istio, Linkerd) or TLS termination at sidecar.
-
Certificate management: Automate renewal via Let’s Encrypt (ACME), AWS ACM, or your PKI. Set calendar reminders for any manually managed certificates.
High Availability
Canopy services are stateless — scale horizontally by running multiple replicas behind a load balancer.
-
Services: 2+ replicas per service. Health-check-aware load balancing.
-
PostgreSQL: Primary + streaming replica with automatic failover (Patroni, RDS Multi-AZ, Cloud SQL HA).
-
RabbitMQ: 3-node cluster with quorum queues. Mirror all durable queues.
-
Redis: Sentinel or cluster mode for session cache failover.
-
S3: Inherently highly available (cloud-managed) or MinIO with erasure coding.
-
Keycloak: Run 2+ replicas with shared PostgreSQL backend and Infinispan cache clustering.
Environment Promotion
Artifact promotion follows the ADR-040 build-once contract (there is no separate staging environment today):
-
Build once: the pipeline builds both deployable images (the shared service image + canopy-portal) exactly once, under pipeline-internal immutable staging refs (
$CI_REGISTRY_IMAGE/build:$CI_COMMIT_SHA+/build/portal:$CI_COMMIT_SHA). -
Gate against the same digests: integration tests run the devstack from those exact staging refs (
docker-compose.prebuilt.yml, #1073) — test-what-you-promote. -
Promote by retag, never rebuild:
docker-promoteretags the gated digests into the production repositories — an immutable:<short-sha>per promoted commit, plus mutable:<tag>/:latestconveniences (and the/portaltwins). Aresource_groupserializes concurrent promotions.
Deployment consumes the production refs via the prebuilt compose override — see the Scaling & Deployment runbook. The ci-config-lint xtask gate asserts these invariants against .gitlab-ci.yml on every push.