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:
+57
-3
@@ -13,6 +13,7 @@
|
|||||||
# ./deploy/webmail.sh reindex [args] rebuild the search index from Postgres
|
# ./deploy/webmail.sh reindex [args] rebuild the search index from Postgres
|
||||||
# ./deploy/webmail.sh psql open a database shell
|
# ./deploy/webmail.sh psql open a database shell
|
||||||
# ./deploy/webmail.sh shell [svc] shell inside a container (default: api)
|
# ./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
|
# ./deploy/webmail.sh doctor check the install before it bites you
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -89,7 +90,34 @@ record_version() {
|
|||||||
|
|
||||||
# --- commands ----------------------------------------------------------------
|
# --- 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() {
|
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
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
@@ -109,11 +137,22 @@ cmd_init() {
|
|||||||
ylw " Without it, saved mail-account passwords cannot be decrypted even"
|
ylw " Without it, saved mail-account passwords cannot be decrypted even"
|
||||||
ylw " from a perfect database backup."
|
ylw " from a perfect database backup."
|
||||||
ylw ""
|
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
|
return 0
|
||||||
fi
|
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
|
require_env
|
||||||
mkdir -p "${DATA_ROOT}"/{postgres,blobs,redis,backups,meili,state} 2>/dev/null \
|
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)"
|
|| 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."
|
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_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_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; }
|
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
|
cmd="${1:-}"; shift || true
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
@@ -394,6 +447,7 @@ case "$cmd" in
|
|||||||
backup) cmd_backup "$@" ;;
|
backup) cmd_backup "$@" ;;
|
||||||
restore) cmd_restore "$@" ;;
|
restore) cmd_restore "$@" ;;
|
||||||
reindex) cmd_reindex "$@" ;;
|
reindex) cmd_reindex "$@" ;;
|
||||||
|
show-key) cmd_show_key "$@" ;;
|
||||||
psql) cmd_psql "$@" ;;
|
psql) cmd_psql "$@" ;;
|
||||||
shell) cmd_shell "$@" ;;
|
shell) cmd_shell "$@" ;;
|
||||||
""|-h|--help|help) usage ;;
|
""|-h|--help|help) usage ;;
|
||||||
|
|||||||
+74
-44
@@ -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
|
Open `http://your-nas:8088/`, create your account, and add a mail account
|
||||||
through the wizard.
|
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 whole install can be done without ever enabling SSH. The trick is **Task
|
||||||
the **repo root** (`/volume1/docker/webmail/src`), and name the project
|
Scheduler**: it runs any shell command as root from the DSM web UI, which covers
|
||||||
`webmail-prod`.
|
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
|
**1. Get the code onto the NAS.** In Gitea, open the repository and use
|
||||||
`docker-compose.yml`: the one in `docker/` is the throwaway *dev* service stack
|
*Download ZIP* (the `<>` Code button). In DSM, open *File Station*, create
|
||||||
(Postgres/Redis/Meilisearch/GreenMail on dev ports) and starting it does not
|
`docker/webmail/`, upload the zip into it, then right-click → *Extract here*.
|
||||||
give you a webmail server. The root one is production.
|
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
|
**2. Configure it — one Task Scheduler job.** *Control Panel → Task Scheduler →
|
||||||
one in the compose file, and keeping them the same means `deploy/webmail.sh`
|
Create → Scheduled Task → User-defined script*. Set **User: root**, leave it
|
||||||
and the GUI manage the same stack instead of two rival copies.
|
unscheduled, and paste this as the script (adjust the four values):
|
||||||
|
|
||||||
**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:
|
|
||||||
|
|
||||||
```bash
|
```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
|
Save, select the task, and click *Run*. The `chmod` line matters: a zip download
|
||||||
a `.env` symlink in the repo root, which is what Container Manager reads — one
|
loses the executable bit. This generates `MASTER_KEY`, a database password, and
|
||||||
file, one place to edit).
|
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,
|
**3. Back up the master key — a second one-off task.** Same recipe, script:
|
||||||
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).
|
|
||||||
|
|
||||||
**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` |
|
| Update safely | `bash deploy/webmail.sh update` | after pulling new code |
|
||||||
| Health-gated updates | it just pulls and restarts; no gate, no rollback | `./deploy/webmail.sh update` |
|
| Roll back | `bash deploy/webmail.sh rollback` | if an update misbehaves |
|
||||||
| Backup before migrating | — | `./deploy/webmail.sh update` does it automatically |
|
| Backup now | `bash deploy/webmail.sh backup` | before anything risky |
|
||||||
| Restore a dump | — | `./deploy/webmail.sh restore <file>` |
|
| Restore | `echo RESTORE \| bash deploy/webmail.sh restore <file>` | disaster recovery |
|
||||||
| Pre-flight checks, reindex | — | `./deploy/webmail.sh doctor` / `reindex` |
|
| 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
|
Updating *through the Container Manager UI instead* (project → Build) does work
|
||||||
**Exited (0)** — they are one-shot jobs that did their work and stopped, and
|
and does apply migrations — but it takes no backup first, has no health gate,
|
||||||
every other service waited for them. And the first start builds both images on
|
and will not roll back if the new version fails to start. The `update` task
|
||||||
the NAS, which is slow and wants ~2 GB of RAM; §6.3 shows how to build them on
|
above does all three. Use it.
|
||||||
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.
|
|
||||||
|
|
||||||
### 2.6 Starting automatically after a reboot
|
### 2.6 Starting automatically after a reboot
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user