Upgrading Pavillion
This guide covers upgrading your Pavillion instance to new versions.
The Only Upgrade Command
bin/deploy.shRun this from your Pavillion checkout. The script is idempotent: it is safe to run at any time, whether you have changes to apply or not.
bin/deploy.sh handles every step of upgrade:
- Checks that your working tree is clean.
- Runs
git pullto fetch the latest code and configuration changes. - Reads
bin/deploy-manifest.tsvto learn what secrets the new version requires. - Compares against your
.env. Silently generates any missing regenerable secrets. Prompts you to paste (or regenerate) any missing stable secrets. - Runs
docker compose pullto fetch the new image. - Runs
docker compose up -dto start (or restart) containers. - Polls
http://localhost:3000/healthuntil the app responds or the timeout expires. - Reports success or failure.
Pre-Upgrade Checklist
Before running bin/deploy.sh for a major upgrade:
1. Review release notes
Read the release notes for breaking changes, new configuration options, or deprecations. A release whose notes describe it as a security-only refresh of the container image changes nothing in the application; see Security refreshes.
2. Back up your database
Backups are not automated. Run a pg_dump before any major upgrade:
docker compose exec db pg_dump -U pavillion pavillion > \
backup-pre-upgrade-$(date +%Y%m%d-%H%M%S).sqlIf you use local media storage (not S3), back up the media volume too:
docker run --rm -v pavillion-media:/data -v "$(pwd):/backup" alpine \
tar czf /backup/media-backup-$(date +%Y%m%d).tar.gz -C /data .Flags
bin/deploy.sh --help| Flag | Purpose |
|---|---|
--non-interactive | Do not prompt. Fail fast if a stable secret is missing. |
--skip-git-pull | Skip the git pull step (for local development). |
--domain=<value> | Supply domain for non-interactive first install. Ignored on upgrade. |
--health-timeout=<seconds> | Override the /health poll timeout (default 120s). |
What happens when a new version adds a required secret
Every required secret is declared in bin/deploy-manifest.tsv, tagged as either regenerable or stable. The script also tracks every secret ever provisioned on this instance in .deploy-state (per-instance, not in version control). On upgrade:
A new secret is introduced this version (its name is in the manifest but not in
.deploy-state): the script silently generates it, regardless of stability. You couldn't have it; the script gives you one. This is the normal path for upgrades that add a new required secret.A
regenerablesecret is missing (was previously provisioned but is no longer in.env): silently regenerated. The associated feature reissues whatever tokens it had — pending DNS verifications get new challenges, for example. No cross-system impact.A
stablesecret is missing AND was previously provisioned (it's in.deploy-statebut not in.env— admin lost it): the script prompts you (interactive) or exits with a clear error (non-interactive). Regenerating a stable secret has real impact (invalidated sessions, broken decryption, re-anonymized moderation reports); the script requires you to acknowledge the impact or paste the existing value from your password manager.
The staging webhook calls bin/deploy.sh --non-interactive. Because new secrets auto-generate on upgrade, the only situation that requires interactive intervention is recovering a previously-provisioned stable secret that's gone missing — which is a rare manual-recovery scenario, not a normal upgrade.
One-time migration for installs that predate bin/deploy.sh
If .deploy-state is absent on first run of bin/deploy.sh, the script seeds it from the contents of your .env. Every secret name currently defined in .env is recorded as previously-provisioned, so subsequent upgrades that touch existing secrets get the right behavior.
Rollback
bin/deploy.sh does not automate rollback. If an upgrade fails:
Stop the containers:
bashdocker compose downRestore your database backup (see step 2 of the pre-upgrade checklist).
Pin the previous image version in
docker-compose.yml:yamlservices: app: image: ghcr.io/stephenhoward/pavillion:v1.1.0 # previous versionStart with the previous version:
bashdocker compose up -dVerify:
curl http://localhost:3000/health.
Troubleshooting
"Working tree is not clean"
The script requires a clean working tree to run git pull. Options:
- Commit or stash your local changes, then re-run.
- Pass
--skip-git-pullto skip the pull and proceed with what's on disk.
"Secret X is missing (stable, non-interactive mode)"
A new version added a required stable secret that is not in your .env. Options:
- Re-run interactively:
bin/deploy.sh(without--non-interactive). The script will prompt you to paste the value or generate a new one. - Or manually: generate with
openssl rand -base64 32, appendX=<value>to.env, and re-run.
Health check timed out
The containers started but /health did not respond within the timeout. Check docker compose logs app for errors. Common causes: migration failure, database connection issue, missing configuration in config/local.yaml.
Getting Help
If you encounter issues:
- Check the troubleshooting section.
- Search existing issues.
- Open a new issue with:
- Current version and target version
- Full output from
bin/deploy.sh(with secrets redacted) - Your
config/local.yaml(secrets redacted)