diff --git a/deploy/webmail.sh b/deploy/webmail.sh index fbdde56..8b7ab5e 100755 --- a/deploy/webmail.sh +++ b/deploy/webmail.sh @@ -13,6 +13,7 @@ # ./deploy/webmail.sh reindex [args] rebuild the search index from Postgres # ./deploy/webmail.sh psql open a database shell # ./deploy/webmail.sh shell [svc] shell inside a container (default: api) +# ./deploy/webmail.sh show-key print MASTER_KEY so you can back it up # ./deploy/webmail.sh doctor check the install before it bites you set -euo pipefail @@ -89,7 +90,34 @@ record_version() { # --- commands ---------------------------------------------------------------- +apply_init_flags() { + [ -n "$set_data_root" ] && set_env_value DATA_ROOT "$set_data_root" && echo " DATA_ROOT = $set_data_root" + [ -n "$set_domain" ] && set_env_value APP_DOMAIN "$set_domain" && echo " APP_DOMAIN = $set_domain" + [ -n "$set_tz" ] && set_env_value TZ "$set_tz" && echo " TZ = $set_tz" + [ -n "$set_port" ] && set_env_value WEB_PORT "$set_port" && echo " WEB_PORT = $set_port" + [ -n "$set_bind" ] && set_env_value WEB_BIND "$set_bind" && echo " WEB_BIND = $set_bind" + return 0 +} + cmd_init() { + # Settings can be passed as flags so the whole install can be driven from a + # NAS GUI (DSM Task Scheduler / QTS cron), with no editing of a hidden + # dotfile in File Station. Re-running with flags updates those values. + local set_data_root="" set_domain="" set_tz="" set_port="" set_bind="" + while [ "$#" -gt 0 ]; do + case "$1" in + --data-root) set_data_root="${2:-}"; shift 2 ;; + --domain) set_domain="${2:-}"; shift 2 ;; + --tz) set_tz="${2:-}"; shift 2 ;; + --port) set_port="${2:-}"; shift 2 ;; + --bind) set_bind="${2:-}"; shift 2 ;; + -h|--help) + echo "usage: webmail.sh init [--data-root PATH] [--domain DOMAIN] [--tz ZONE] [--port N] [--bind IP]" + return 0 ;; + *) die "init: unknown option $1" ;; + esac + done + if [ ! -f "$ENV_FILE" ]; then cp "$ENV_EXAMPLE" "$ENV_FILE" chmod 600 "$ENV_FILE" @@ -109,11 +137,22 @@ cmd_init() { ylw " Without it, saved mail-account passwords cannot be decrypted even" ylw " from a perfect database backup." ylw "" - echo "Then edit $ENV_FILE and set at least: DATA_ROOT, APP_DOMAIN, TZ, WEB_PORT." + apply_init_flags + if [ -n "$set_data_root" ]; then + grn "configured from flags; you can go straight to: webmail.sh up" + else + echo "Then edit $ENV_FILE and set at least: DATA_ROOT, APP_DOMAIN, TZ, WEB_PORT." + echo "(or re-run: webmail.sh init --data-root ... --domain ... --tz ... --port ...)" + fi return 0 fi - ylw "$ENV_FILE already exists; leaving it alone." + if [ -n "${set_data_root}${set_domain}${set_tz}${set_port}${set_bind}" ]; then + apply_init_flags + grn "updated $ENV_FILE" + else + ylw "$ENV_FILE already exists; leaving it alone." + fi require_env mkdir -p "${DATA_ROOT}"/{postgres,blobs,redis,backups,meili,state} 2>/dev/null \ || ylw "could not create every directory under ${DATA_ROOT} (the init container will)" @@ -374,11 +413,25 @@ cmd_restore() { grn "restored. If the blob store was lost too, mail bodies will refill on next sync." } +# Print the master key so it can be backed up without opening a hidden dotfile +# in File Station. Intended for a one-off DSM Task Scheduler job: +# bash deploy/webmail.sh show-key > /volume1/docker/webmail/MASTER_KEY.txt +# copy it into your password manager, then delete that file. +cmd_show_key() { + require_env + echo "MASTER_KEY=${MASTER_KEY}" + echo + echo "Copy this into your password manager, then DELETE the file you wrote" + echo "it to. Without this key, saved mail-account passwords cannot be" + echo "decrypted from any backup." +} + cmd_reindex() { require_env; compose run --rm api reindex "$@"; } cmd_psql() { require_env; compose exec postgres psql -U "${POSTGRES_USER:-webmail}" -d "${POSTGRES_DB:-webmail}"; } cmd_shell() { require_env; compose exec "${1:-api}" /bin/sh; } -usage() { sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's|^# \?||'; } +# Print the header comment block, however long it grows. +usage() { awk 'NR>1 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "${BASH_SOURCE[0]}"; } cmd="${1:-}"; shift || true case "$cmd" in @@ -394,6 +447,7 @@ case "$cmd" in backup) cmd_backup "$@" ;; restore) cmd_restore "$@" ;; reindex) cmd_reindex "$@" ;; + show-key) cmd_show_key "$@" ;; psql) cmd_psql "$@" ;; shell) cmd_shell "$@" ;; ""|-h|--help|help) usage ;; diff --git a/docs/nas-install.md b/docs/nas-install.md index e97536e..bb0e5bb 100644 --- a/docs/nas-install.md +++ b/docs/nas-install.md @@ -106,62 +106,92 @@ is small or you'd rather not tie it up, build on your PC instead — see §6.3. Open `http://your-nas:8088/`, create your account, and add a mail account through the wizard. -### 2.5 Synology Container Manager (GUI instead of SSH) +### 2.5 Installing entirely from the DSM GUI (no SSH) -Yes, this runs from the GUI. *Container Manager → Project → Create*, point it at -the **repo root** (`/volume1/docker/webmail/src`), and name the project -`webmail-prod`. +The whole install can be done without ever enabling SSH. The trick is **Task +Scheduler**: it runs any shell command as root from the DSM web UI, which covers +the three things the Container Manager UI cannot do on its own. -Point it at the repo root, **not** at `docker/`. Both folders contain a -`docker-compose.yml`: the one in `docker/` is the throwaway *dev* service stack -(Postgres/Redis/Meilisearch/GreenMail on dev ports) and starting it does not -give you a webmail server. The root one is production. +**1. Get the code onto the NAS.** In Gitea, open the repository and use +*Download ZIP* (the `<>` Code button). In DSM, open *File Station*, create +`docker/webmail/`, upload the zip into it, then right-click → *Extract here*. +Rename the extracted folder to `src`, so you end up with +`/volume1/docker/webmail/src/docker-compose.yml`. -Name the project `webmail-prod` too — the name you type in the UI overrides the -one in the compose file, and keeping them the same means `deploy/webmail.sh` -and the GUI manage the same stack instead of two rival copies. - -**You still need a shell once**, to create `docker/.env.prod` with a generated -`MASTER_KEY` — there is no way to generate a random key in the GUI. Either SSH -in and run `./deploy/webmail.sh init`, or, if you would rather not enable SSH at -all, use *Control Panel → Task Scheduler → Create → Scheduled Task → User-defined -script*, run as `root`, with: +**2. Configure it — one Task Scheduler job.** *Control Panel → Task Scheduler → +Create → Scheduled Task → User-defined script*. Set **User: root**, leave it +unscheduled, and paste this as the script (adjust the four values): ```bash -cd /volume1/docker/webmail/src && ./deploy/webmail.sh init +cd /volume1/docker/webmail/src +chmod +x deploy/*.sh docker/*.sh +bash deploy/webmail.sh init \ + --data-root /volume1/docker/webmail/data \ + --domain mail.home.lan \ + --tz Europe/Warsaw \ + --port 8088 ``` -Run it once, then edit `docker/.env.prod` in DSM's Text Editor (`init` also puts -a `.env` symlink in the repo root, which is what Container Manager reads — one -file, one place to edit). +Save, select the task, and click *Run*. The `chmod` line matters: a zip download +loses the executable bit. This generates `MASTER_KEY`, a database password, and +the `.env` that Container Manager will read — with no hidden files to edit. -**What you get in the GUI:** start/stop, live logs, per-container resource use, -automatic restart after reboot, and the nightly database backups (the `backup` -service is an ordinary container, so it runs regardless of how you started the -stack). +**3. Back up the master key — a second one-off task.** Same recipe, script: -**What the GUI cannot do:** +```bash +cd /volume1/docker/webmail/src +bash deploy/webmail.sh show-key > /volume1/docker/webmail/MASTER_KEY.txt +``` -| | Why | Instead | +Run it, open `MASTER_KEY.txt` in File Station, copy the key into your password +manager, then **delete the file**. Skipping this is the one mistake with no +recovery: without that key, no backup can decrypt your saved mail-account +passwords. + +**4. Create the project.** *Container Manager → Project → Create*: + +| Field | Value | +|---|---| +| Project name | `webmail-prod` | +| Path | `/volume1/docker/webmail/src` | +| Source | it will find `docker-compose.yml` — choose to use the existing file | + +Pick the **repo root**, not `docker/`. Both contain a `docker-compose.yml`, and +the one in `docker/` is the throwaway *dev* service stack — starting it gives +you containers but no webmail server. Use the project name `webmail-prod` +exactly: the name typed here overrides the one in the file, and matching them +means the GUI and `deploy/webmail.sh` drive the same stack. + +Click through, and it builds. **First build takes 5-15 minutes and wants ~2 GB +of RAM** — it compiles the frontend on the NAS. This is the one place the GUI +route is meaningfully worse than SSH, where you can build on your PC and copy +the images over (§6.3). + +**5. Open `http://your-nas:8088/`** and register your account. + +**What you will see in Container Manager:** `init` and `migrate` sitting at +**Exited (0)**. That is correct — they are one-shot jobs that did their work and +stopped, and everything else waited on them. + +#### Running the maintenance commands without a shell + +Task Scheduler is not just for setup — it runs any of these, and can schedule +them. Each is a user-defined script, user **root**, starting with +`cd /volume1/docker/webmail/src`: + +| Task | Script line | When | |---|---|---| -| Start Meilisearch | compose profiles aren't exposed in the UI | `./deploy/webmail.sh up` — or delete the `profiles:` line from `docker/docker-compose.prod.yml` | -| Health-gated updates | it just pulls and restarts; no gate, no rollback | `./deploy/webmail.sh update` | -| Backup before migrating | — | `./deploy/webmail.sh update` does it automatically | -| Restore a dump | — | `./deploy/webmail.sh restore ` | -| Pre-flight checks, reindex | — | `./deploy/webmail.sh doctor` / `reindex` | +| Update safely | `bash deploy/webmail.sh update` | after pulling new code | +| Roll back | `bash deploy/webmail.sh rollback` | if an update misbehaves | +| Backup now | `bash deploy/webmail.sh backup` | before anything risky | +| Restore | `echo RESTORE \| bash deploy/webmail.sh restore ` | disaster recovery | +| Health check | `bash deploy/webmail.sh doctor` | when something looks wrong | +| Rebuild search | `bash deploy/webmail.sh reindex --purge` | after a restore | -**Two things that look wrong but aren't:** `init` and `migrate` sit there as -**Exited (0)** — they are one-shot jobs that did their work and stopped, and -every other service waited for them. And the first start builds both images on -the NAS, which is slow and wants ~2 GB of RAM; §6.3 shows how to build them on -your PC instead. - -After editing `.env`, containers keep their old values until they are recreated -— `./deploy/webmail.sh up` does that cleanly. - -The honest recommendation: install and update over SSH, and use the GUI for -day-to-day watching, starting and stopping. The update path is where all the -safety lives, and it is the one thing the GUI has no equivalent for. +Updating *through the Container Manager UI instead* (project → Build) does work +and does apply migrations — but it takes no backup first, has no health gate, +and will not roll back if the new version fails to start. The `update` task +above does all three. Use it. ### 2.6 Starting automatically after a reboot