Load .env.<APP_ENV>, validate config at boot, keep secrets out of the image
`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>
This commit is contained in:
103
docs/ENVIRONMENT.md
Normal file
103
docs/ENVIRONMENT.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# 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
|
||||
|
||||
```sh
|
||||
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` (or `JWT_SECRET_KEY` as 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:
|
||||
|
||||
1. Rotate the database, catalogue, Spaces, MQTT and Redis credentials and the
|
||||
POS signing secret, and update them in the platform.
|
||||
2. Take the files back out of the index and restore the ignore rules:
|
||||
```sh
|
||||
git rm --cached .env .env.local .env.production
|
||||
printf '.env\n.env.*\n!.env.example\n' >> .gitignore
|
||||
```
|
||||
The files stay on disk; they just stop being committed.
|
||||
Reference in New Issue
Block a user