Deploy: fix the two things that broke the Container Manager build

DSM reported "invalid interpolation format for x-backend-env.MASTER_KEY" and
"required variable MASTER_KEY is missing a value". Both were mine.

1. DSM's bundled Compose cannot parse `${VAR:?message}`. Those guards are gone;
   interpolation now sticks to `${VAR}` and `${VAR:-default}`, the forms every
   Compose understands. Bind mounts keep a `${DATA_ROOT:-/DATA_ROOT-is-unset}`
   sentinel so a missing config still cannot scatter directories across the
   root of the NAS filesystem, and the header says to keep it this way.
2. Config was at docker/.env.prod with a `.env` symlink beside it. Container
   Manager did not pick that up. The real file is now `.env` in the project
   folder — what every compose front end reads without being told — and
   docker/.env.prod becomes a symlink to it. `init` and `require_env` migrate
   an existing install in place, keeping its MASTER_KEY.

Also, while testing the migration: `init` only generated secrets when creating
a file from scratch, so a migrated or hand-written .env with an empty
POSTGRES_PASSWORD started Postgres in trust-auth mode. It now tops up empty
secrets — and never touches a MASTER_KEY that already has a value, since
replacing it would make every stored credential undecryptable. doctor flags an
empty password too.

Verified: parses cleanly with no .env at all (the failure DSM hit), migration
preserves an existing key, and `docker compose up -d --wait` from the project
folder brings the stack up healthy and serving.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nux3LCu4zP9nih4BRhPyHw
This commit is contained in:
2026-09-14 19:49:03 +02:00
co-authored by Claude Opus 5
parent 5c444a6fd3
commit f3dcd938b1
3 changed files with 73 additions and 28 deletions
+9 -9
View File
@@ -75,7 +75,7 @@ No git on the NAS? Copy the folder over with File Station or `rsync` instead.
./deploy/webmail.sh init
```
This creates `docker/.env.prod` with a freshly generated `MASTER_KEY` and
This creates `.env` with a freshly generated `MASTER_KEY` and
database password. Then open it and set:
```ini
@@ -90,7 +90,7 @@ WEB_BIND=0.0.0.0 # or one NAS IP, e.g. 192.168.1.185
> It encrypts every stored mail-account password. It exists **only** in this
> file. A perfect database backup without this key gives you your mail but not
> your credentials — you would have to re-enter every account password by hand.
> `docker/.env.prod` is `chmod 600` and git-ignored; treat it as a backup
> `.env` is `chmod 600` and git-ignored; treat it as a backup
> artefact in its own right.
### 2.4 Start
@@ -214,7 +214,7 @@ Containers are disposable. These are not:
|---|---|---|---|
| 1 | **Database** | `$DATA_ROOT/postgres` + nightly dumps in `$DATA_ROOT/backups` | Everything: accounts, threads, flags, calendar, contacts |
| 2 | **Blob store** | `$DATA_ROOT/blobs` | Mail bodies and attachments (re-downloadable from the servers, slowly) |
| 3 | **`MASTER_KEY`** | `docker/.env.prod` + your password manager | Every saved mail-account password, permanently |
| 3 | **`MASTER_KEY`** | `.env` + your password manager | Every saved mail-account password, permanently |
Everything else is derived and needs no backup: Redis holds queues and leases,
Meilisearch's index rebuilds from Postgres (§8), and images rebuild from source.
@@ -240,7 +240,7 @@ A snapshot of a *running* Postgres directory is crash-consistent — it usually
replays fine, but it is not a substitute for the dumps, which are verified and
restorable into a different Postgres version. Keep both.
Also back up `docker/.env.prod` somewhere that is not the NAS.
Also back up `.env` somewhere that is not the NAS.
### Restore drill — do this once, before you need it
@@ -255,7 +255,7 @@ the old database untouched), and starts everything back up.
Full disaster recovery onto a new box:
1. Install Docker, copy `$DATA_ROOT` back from your backup.
2. Copy the repo and `docker/.env.prod` — **with the original `MASTER_KEY`**.
2. Copy the repo and `.env` — **with the original `MASTER_KEY`**.
3. `./deploy/webmail.sh up`
That's it. If the blob store was lost but the database survived, mail bodies
@@ -301,7 +301,7 @@ Building on the NAS is simple but slow and RAM-hungry. Two alternatives:
```bash
# On your PC — push to your Gitea registry:
REGISTRY_PREFIX=192.168.1.185:8418/claude/webmail ./deploy/build-push.sh v1.0.0
# then on the NAS, set REGISTRY_PREFIX + IMAGE_TAG in docker/.env.prod and:
# then on the NAS, set REGISTRY_PREFIX + IMAGE_TAG in .env and:
./deploy/webmail.sh update v1.0.0
# Or with no registry at all — build to a file and carry it over:
@@ -353,7 +353,7 @@ API is started with `--proxy-headers`. Nothing to configure.
Steady state for one or two mail accounts, `SEARCH_BACKEND=pg`:
| | Typical | Cap (`docker/.env.prod`) |
| | Typical | Cap (`.env`) |
|---|---|---|
| postgres | 150-300 MB | `PG_MEM_LIMIT=768m` |
| api | 150-250 MB | `API_MEM_LIMIT=512m` |
@@ -385,7 +385,7 @@ large mailboxes, at the cost of a container and 300-800 MB.
To switch:
```bash
# in docker/.env.prod: SEARCH_BACKEND=meili and set MEILI_MASTER_KEY
# in .env: SEARCH_BACKEND=meili and set MEILI_MASTER_KEY
./deploy/webmail.sh up # starts Meilisearch automatically
./deploy/webmail.sh reindex # build the index from existing mail
```
@@ -411,7 +411,7 @@ backends, or search ever looks stale, just rebuild it:
| "port is already allocated" | 8088 taken by another app | change `WEB_PORT` |
| Mail not syncing | worker stopped, or credentials rejected | `./deploy/webmail.sh logs worker-sync` |
| Search finds nothing after a restore | index not rebuilt | `./deploy/webmail.sh reindex --purge` |
| App won't start after editing `.env.prod` | malformed `MASTER_KEY` | `./deploy/webmail.sh doctor` |
| App won't start after editing `.env` | malformed `MASTER_KEY` | `./deploy/webmail.sh doctor` |
Logs: `./deploy/webmail.sh logs [service] -f`. All logs are capped at 10 MB ×3
per container, so they can't fill the volume.