Backup and restore
An Iskue installation has two pieces of persistent state: the Postgres database and the attachment files on disk. Everything else — containers, images, the frontend — is rebuilt from the repository. This page covers the single-host Docker Compose deployment (the docker-compose.yml at the repository root); cluster deployments are addressed at the end.
What to back up
| Data | What it contains | Where it lives |
|---|---|---|
| Database | Tickets, projects, users, comments, wiki pages and configuration in the issuehub Postgres database | Named volume pgdata, served by container issuehub-postgres |
| Attachments | Uploaded file content, stored under STORAGE_DIR (/data/attachments inside the backend container) | Named volume attachments, mounted into container issuehub-backend |
| Secrets and settings | JWT_SECRET, DB_PASSWORD, OAuth client secrets and any other overrides | The .env file next to docker-compose.yml |
Attachment metadata lives in the database; the file on disk holds only the bytes, and the database row is the source of truth. A database dump and an attachments archive therefore only make sense as a pair taken at the same moment — restoring one without the other leaves orphaned rows or invisible files.
Keep a secure copy of .env with every backup. It is not recoverable from the containers, and without JWT_SECRET and DB_PASSWORD a rebuilt host cannot reproduce the previous deployment (losing JWT_SECRET also signs every user out).
Never run docker compose down -v. The -v flag deletes the pgdata and attachments volumes — the entire database and every uploaded file.
Backing up the single-host deployment
Stop the application first
Stop the backend (and frontend) so no writes land between the database dump and the attachments archive. Postgres stays up — it is what you are dumping from.
docker compose --profile full stop backend frontendDump the database
Run pg_dump inside the issuehub-postgres container. It connects over the container's local socket, so no password is required. The custom format (--format=custom) is compressed and lets pg_restore drop and recreate objects on restore.
docker exec issuehub-postgres pg_dump -U issuehub --format=custom issuehub \
> issuehub-$(date +%F).dumpDo not add -t/-it to docker exec here — a TTY corrupts binary output on stdout. A raw copy of the pgdata volume is also a valid cold backup, but only with Postgres stopped and only for restoring onto the same Postgres major version (the stack pins postgres:16-alpine); the logical dump is portable and preferred.
Archive the attachments
The attachments named volume is mounted at /data/attachments in the backend container. Using --volumes-from issuehub-backend avoids needing the volume's project-prefixed name, and works while the backend container is stopped (it must exist, which it does after any --profile full start).
docker run --rm --volumes-from issuehub-backend -v "$(pwd)":/backup alpine:3 \
tar czf /backup/attachments-$(date +%F).tar.gz -C /data attachmentsRestart the application
docker compose --profile full start backend frontendMove the dump, the attachments archive and a copy of .env off the host together. The three files are one point-in-time snapshot; store and restore them as a unit.
Restoring
Restore order: database first, then attachments, then start the application. The steps below assume a fresh host with the repository checked out and the backed-up .env in place; on an existing host, stop the backend and frontend first and skip straight to step 2.
docker compose up -d --wait postgresdocker exec -i issuehub-postgres pg_restore -U issuehub -d issuehub \
--clean --if-exists < issuehub-2026-08-03.dumpStep 3 — bring the full stack up once so the backend container and the attachments volume exist (the backend's Flyway validation will pass against the restored schema), then stop the app again and unpack the archive into the volume:
docker compose --profile full up -d --build
docker compose --profile full stop backend frontend
docker run --rm --volumes-from issuehub-backend -v "$(pwd)":/backup alpine:3 \
sh -c 'rm -rf /data/attachments/* && tar xzf /backup/attachments-2026-08-03.tar.gz -C /data'
docker compose --profile full start backend frontendVerify
docker exec issuehub-postgres psql -U issuehub -d issuehub \
-c "SELECT count(*) FROM flyway_schema_history;"Then open http://localhost:8088, sign in, and download an attachment from a ticket you know carried one — this exercises the database and the storage volume together.
Scheduled backups
The dump command is safe to run unattended while the application is up; for strict dump/archive consistency, prefer a maintenance window that stops the backend as above. A minimal nightly database dump from cron:
0 2 * * * docker exec issuehub-postgres pg_dump -U issuehub --format=custom issuehub > /var/backups/issuehub/issuehub-$(date +\%F).dumpCluster deployments
The dev/e2e cluster (deploy/docker-compose.yml) defines no database volumes at all — its Citus nodes are deliberately ephemeral and any container recreate discards the data. Do not put real data on it; there is nothing meaningful to back up.
The production cluster (deploy/docker-compose.prod.yml) stores database state in three named volumes — coordinator-data, worker1-data, worker2-data — behind the containers issuehub-cl-coordinator, issuehub-cl-worker1 and issuehub-cl-worker2, plus a shared attachments volume mounted into issuehub-cl-backend1 and issuehub-cl-backend2.
Do not transplant the single-host procedure onto the cluster. Citus distributes table rows across the worker nodes: a pg_dump taken on the coordinator does stream distributed-table rows through it, but it is not a guaranteed-consistent cluster-wide snapshot under concurrent writes and does not preserve the Citus distribution layout, and independent per-node dumps are not mutually consistent, so they cannot be combined into a valid snapshot. Consistent online backup of a Citus cluster is a Citus operational topic — follow the Citus documentation for your version (citusdata/citus:14.0.0-pg17).
The only safe compose-level fallback is a cold backup: stop the entire stack, then copy all three database volumes together as one set (plus the attachments volume, which can be archived with the same --volumes-from technique against issuehub-cl-backend1).
docker compose -f deploy/docker-compose.prod.yml stop