Deploy: make a no-SSH install possible from the DSM GUI

The install guide was SSH-first and left the GUI as a footnote, but the two
things it could not do were both fixable rather than inherent.

- `init` takes --data-root / --domain / --tz / --port / --bind, so configuring
  is one command instead of hand-editing a dotfile that File Station hides by
  default. Re-running with flags updates an existing .env.prod.
- `show-key` prints MASTER_KEY, so it can be backed up without opening that
  hidden file — intended to be redirected to a file, copied into a password
  manager, and deleted.
- usage() printed a hardcoded line range that spilled into the code once the
  header grew; it now prints the leading comment block however long it gets.
- docs §2.5 is a complete GUI walkthrough: Gitea "Download ZIP" + File Station
  (with the chmod a zip download needs), two one-off Task Scheduler jobs for
  config and key backup, then the Container Manager project on the repo root
  named webmail-prod. Plus a table for running update/rollback/backup/restore
  /doctor/reindex as Task Scheduler jobs, and why the UI's own Build action is
  not a substitute for `update` (no pre-backup, no health gate, no rollback).

Verified by running the documented sequence verbatim: flag-driven init,
show-key, then `docker compose up -d --wait` from the project folder as the
GUI does — all services healthy, account created, backup and doctor tasks
working against the result.

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:18:41 +02:00
co-authored by Claude Opus 5
parent c472404486
commit 24031f1daa
2 changed files with 131 additions and 47 deletions
+55 -1
View File
@@ -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 ""
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
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 ;;
+74 -44
View File
@@ -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 <file>` |
| 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 <file>` | 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