Disaster recovery
This page is the runbook for the bad day: the server is gone — seized, dead, deleted, or unreachable in a way that is not coming back — and the board has to exist again somewhere else. Backup and restore is the everyday half of this story: what to take, how to take it, and how to restore one piece. This page is the order of operations when everything has to be restored at once, written to be followed under stress. Read it once on a calm day; the last section asks you to do slightly more than read it.
What recovery consumes
Recovery is assembled from three artifacts, and the reason each backup guide says copy them off the machine is that this page assumes the machine is gone:
| Artifact | Without it |
|---|---|
| The database dump | There is no board to recover. Everything the board knows — accounts, posts, settings, permissions — is here. |
| The uploads — a volume archive, or the S3 bucket | Every post keeps its text and loses its images; every member loses their avatar. A board on object storage skips this step entirely: the bucket never lived on the machine. |
The environment — your .env, or the secrets the panel generated |
The board boots with new secrets, but AUTH_SECRET seals members' two-factor secrets: lose it and every enrolled authenticator app is stranded, and every unsubscribe link in already-sent mail dies. Sessions survive either way — they are random tokens stored hashed in the database. |
The code is not on the list. It is in git, pinned by the release tag the board was running — which the dump itself can tell you: the recorded version is in the database, and the admin panel showed it every day.
The order
Each step exists because a later one assumes it. Resist reordering under pressure — restoring uploads before the database wastes no work, but serving traffic before verifying does.
1. Provision
A server like the one you lost: the
requirements have not changed because
the old machine died. Install Docker, clone the repository, and check out
the release the board was running — never a newer one, never main:
git clone https://github.com/meith-dev/meith && cd meith/docker
git checkout v0.12.0Recovering and upgrading are two changes; make them one at a time. Once the board is verified and serving, upgrade the ordinary way — Upgrading a board — with this recovery as the backup you just proved restorable.
2. Write the environment
Recreate .env from your copy — the same POSTGRES_PASSWORD, the same
AUTH_SECRET, the same TICK_SECRET, the same S3_* and mail values.
This step comes before the database because the compose stack will not
start without it, and the migrator refuses to run unnamed.
If a secret truly is lost, the table above says what each one costs.
TICK_SECRET is the cheap one — generate a new value and update whatever
external scheduler presents it, if anything does. AUTH_SECRET is the
expensive one; regenerating it means telling your members to re-enrol
their authenticator apps, so exhaust the places a copy might be first.
3. Restore the database
Bring up Postgres alone, restore into it, and keep the board down until the data is in:
docker compose up -d postgres
gunzip -c board-2026-08-20.sql.gz | docker compose exec -T postgres psql -U community community(For a --format=custom dump, pg_restore --no-owner --no-privileges
into the same place —
Restoring has the variants.) The compose
stack's migrate service will run before anything serves; on a dump
taken from the same version it reports nothing to do, and that silence is
itself a check.
4. Restore the uploads
On local disk, unpack the archive into the named volume before the board comes up:
docker compose up -d --no-start web
docker run --rm -v docker_uploads:/u -v "$PWD":/backup alpine \
tar xzf /backup/uploads-2026-08-20.tar.gz -C /u(docker volume ls for the real volume name — Compose prefixes it with
the project directory.) On S3 there is nothing to restore: confirm the
credential in .env still works and move on. This asymmetry is most of
the argument for
moving uploads to object storage
on a calm day.
5. Boot and verify
docker compose up -d --buildThen the same three checks a restore rehearsal uses, plus two this situation adds:
select count(*) from posts;— the content is there.- Sign in as an administrator — the credentials survived.
community upgrade --dry-run— it reports nothing to do.- Open a thread with attachments and a page with avatars — the uploads restore actually met the database restore.
/admin/settings?group=mail→ Send a test message — mail is the subsystem that fails silently, and the provider may be seeing a new IP address.
Verify on the new machine directly — curl against localhost, or a
hosts-file entry for your domain — while the world still resolves to the
old address. Nothing here needs DNS.
6. Cut over
Point the domain at the new machine. The reverse proxy obtains its certificate on the first request after DNS moves (put a proxy in front); until propagation finishes, some visitors reach the corpse and some the recovery — which is another reason the old address should serve nothing rather than something stale. If you can plan ahead at all, a short TTL on the record turns this step from hours into minutes.
7. Resume the backups
The new machine has no cron, and the recovery you just finished consumed a backup rather than producing one. Re-create the schedule from taking one, run it once by hand, and copy the result off the machine — the next disaster does not care how recent the last one was.
Partial losses are smaller pages
Full loss is rare; most bad days are one of these, and each has a shorter answer than this runbook:
| What happened | Do this |
|---|---|
| A bad upgrade | Restore the pre-upgrade backup — Downgrades explains why that is the whole answer. |
| The database is fine, uploads are gone | Restore the uploads archive alone (step 4); the board can serve, with broken images, while it runs. |
| Uploads are fine, the database is gone | Steps 3 and 5. Do not skip the verification because the machine survived. |
| The Valkey/Redis cache server died | Nothing. It held cache entries only; restart it and the cache warms on the next requests. |
| The disk is full | Not a restore at all — free space (old backups on the server are the usual culprit, which is its own lesson) and the board resumes. |
Rehearse it, and write the number down
The backup page's advice — a backup nobody has restored is a file, not a backup — applies to this whole page: a runbook nobody has run is a hope, not a plan. Once, on a scratch server or a laptop, run steps 1 through 5 against last week's real backups and time it. That number is your recovery time; the gaps you hit are this page's errata for your board — a panel-generated secret you never copied, a volume name you guessed wrong. Fix what you find, note the time somewhere that is not on the server, and repeat after anything about the deployment changes shape.