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
+57 -3
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 ""
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 ;;