Skip to Content
OperationsLocal Development & Deployment

Local Development & Deployment

Deployment targets

TargetHow
Local developmentDocker Compose, driven through ./deploy.sh at the repo root
StagingKubernetes on a cloud provider — manifests not yet written
ProductionKubernetes with HA — not yet built

Quick start: ./deploy.sh

The compose file lives at deployment/docker-compose.yml; all Gradle modules are under :backends:*. Everything runs through ./deploy.sh at the repo root — do not call docker compose directly.

# First time only: map *.localhost hostnames to 127.0.0.1 ./deploy.sh setup # Start everything (postgres, redis, all backends, all frontends, nginx, docs site) ./deploy.sh start # Start only backends or only frontends ./deploy.sh start backends ./deploy.sh start frontends # Rebuild + restart everything, or one app ./deploy.sh restart core-engine # Show running containers + all service URLs ./deploy.sh status # Stream logs (all services, or one) ./deploy.sh logs ./deploy.sh logs api-gateway # Stop everything, or one target ./deploy.sh stop # Full reset: containers, images, volumes, build cache + outputs ./deploy.sh purge

./deploy.sh status prints the live URL map. In local Docker Compose mode, services are reached through nginx virtual hosts, not raw container ports:

http://authority.localhost Attest Authority (Issuer Portal) http://verify.localhost Verifier Portal http://credentials.localhost Holder Wallet http://console.localhost Admin Console http://custodian.localhost Custodian Portal http://docs.localhost Documentation site http://localhost:9200 API Gateway http://localhost:9280 Core Engine (+ /swagger-ui.html) http://localhost:9281 Credential Issuance http://localhost:9282 Verification Engine http://localhost:9283 Issuer Registry http://localhost:9284 Admin Console http://localhost:9285 Program Catalog http://localhost:9286 Learner Records http://localhost:9287 Custodian Registry

These 92xx host ports are Docker Compose’s external mapping only — inside the compose network (and when running a service directly via bootRun, outside Docker), every service listens on its CLAUDE.md-documented port (API Gateway 8000, Core Engine 8080, Credential Issuance 8081, etc.). Don’t mix the two: localhost:8080 works for a directly-bootRun Core Engine, but under ./deploy.sh you’d reach the same service at localhost:9280.

Individual service startup (without Docker)

# Data tier only via Docker ./deploy.sh start backends # or target only the data tier — see docker-compose.yml profiles # Then run services individually ./gradlew :backends:api-gateway:bootRun ./gradlew :backends:core-engine:bootRun ./gradlew :backends:credential-issuance:bootRun ./gradlew :backends:verification-engine:bootRun ./gradlew :backends:issuer-registry:bootRun ./gradlew :backends:admin-console:bootRun ./gradlew :backends:program-catalog:bootRun ./gradlew :backends:learner-records:bootRun ./gradlew :backends:custodian-registry:bootRun

Frontend dev servers

Each frontend is React 19 + TypeScript + Vite with hot reload, run independently (npm install && npm run dev inside its own directory) rather than through ./deploy.sh when actively developing on it:

FrontendPortNotes
frontends/issuer-portal5173
frontends/verifier-portal5174
frontends/admin-console5175
frontends/credential-wallet5176Holder-facing PWA
frontends/landing5177Public marketing site — no API Gateway proxy
frontends/custodian-portal5178Bulk document intake/review/attestation — see Custodian Onboarding
frontends/docs5179This documentation site — reads documentation/ via a symlink, no API Gateway proxy

All frontends except docs proxy API calls to API Gateway (http://localhost:8000) via each app’s own vite.config.ts.

Environment configuration

application.yml per service, overridable via environment variables. In deployment/docker-compose.yml, DB_URL points through pgbouncer (port 6432) while FLYWAY_DB_URL connects straight to Postgres (port 5432) for migrations:

DB_URL=jdbc:postgresql://pgbouncer:6432/attest_pro?prepareThreshold=0 FLYWAY_DB_URL=jdbc:postgresql://postgres:5432/attest_pro SPRING_DATA_REDIS_HOST=redis SPRING_DATA_REDIS_PORT=6379 GRPC_SERVER_PORT=50051

Container images

# Build all images ./deploy.sh build # Build one service's image (build context is the repo root) docker build -t attestpro/credential-issuance:latest \ -f backends/credential-issuance/Dockerfile .

Kubernetes — not yet built

No Kubernetes manifests exist in this repository — local/staging today is Docker Compose only via ./deploy.sh. The snippets below are illustrative of the target shape, not runnable today.

kubectl create namespace attest-pro kubectl apply -f k8s/config.yaml -n attest-pro kubectl apply -f k8s/ -n attest-pro kubectl get pods -n attest-pro
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: credential-issuance-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: credential-issuance minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: { type: Utilization, averageUtilization: 70 }

Monitoring & observability

Each service exposes /actuator/prometheus:

curl http://localhost:8080/actuator/prometheus curl http://localhost:8080/actuator/health/liveness curl http://localhost:8080/actuator/health/readiness

Enforced Prometheus/Grafana alerting is not yet in place — an explicit launch-blocking item in CLAUDE.md’s Roadmap Context, not a solved problem.

Database migrations

Flyway runs automatically on service startup. Each service owns its own migration directory: V1__schema.sql (+ V2__seed_data.sql where seed data exists) — see Database Schema Ownership. Project convention is in-place edits to V1/V2 only, never V3+ files.

Editing V1__schema.sql in place changes its checksum, so any environment with migrations already applied needs its database reset (docker-compose down -v or equivalent, then re-up) before Flyway will apply the edited V1 cleanly — expected whenever V1 changes.

./gradlew flywayInfo ./gradlew flywayRepair

Backup & recovery

pg_dump attest_pro > backup.sql psql attest_pro < backup.sql

A verified backup/restore drill has not been performed — an explicit launch-blocking item. Redis is configured for RDB snapshots and AOF for durability.

Security

  • All external endpoints use HTTPS/TLS 1.3 in the target production design; local dev via ./deploy.sh runs plain HTTP through nginx virtual hosts.
  • Internal gRPC mTLS is implemented, not just planned — every gRPC connection between Core Engine and its downstream services requires mutual TLS with no plaintext fallback. See Internal gRPC mTLS.

Scaling strategies

ServiceNotes
Verification EngineStateless — scales to any number of replicas
API GatewayScales horizontally
Core EngineRun 2+ for HA
Credential IssuanceStateless, scales horizontally
Issuer Registry / Custodian RegistryRedis cache (Issuer Registry only), scale with cache strategy
PostgreSQLPrimary bottleneck — consider read replication; local dev already runs pgbouncer in front

Performance tuning

JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 --enable-preview"
  • Connection pooling: HikariCP + pgbouncer in front of Postgres.
  • Query caching: Redis for frequently accessed issuers/DIDs/catalog entries.
  • Indexes on issuer_id, credential_id, status, created_at.
  • Redis eviction: LRU, 1-hour TTL for most data; 10-minute TTL for verification results; 24-hour TTL for DID document cache.

Troubleshooting

Prefer ./deploy.sh logs <app> and ./gradlew :backends:<service>:... throughout — there is no raw docker-compose invocation in this project’s actual workflow.

# Service won't start ./deploy.sh logs api-gateway lsof -i :9200 # see the 92xx host-port map above ./deploy.sh restart gateway # High latency grpcurl -plaintext localhost:50051 list redis-cli INFO stats # Out of memory (directly-bootRun service) JAVA_OPTS="-Xms4g -Xmx8g" ./gradlew :backends:api-gateway:bootRun

Rollback

# Docker Compose ./deploy.sh stop docker tag attestpro/api-gateway:latest attestpro/api-gateway:rollback ./deploy.sh start # redeploy with previous image tag # Kubernetes (once manifests exist) kubectl rollout history deployment/api-gateway -n attest-pro kubectl rollout undo deployment/api-gateway -n attest-pro kubectl rollout undo deployment/api-gateway -n attest-pro --to-revision=5

Production checklist

See CLAUDE.md’s “Roadmap Context” for the authoritative, current launch-blocking list. Still open as of this writing: staging environment with a promotion gate, enforced Prometheus/Grafana alerting, end-to-end tracing, a third-party security audit, legal/compliance sign-off, a verified backup/restore drill, load testing, an incident-response runbook, and holder claim-ownership integrity (see Holder Claim Integrity).

  • All services passing tests
  • Security scan (OWASP, CVE checks)
  • Load testing completed
  • Database backups configured and a restore drill verified
  • Monitoring/alerting enforced (not just instrumented)
  • Incident response plan in place
  • TLS certificates installed and valid (production)
  • Rate limiting configured — see API & gRPC Reference
  • Database connection pooling optimized (pgbouncer already in place locally)
  • Redis persistence enabled
  • Graceful shutdown configured (10–30 second timeout)

See also CI/CD Pipeline for the automated build/deploy path.