POSTGRESDSN=postgres://postgres:postgres@localhost:5434/asce?sslmode=disable
Root cause: .env.example shipped compose-network DSNs (postgres:5432, redis:6379, searxng1:8080). Those hostnames only resolve on the compose network, so a bare-host cp .env.example .env && make run panicked at boot: dial tcp: lookup postgres: no such host. The fix splits the two environments — the host gets localhost, the containers keep the network names, with environment: (which wins over env_file:) as the override mechanism.
1. .env.example — host-first DSNs, ports matching the compose published range:
# Host-first: resolvable on the bare host; compose environment: blocks override
# inside containers (environment: wins over env_file: — container path unchanged).
PORT=8080
POSTGRES_DSN=postgres://postgres:postgres@localhost:5434/asce?sslmode=disable
REDIS_ADDR=localhost:6380
SEARXNG_URL=http://localhost:8888
2. docker-compose.yml — force network names via environment: on every config.Load() consumer (asce_api and mcp-server; ports stay host-first so localhost:5434/6380/8888-8890 line up):
services:
postgres:
image: postgres:16
ports: ["5434:5432"] # host-first published port
redis:
image: redis:7
ports: ["6380:6379"]
searxng1:
image: searxng/searxng:latest
ports: ["8888-8890:8080"]
asce_api:
build: .
env_file: [.env] # host-first values, but...
environment: # ...this wins inside the container:
POSTGRES_DSN: postgres://postgres:postgres@postgres:5432/asce?sslmode=disable
REDIS_ADDR: redis:6379
SEARXNG_URL: http://searxng1:8080
depends_on: [postgres, redis, searxng1]
# Worker deviation worth keeping: every service that consumes env_file: .env
# AND config.Load() needs the same override block (mcp-server, asce-worker, ...).
mcp-server:
image: asce-mcp:latest
env_file: [.env]
environment: # same block — otherwise it re-breaks in-container
POSTGRES_DSN: postgres://postgres:postgres@postgres:5432/asce?sslmode=disable
REDIS_ADDR: redis:6379
SEARXNG_URL: http://searxng1:8080
depends_on: [postgres, redis, searxng1]
The mechanics: Compose merges env_file: values first, then environment: overrides them (spec-defined precedence), and at runtime the app's env loader (godotenv-style Load()) never clobbers an already-set variable — so both layers agree. make run / CI on a host resolves localhost, compose resolves the network names. Nothing else in the app changes.
Built a faithful reproduction in `/tmp/asce-repro` (same `config.Load()` + boot-time host-resolution + `listening` log) and ran 8 checks: | # | Check | Result | |---|-------|--------| | 1 | Old `.env` (compose names) on host → panic | ✅ reproduced: `panic: dial tcp: lookup postgres: no such host`, exit 2, no `listening` | | 2 | `.env.example` → `.env`, `PORT=<free> timeout 8 go run ./cmd/asce-api` | ✅ `listening on :56141`, zero panics (exit 124 = timeout reaping healthy server) | | 3 | `docker compose config` — `asce_api` env rendered | ✅ `POSTGRES_DSN=…@postgres:5432`, `REDIS_ADDR=redis:6379`, `SEARXNG_URL=http://searxng1:8080` (override beats `.env`) | | 4 | `docker compose config` — `mcp-server` env rendered | ✅ same network-name overrides present (worker deviation kept) | | 5 | Infra published ports rendered | ✅ `5434→5432`, `6380→6379`, `8888-8890→8080` (host-first matches `.env.example`) | | 6 | Edge: env var beats `.env` file value (partial override) | ✅ env-set `POSTGRES_DSN=localhost` honored while un-overridden `REDIS_ADDR=redis:6379` still loaded — the exact `environment:` > `env_file:` precedence | | 7 | Edge: full 3-DSN env override over buggy `.env` on disk | ✅ `listening` — simulates container env winning at the code level | | 8 | Edge: fresh clone, no `.env`, built-in defaults | ✅ defaults are host-first → `listening on :18083`, no panic (`.env.example`-missing safe) | `docker compose config` runs client-side (no daemon needed) and rendered cleanly; Go 1.26 built and ran the repro. In-container resolution of `postgres/redis/searxng1` is guaranteed by network membership — the override renders (checks 3–4) and the port contract is unchanged (check 5), so the container path is byte-identical to before the fix.
{"model": "deepseek-v4-flash", "problem_class": "go-host-mode-dsn-compose-network-names", "result": "passed", "tests": 8}