Add a deploy script, with no host in it

The live deploy script has never been under version control, which makes
it the one part of the pipeline that can silently fall out of step with
the repo -- as it just did: it builds without --build-arg
IHASMAIL_VERSION, so every deployment would report 2.16.0 no matter what
was actually built.

This is that script with the host taken out of it. Every path, name,
port and volume is a variable with a default that describes the shape of
a deployment rather than any particular one, so what is published is the
logic and none of the topology. It follows Caddyfile.example and
nginx.example.conf, which are here for the same reason.

Nothing sensitive was in the original either -- it never reads the
environment file, only hands the path to docker run --env-file -- but
the absolute paths named a user and a directory layout, and there is no
reason for those to be public to buy version control over the guards.

Two things it does that the original did not:

  - passes the version to the build, which is the whole reason this came
    up. A Docker tag may not contain "+", which a version for a commit
    that arrived outside a pull request does (2.16.57+g1fa6578), so the
    tag turns it into "-" while the build is told the real form. About
    and /api/health still report it correctly.

  - tags each build with its own version as well as :current, so rolling
    back is running the previous tag rather than rebuilding it. The
    failure path lists what is there to go back to.

Exercised against a throwaway clone, container, volume and image:
the hold guard refuses a held commit that production does not already
carry and lets one through that it does; the confirmation guard refuses
to run over a pipe without --yes; a dry run stops before building; and a
full run built, replaced the container and reported healthy at
2.16.57+g0549a09 -- from an image tagged 2.16.57-g0549a09, which is the
sanitising working.
This commit is contained in:
2026-08-26 10:18:44 -07:00
parent 4618f7656e
commit 2d72e870c4
2 changed files with 193 additions and 0 deletions
+28
View File
@@ -209,6 +209,34 @@ session to find:
for unsupported servers can be tested. That is all it does — the rest still
behaves like 0.16. Emulating 0.15 properly went with the support for it
### Deploying
[`deploy.example.sh`](deploy.example.sh) is a single-host Docker deploy: it
fetches, refuses anything held back, shows what is about to be introduced and
asks, rebuilds the image with the right version baked in, replaces the
container and waits for it to report healthy. Copy it, or run it as-is and set
what differs in the environment — `IHASMAIL_APP`, `IHASMAIL_ENV`,
`IHASMAIL_NAME`, `IHASMAIL_BIND`, `IHASMAIL_VOLUME`, `IHASMAIL_IMAGE`.
```bash
./deploy.sh # origin/main, asks before shipping new commits
./deploy.sh --dry-run # run the guards and stop
./deploy.sh v2.16.57 --yes # a named ref, no prompt (there is no tty over ssh)
```
Two guards, because a deploy script is exactly where a careless run does the
most damage. `.deploy-hold` lists commits that must not reach production yet,
one per line, and a target carrying one that production does not already have
is refused outright — `--yes` does not override it, and clearing a hold means
deleting its line. Separately, anything introducing new commits is listed and
has to be confirmed; over SSH, with no terminal to answer on, that means
passing `--yes` deliberately rather than a bare run shipping whatever `main`
has picked up since.
Each build is tagged with its own version as well as `:current`, so rolling
back is running the previous tag rather than rebuilding it. The environment
file is never read by the script, only handed to `docker run --env-file`.
## Configuration
All configuration is via environment variables (see `.env.example`):