Production Guides
Twelve-Factor Config in GoFr
Summary
GoFr's config.Config interface reads from process environment variables and .env files in the configs/ directory, with system env vars taking precedence over file values. In Kubernetes, ship the same binary across environments and inject configuration through envFrom referencing a ConfigMap (non-secret) and a Secret (credentials), keeping secrets out of source control.
When to use
Twelve-factor config matters whenever the same artifact runs in more than one place — local laptop, CI, staging, production. GoFr is designed around this from the start: the framework itself is configured by env vars (HTTP_PORT, DB_DIALECT, LOG_LEVEL, etc.), and app.Config.Get(...) exposes the same surface to your application code.
How GoFr loads config
The default loader is config.NewEnvFile(configFolder, logger) and the precedence is:
- System environment variables — values present in
os.Environ()before the app starts win. configs/.env— base values for every environment.configs/.<APP_ENV>.env— overrides for the named env (e.g.,configs/.staging.envwhenAPP_ENV=staging). Falls back toconfigs/.local.envwhenAPP_ENVis unset.
The loader actually re-applies the captured initial environment after reading the override file, which is what guarantees system env > file. In a Kubernetes pod, every value injected via env: or envFrom: is a system env var and therefore beats anything baked into the configs/ folder of the image.
The Config interface itself is small:
type Config interface {
Get(string) string
GetOrDefault(string, string) string
}
Use it from any handler or service:
threshold := app.Config.GetOrDefault("PAYMENT_RETRY_THRESHOLD", "3")
Local development with .env
Keep a checked-in configs/.env with safe defaults and a gitignored configs/.local.env for personal overrides:
# configs/.env
APP_NAME=orders-api
HTTP_PORT=8000
LOG_LEVEL=DEBUG
DB_DIALECT=postgres
DB_HOST=localhost
DB_PORT=5432
DB_NAME=orders_dev
# configs/.local.env (gitignored)
DB_PASSWORD=local-dev-password
When APP_ENV is unset GoFr loads .env then overlays .local.env. Set APP_ENV=staging and it overlays .staging.env instead.
Kubernetes: ConfigMap + Secret
In production, the configs/ directory inside the image is largely empty (or only holds non-environmental files like a GraphQL schema). Everything environmental comes from Kubernetes:
apiVersion: v1
kind: ConfigMap
metadata:
name: orders-api-config
namespace: prod
data:
APP_NAME: orders-api
APP_ENV: prod
HTTP_PORT: "8000"
METRICS_PORT: "2121"
LOG_LEVEL: INFO
DB_DIALECT: postgres
DB_HOST: postgres-primary.prod.svc.cluster.local
DB_PORT: "5432"
DB_NAME: orders
DB_MAX_OPEN_CONNECTION: "20"
DB_MAX_IDLE_CONNECTION: "5"
TRACE_EXPORTER: otlp
# GoFr's OTLP exporter speaks gRPC (otlptracegrpc). TRACER_URL must be a bare
# host:port — no http:// scheme — and the OTLP gRPC port is 4317 (4318 is OTLP
# HTTP, which GoFr does NOT use).
TRACER_URL: otel-collector.observability.svc.cluster.local:4317
---
apiVersion: v1
kind: Secret
metadata:
name: orders-api-secret
namespace: prod
type: Opaque
stringData:
DB_USER: orders_app
DB_PASSWORD: replace-me
Wire both into the Deployment with envFrom so every key becomes an env var without listing them individually:
spec:
template:
spec:
containers:
- name: api
image: ghcr.io/example/orders-api:1.4.2
envFrom:
- configMapRef:
name: orders-api-config
- secretRef:
name: orders-api-secret
ports:
- name: http
containerPort: 8000
- name: metrics
containerPort: 2121
If both the ConfigMap and Secret define the same key, the later envFrom entry wins — list the Secret last for credentials that must override defaults.
Secret management
Don't commit Secret manifests with real values to Git. Two well-supported options:
- Sealed Secrets — encrypt the Secret manifest with a controller-held key; safe to commit.
- External Secrets Operator — sync from Vault, AWS Secrets Manager, GCP Secret Manager, etc.
GoFr does not need to know which one you use; both materialize a normal Secret that envFrom consumes.
When to use the configs/ folder vs env
Use env vars for anything that varies by environment: hostnames, ports, log levels, feature flags, credentials.
Use the configs/ folder for static assets the binary needs at runtime: a GraphQL schema.graphql, an OpenAPI openapi.json (which GoFr auto-mounts as Swagger UI when present), or a fixed routing table. Bake these into the image — they don't change between staging and prod.
Anti-patterns
- Hardcoded URLs (
"http://payments.internal") — breaks the moment staging needs a different host. - Secrets committed to Git, even in a private repo — they leak via clones, CI artifacts, and IDE history.
- Reading
os.Getenvdirectly in handlers — useapp.Config.Getso tests can substitute a mockConfig. - One ConfigMap that mixes secrets with non-secrets — defeats the point of using a Secret resource for RBAC and audit.