Twelve-Factor App
Proxima Console follows the Twelve-Factor App methodology. This page maps each factor to its implementation in the project.
I. Codebase
One codebase tracked in revision control, many deploys.
Proxima Console uses a single GitLab repository organized as a monorepo with three components:
agent/— Go agent deployed on managed serversbackend/— Go API server and workersfrontend/— React single-page application
All environments (development, staging, production) are deployed from the same codebase. Each deploy corresponds to a specific Git commit. The Kubernetes desired state lives in a separate argocd-infra repository, which references container images tagged with the application repo's commit SHA (registry.prxm.uz/proxima/console/<svc>:<git-sha>) — so a production deploy still maps one-to-one to an application Git commit.
II. Dependencies
Explicitly declare and isolate dependencies.
- Go: Dependencies are declared in
go.modand locked ingo.sum. The Go module system provides hermetic builds with no reliance on system-wide packages. - Frontend: Dependencies are declared in
package.jsonand locked inpackage-lock.json. Thenpm cicommand ensures reproducible installs.
No implicit system dependencies are assumed. All required tools are documented in the prerequisites.
III. Config
Store config in the environment.
Deployment-specific configuration is provided via environment variables. This includes database connection strings, NATS URLs, API keys, and port bindings. Secrets are never committed to the repository.
In production, secret values are stored in Vault (vault.prxm.uz) and injected into pods by the External-Secrets Operator (ESO) — no secrets appear in the argocd-infra manifests. Locally, the same variables come from the developer's environment / docker-compose.
Application defaults (non-secret, non-environment-specific values) are stored in YAML configuration files that are loaded first, then overridden by environment variables.
IV. Backing Services
Treat backing services as attached resources.
All external services are accessed via URLs provided as environment variables:
- PostgreSQL: Connected via
PROXIMA_DB_URL(production: the external managedpsql01at10.10.3.11:30034) - NATS: Connected via
PROXIMA_NATS_URL(production:nats://nats.prxm.uz:4222) - Metrics (VictoriaMetrics): Reached through the in-cluster
vmauth-proxy(vmauth-proxy.monitoring.svc.cluster.local:8427) - Cache (Valkey): An in-cluster Redis-compatible service
- Tempo/OTLP: Connected via
PROXIMA_OTEL_ENDPOINT
Swapping a local PostgreSQL instance for a managed cloud database requires only changing the connection URL. No code changes are needed. The same indirection is what lets the identical container image run against local docker-compose backing services or the production cluster's attached resources.
V. Build, Release, Run
Strictly separate build and run stages.
- Build: GitLab CI (
docker:*jobs) compiles Go binaries, builds the frontend static bundle, and pushes Docker images tagged with the Git commit SHA toregistry.prxm.uz/proxima/console/<svc>:<git-sha>. - Release: The
deploy:k8s-gitopsjob (a manual gate onmain) clones theargocd-infrarepo over an SSH deploy key, bumps every console image tag underclusters/production/console-system/to the new SHA, and commits. That commit is the immutable release — image SHA pinned to manifests, with secrets resolved at runtime by ESO. - Run: ArgoCD reconciles the
argocd-infracommit into the cluster. The backend rolls out via Argo Rollouts blue-green (a preview ReplicaSet must pass readiness before traffic is promoted; failures auto-rollback). Each running pod uses one specific, immutable release.
Releases are never modified after creation. A new build is required for any code change. The retired single-box flow (deploy:dev / migrate:dev over SSH, docker-compose down/up) is no longer used for production.
VI. Processes
Execute the app as one or more stateless processes.
The backend process is stateless. All persistent state is stored in PostgreSQL; transient/shared state (sessions, rate-limit and lockout counters, access-resolution cache) lives in Valkey; all message state is managed by NATS JetStream. No in-memory state survives a process restart — which is what makes the backend safe to run as multiple replicas and to cut over during a blue-green rollout.
The frontend is a static file bundle served by an nginx-based pod and contains no server-side state.
Agents maintain a local configuration file but treat the NATS connection as the sole channel for data delivery.
VII. Port Binding
Export services via port binding.
The backend is a self-contained server that binds to a configurable port (default 8080 for REST, with the gRPC terminal service on :9090). It does not depend on an external application server or servlet container. The HTTP port is configurable via the PROXIMA_SERVER_PORT environment variable. In the cluster, the istio Gateway routes external traffic to these container ports via HTTPRoutes.
VIII. Concurrency
Scale out via the process model.
Horizontal scaling is achieved by running multiple backend replicas in the cluster, fronted by the istio Gateway. Each replica is stateless and connects to the same PostgreSQL database and NATS cluster. NATS JetStream consumer groups ensure that each message is processed by exactly one worker instance.
IX. Disposability
Maximize robustness with fast startup and graceful shutdown.
- Fast startup: The backend starts and is ready to serve requests within seconds. No long initialization sequences or cache warming. A fast, accurate readiness probe is what lets Argo Rollouts safely gate the blue-green promotion of a new version.
- Graceful shutdown: When Kubernetes sends
SIGTERM(during a rollout, rescheduling, or node drain), the backend stops accepting new HTTP requests, waits for in-flight requests to complete, drains NATS subscriptions, and closes database connections cleanly.
Agents also handle SIGTERM gracefully, completing any in-progress data collection before exiting.
Because pods are disposable and can be rescheduled at any time, no node-local state is assumed — all durable state lives in the attached backing services (PostgreSQL, NATS, VictoriaMetrics, Valkey).
X. Dev/Prod Parity
Keep development, staging, and production as similar as possible.
Production runs on Kubernetes (the proxima-production cluster, deployed via GitOps), while local development uses the docker-compose.yml file in the repository — make docker-up spins up the same kinds of backing services used in production:
- PostgreSQL
- VictoriaMetrics Cluster (vmstorage, vminsert, vmselect)
- NATS 2.10+ with JetStream enabled
- VictoriaLogs for log storage
- Tempo for trace storage (visualized via Grafana)
- Vault for secrets
Developers run the same database engine, the same message broker, and the same observability backends locally. There are no "lightweight" substitutes (e.g., SQLite instead of PostgreSQL).
Because the orchestration differs (local docker-compose vs. production Kubernetes), parity is maintained at the container-image level: the exact same image (registry.prxm.uz/proxima/console/<svc>:<git-sha>) that runs in the cluster is the one built from the codebase, and each service reads its configuration from environment variables regardless of orchestrator. So while the topology is not identical, the unit of deployment and the application behavior are. docker-compose is for local development only — it is no longer how production is deployed.
XI. Logs
Treat logs as event streams.
All components write structured JSON logs to stdout only. No log files are written to disk. No log routing is configured within the application.
In production, the Kubernetes runtime captures each pod's stdout and a log collector ships it to VictoriaLogs. Locally, docker-compose plays the same role. The application itself does no log routing.
Log entries include structured fields: request_id, component, level, timestamp, and contextual data relevant to the operation.
XII. Admin Processes
Run admin/management tasks as one-off processes.
Administrative tasks are executed as one-off processes, not built into the running application:
- Database migrations: Run by
golang-migrate(migrate up). In production they execute as an ArgoCD PreSync-hook Job (console-migrate) that runs before each backend sync — never from CI, which cannot reach the cluster-internal database. Locally, the same migrations run viamake migrate-up/make migrate-down. - Data seeding: Development seed data is loaded via
make seed, which runs SQL scripts against the database - Health checks: The
/healthand/readyendpoints provide runtime diagnostics and back the Kubernetes liveness/readiness probes
The migration Job runs from the same image (and therefore the same codebase and configuration) as the backend it precedes, ensuring consistency.