`main.go` only ever loaded `.env`; the `APP_ENV` switch described in `.env.local` / `.env.production` did not exist, and a missing variable surfaced one restart at a time as a log.Fatalf inside db.Connect. config.Load now picks `.env.<APP_ENV>` (default local) then `.env`, with real environment winning, reads every setting into one typed Config and reports everything missing in one message. Production insists on a POS signing secret; local warns when DB_HOST is not a local address. db, redis and the image store take the Config instead of reading env themselves. Also: - livehub read MQTT_USERNAME while everything else uses MQTT_USER, so the console stream connected to the broker unauthenticated. Both accepted. - .dockerignore: `COPY . .` was baking .env.production into the image. Dockerfile sets APP_ENV=production. - Drop utils/config.go (dead viper loader) and create_table.go (unused, hardcoded production DSN); go mod tidy removes viper. - .env.example lists every variable the code reads; docs/ENVIRONMENT.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.4 KiB
Environment & configuration
Everything the API reads from its environment goes through config/config.go.
It loads the right .env file, reads every setting into one Config, and
refuses to start — listing everything that is wrong in one message — before
a single connection is attempted.
Which file loads
APP_ENV names the environment. It defaults to local.
| Command | Files loaded, in order |
|---|---|
go run . |
.env.local, then .env |
APP_ENV=production go run . |
.env.production, then .env |
APP_ENV=staging go run . |
.env.staging, then .env |
Precedence, highest first:
real environment > .env.<APP_ENV> > .env
A variable that is already set is never overwritten by a file, and no file has
to exist. APP_ENV itself is read from the real environment before any file
is opened — a file cannot decide which file gets loaded.
| File | Role |
|---|---|
.env.example |
The complete list of settings, with comments. Start here. |
.env.local |
The docker-compose stack. Every host is localhost. |
.env.production |
The live hosts. Loaded only when asked for. |
.env |
Shared base: fallbacks for whatever the file above left out. Keep it local. |
Running locally
docker compose -f docker-compose.local.yml up -d # postgres :5433, pgvector :5434, redis :6379
go run . # APP_ENV unset → .env.local
An empty database is not enough — main.go runs migrations that assume the
live schema. See init/README.md for loading a schema dump first.
Startup prints what it loaded and where it is pointed:
config: loaded .env.local
config: loaded .env
config: APP_ENV=local, listening on :1122, database nearle@localhost:5433/nearledb
If DB_HOST is not a local address under APP_ENV=local, it says so:
⚠️ APP_ENV=local but DB_HOST=66.116.x.x is not a local address — every write goes to that database for real
That is a warning, not a stop. There is no "local mode" that protects
production: go run . against the live host creates real tenants and real
logins, and runs schema migrations on boot.
Running in production
The container gets no .env file at all — .dockerignore keeps every
.env* out of the image — and the Dockerfile sets APP_ENV=production.
Every value comes from the platform's environment settings (Dokploy today;
ConfigMaps/Secrets under Kubernetes).
Under APP_ENV=production startup additionally insists on:
POS_TOKEN_SECRET(orJWT_SECRET_KEYas a fallback), at least 16 characters.
A variable added to .env.production and not to the platform is a variable
that is unset in production. Missing required ones stop the boot with the
full list; missing optional ones (MQTT_URL, REDIS_HOST, USE_S3,
CATALOGUE_DB_HOST) silently disable that subsystem — check the startup log
lines when something is "not working".
What is required
| Always | Only when enabled |
|---|---|
DB_HOST DB_USER DB_PASSWORD DB_NAME |
CATALOGUE_DB_HOST set → CATALOGUE_DB_USER/PASSWORD/NAME |
(production) POS_TOKEN_SECRET |
USE_S3=true → S3_ENDPOINT/BUCKET/ACCESS_KEY/SECRET_KEY/REGION |
A half-configured subsystem is an error, not a warning: a warning reads as "fine" in a log and turns into "why are there no images" a week later.
The committed credentials
The three .env files, including .env.production, are currently tracked in
git (commit be47435), and an earlier .env was committed before
2026-08-03. Every credential in them has to be treated as public:
- Rotate the database, catalogue, Spaces, MQTT and Redis credentials and the POS signing secret, and update them in the platform.
- Take the files back out of the index and restore the ignore rules:
The files stay on disk; they just stop being committed.
git rm --cached .env .env.local .env.production printf '.env\n.env.*\n!.env.example\n' >> .gitignore