The caddy-init service's `command:` used a YAML FOLDED scalar (`>`). Base
indent is set by `sh -c '` at 6; the closing `}` and the heredoc's `EOF`
were BOTH at 6, so YAML folded them into `} EOF`. The heredoc opened with
`<<EOF` therefore never saw a terminator line.
Confirmed by parsing the generated compose file: the command contains
`} EOF` and no bare EOF line. Feeding the resulting script to sh:
here-document at line 3 delimited by end-of-file (wanted `EOF')
syntax error: unexpected end of file (rc=2)
So caddy-init exited 2 before its `>` redirect ever ran -- no Caddyfile was
ever written, on any deploy. The `if [ ! -f /etc/caddy/Caddyfile ]` guard
never executed at all. caddy gates on
`caddy-init: condition: service_completed_successfully`, and smp-server and
xftp-server gate on caddy being healthy, so nothing in the stack started.
`docker compose up -d` returned non-zero and `set -euo pipefail` aborted the
script before the final report. The same failure hit every entry point:
automations.sh, cloud-init, the OpenRC unit's start() (so every boot), and
restore.sh -- which restores the same broken compose file backup.sh saved.
Generate the Caddyfile from the deploy shell instead and drop caddy-init
entirely, which removes the whole class of problem. Unlike the old
first-run-only guard this also applies a changed DOMAIN, ACME_EMAIL or
KEY_TYPE on a re-run rather than freezing them at the first deploy.
The heredoc here is deliberately unquoted so ${DOMAIN}/${ACME_EMAIL}/
${KEY_TYPE} expand; Caddy's {uri} has no `$` and survives. `cmp -s` stays
inside an `if` condition -- as `cmp -s A B && CADDY_CHANGED=1` it would trip
set -e whenever the files matched. Caddy is restarted only when the file
actually changed AND compose did not already replace the container, since a
bind-mounted file's contents are not part of the compose config hash and
restarting seconds after a first start would interrupt initial ACME issuance.
The Caddyfile is now real on-disk state rather than something a container
regenerates, so it is added to backup.sh's targets and restore.sh's file
list; without that, a restore would start caddy against an empty
./caddy_conf and the servers would never pass their health gate.
Verified: first deploy writes it and reports no change; an identical re-run
reports no change; a corrected DOMAIN rewrites it and flags the restart;
{uri} survives and no .new file is left behind.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Restructure around a single entry point (automations.sh) with a Gum wizard and
a self-extracting bundle for repo-less installs. Add scripts/oslib.sh so the
provisioning scripts (setup-host, harden-ssh, harden-jumphost, sshuser) run on
Alpine/Debian/Alma; seed root keys from globals/.
- ntfy SSH-login alerts (user, source IP, key, region, jump target) via pam_exec
- daily auto-updates with AUTO_REBOOT=idle (reboots only when no SSH active) and
opt-in Alpine stable-branch upgrades
- generic + per-deployment cloud-init; Gitea release workflow on tag
- README/LICENSE/.gitignore/.gitattributes (force LF); repo URLs -> Gitea