Skip to main content

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 servers
  • backend/ — Go API server and workers
  • frontend/ — 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.mod and locked in go.sum. The Go module system provides hermetic builds with no reliance on system-wide packages.
  • Frontend: Dependencies are declared in package.json and locked in package-lock.json. The npm ci command 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 managed psql01 at 10.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.

  1. Build: GitLab CI (docker:* jobs) compiles Go binaries, builds the frontend static bundle, and pushes Docker images tagged with the Git commit SHA to registry.prxm.uz/proxima/console/<svc>:<git-sha>.
  2. Release: The deploy:k8s-gitops job (a manual gate on main) clones the argocd-infra repo over an SSH deploy key, bumps every console image tag under clusters/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.
  3. Run: ArgoCD reconciles the argocd-infra commit 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 via make 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 /health and /ready endpoints 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.