11 KiB
Deployment
Server setup, deployment, backups, and rollback procedures for XAMXAM.
Overview
- Production host:
xamxam(over SSH), app root/var/www/xamxam/ - Files are pushed with
rsync— there is no git repo on the remote - Web / FPM user:
www-data, app group:xamxam - The DocumentRoot is
app/public/, but on the server the code lives flat under/var/www/xamxam/(Composer autoload path adjusted tosrc/) - SQLite database at
/var/www/xamxam/storage/xamxam.db
Deployment is orchestrated through the justfile (deploy group).
One-time server setup
From a machine with ssh access to the xamxam host, first bootstrap the
role/user/dirs with scripts/setup-server.sh (creates the xamxam group, adds
www-data and the deploy user to it, creates /var/www/xamxam and the
cache/log/backup dirs), then run the full remote provisioning once:
# 1. role/user/dir bootstrap (as root on the host):
rsync scripts/setup-server.sh xamxam:/tmp/setup-server.sh
ssh -t xamxam "sudo DEPLOY_USER=\$USER bash /tmp/setup-server.sh"
# 2. full provisioning (env/APP_KEY + deploy + nginx + backup + cron + logrotate):
just provision-server
Fresh box caveat:
provision-serverassumessetup-server.shalready ran. On a truly empty/var/www/xamxamthe deploy's rsync fails withPermission deniedonstorage/until the setgid group-writable tree exists.scripts/deploy-server.sh(viajust deploy-nginx) is what normalises ownership/perms on each deploy.
This chains (each step is also runnable individually):
scripts/provision-server-env.sh— ensure the server has anAPP_KEYin/var/www/xamxam/.env(idempotent; see below).just deploy— code + Composer deps + migrations +deploy-nginx(install + apply the nginx config and fix permissions) + env + permissions check.just deploy-backup— install the backup script + cron jobs.just deploy-cleanup-cron,just deploy-logrotate— install the orphaned- draft cleanup cron and log rotation.
It finishes by telling you what's left to do in /admin/account (set the admin
password — a fresh DB starts unauthenticated — and configure SMTP/PeerTube
credentials and Nextcloud sync).
The abandoned-upload GC cron (
just deploy-tmp-cleanup-cron) is not part ofprovision-server; install it separately if you want hourly GC of abandoned FilePond uploads.
If you'd rather do it step by step (e.g. you already provisioned nginx):
just deploy
just deploy-nginx
For a full initial rollout without the env/nginx steps (backup + cleanup cron only), when those are already handled:
just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron + deploy-tmp-cleanup-cron + deploy-logrotate
Server APP_KEY — idempotent, never overwrites
just provision-server (via scripts/provision-server-env.sh) ensures the
server has an APP_KEY in /var/www/xamxam/.env. It is idempotent and will
never overwrite an existing key:
- If
/var/www/xamxam/.envalready has anAPP_KEY=…→ it is left untouched and the script prints that the key already exists (so encrypted credentials stay decryptable). It exits0. - If the file exists but has no
APP_KEY→ a fresh key is appended, existing lines untouched. - If the file is absent → it is created with a fresh key.
Ownership (www-data:xamxam) and permissions (640) are normalised after any
write. deploy-env also refuses to overwrite a remote .env that already has
APP_KEY.
If you ever rotate APP_KEY, re-encrypt the SMTP password with
just reencrypt-password <new_base64_key> and push the new key via
just deploy-env.
Deploying
| Command | Purpose |
|---|---|
just deploy |
Full deploy: build + code + Composer deps + migrations + env + permissions check |
just deploy-code |
rsync app files only (no Composer, no nginx, no migrations) |
just deploy-deps |
Sync composer.{json,lock} → server, then composer install/dump-autoload |
just deploy-migrate |
Run pending DB migrations on the server |
just deploy-env |
Upload app/.env (only if the remote .env is absent — never overwrites a key) |
just deploy-nginx |
Upload + apply + reload nginx config |
just deploy-permissions |
Restore www-data:xamxam ownership + 2775/664 perms (via scripts/fix-permissions.sh, needs sudo) |
just deploy-db |
Push local xamxam.db → remote (refuses if a remote DB already exists) |
just deploy-verify-permissions |
Check ownership / permissions on the server |
ℹ️ First deploy? After
just deploy, runjust deploy-backup,just deploy-cleanup-cronandjust deploy-tmp-cleanup-cronto install the backup/cleanup cron jobs, andjust deploy-logrotateto install log rotation. A singlejust deploy-all-firstchains all of these together. The app log directory/var/log/xamxam/is provisioned automatically byscripts/deploy-server.shon everyjust deploy-nginx(whichjust deployincludes), so no separate step is needed for it. To migrate logs written by an older build, runjust migrate-log-names --applyonce.
Environment file & re-encryption
app/.env holds secrets (e.g. APP_KEY). deploy-env will not overwrite a
remote .env that already has an APP_KEY. If you rotate APP_KEY, re-encrypt
the SMTP password and push the new key:
just reencrypt-password <new_base64_key> # runs scripts/reencrypt-smtp-password.php on server
just deploy-env
Backups
SQLite backups are taken with a WAL-safe hot backup (sqlite3 .backup) then
gzipped to /var/backups/xamxam/ on the server.
| Command | Purpose |
|---|---|
just deploy-backup |
Install backup script + cron jobs (one-shot) |
just deploy-backup-script |
Install /usr/local/bin/backup-sqlite.sh |
just deploy-backup-cron |
Install /etc/cron.d/xamxam-backup (hourly 30d + daily 90d) + dirs/log |
just deploy-check-backup-log |
Tail /var/log/xamxam-backup-YYYY-MM-DD.log |
just deploy-list-backups |
List backups on the server |
just trigger-backup |
Run the backup script now |
just test-restore <path.gz> |
Fetch + decompress + verify a remote snapshot |
Retention: hourly backups kept 30 days, nightly (02:00) backups kept 90 days.
Backup files: /var/backups/xamxam/db-<timestamp>.db.gz.
Off-site copies are synced daily to Nextcloud WebDAV — see nextcloud-sync.md.
Draft cleanup is handled by a separate cron (/etc/cron.d/xamxam-cleanup),
installed via just deploy-cleanup-cron, logging to
/var/log/xamxam-cleanup-YYYY-MM-DD.log. Verify with just deploy-check-cleanup-log.
Abandoned FilePond uploads (staging dirs never linked to a published TFE) are
garbage-collected hourly by /etc/cron.d/xamxam-tmp-cleanup, installed via
just deploy-tmp-cleanup-cron, logging to
/var/log/xamxam-tmp-cleanup-YYYY-MM-DD.log. Verify with
just deploy-check-tmp-cleanup-log.
Because abandoned uploads are removed automatically, the admin cleanup page
(/admin/cleanup.php, titled Nettoyage) now only surfaces the manually-managed
Corbeille (trashed files, with per-row restore/delete and bulk actions). The
manual "Téléversements abandonnés" table was removed from the UI; those files
are purged by the cron above.
Permissions model
Ownership and permissions are applied by scripts/deploy-server.sh (run via
just deploy-nginx, which just deploy includes) and verified by
deploy-verify-permissions:
- Ownership:
www-data:xamxam - Directories: 2775 (setgid — new files inherit the
xamxamgroup) - Regular files: 664
storage/xamxam.dband other*.db: 660app/.env: 640
just deploy-code only rsyncs code (as the SSH/deploy user); it does not
set ownership itself. It therefore finishes by running just deploy-permissions
(scripts/fix-permissions.sh via sudo) so a standalone deploy-code can never
leave the site broken. Ownership is normalised by deploy-server.sh
(chown -R www-data:xamxam /var/www/xamxam, setgid dirs, locked DBs) too, via
just deploy-nginx.
Why this matters (HTTP 500): php-fpm runs as
www-data. After a plainrsync -az(run as the deploy user, no--chown), synced files/dirs are owned by the deploy user, and top-level dirs such asstorage/lose thewww-datagroup-write bit. The app opens the SQLite DB in WAL mode, so www-data must be able to createxamxam.db-wal/xamxam.db-shminstorage/; without write access there the request fatals → 500. If this ever recurs (e.g. a manual rsync that skippeddeploy-permissions), runjust deploy-permissions(sudo chown -R www-data:xamxam+ 2775/664) to restore it.
The nginx/deploy-server.sh step (just deploy-nginx, or
sudo DEPLOY_USER=$USER bash /tmp/deploy-server.sh via just deploy-script)
fixes permissions and installs the nginx config.
Storage migration
If paths moved (e.g. legacy upload locations), a one-off migration script can rewrite stored paths on the server:
just deploy-migrate-storage # apply
just deploy-migrate-storage --dry-run # dry-run only
Rollback
Because there is no repo on the remote, rollback means restoring files and/or restoring the database from a backup.
Restore the database
ssh xamxam
sudo systemctl stop nginx
cp /var/backups/xamxam/db-<timestamp>.db.gz /tmp/restore.db.gz
gunzip -c /tmp/restore.db.gz > /var/www/xamxam/storage/xamxam.db
chown www-data:xamxam /var/www/xamxam/storage/xamxam.db
chmod 660 /var/www/xamxam/storage/xamxam.db
sudo systemctl start nginx
Restore application files
Re-run just deploy from a good local state. In jj you can jump back to a
previous commit and redeploy:
jj log # find a known-good change
jj edit <previous-change-id> # work from that revision
just deploy
Verify after deploy
curl -I https://xamxam.erg.be/ # expect 200
curl -I https://xamxam.erg.be/admin/ # expect 200
curl -I https://xamxam.erg.be/storage/ # expect 404 (blocked)
just deploy-verify-permissions # expect "All permissions OK"
Troubleshooting
- 502 Bad Gateway
sudo systemctl status php8.4-fpm sudo systemctl restart php8.4-fpm - Nginx config error
sudo nginx -t deploy-dbrefuses to run The remote DB already exists. Remove it manually only if you intend to overwrite production data.- Composer did not pick up new classes
deploy-depsrunscomposer dump-autoloadwhen the lock checksum is unchanged; if that still fails, force a reinstall. - Backup/schedule not running
Confirm the cron files are installed (
/etc/cron.d/xamxam-backup,/etc/cron.d/xamxam-cleanup,/etc/cron.d/xamxam-tmp-cleanup) and the log files are writable bywww-data.
Selected commands reference
| Command | Purpose |
|---|---|
just deploy |
Full deploy (build + code + deps + migrate + env + perms) |
just deploy-nginx |
Apply nginx config |
just deploy-backup |
Install backup script + cron |
just deploy-cleanup-cron |
Install orphaned-draft cleanup cron |
just deploy-tmp-cleanup-cron |
Install abandoned-upload GC cron |
just deploy-list-backups |
List server backups |
just trigger-backup |
Run backup now |
just reencrypt-password <key> |
Re-encrypt SMTP password after key rotation |
just test-restore <path.gz> |
Verify a snapshot |