Backups & upgrades
All of your business data lives in Postgres - clients, projects, time, invoices, expenses, logos and receipts included. That means backups and upgrades are ordinary Postgres and Docker operations, with nothing Clerq-specific to learn.
The examples assume the bundled docker-compose.yml, whose database service
is named postgres and whose default role and database are both clerq.
Adjust the names if you overrode POSTGRES_USER or POSTGRES_DB.
Back up
Dump the database on a schedule. A nightly cron is plenty for most setups:
docker compose exec -T postgres \
pg_dump -U clerq clerq | gzip > clerq-$(date +%F).sql.gz
Store the dumps off the host - object storage or another machine - so a single disk failure cannot take both the instance and its backups. Test a restore into a throwaway database now and then; an untested backup is a guess.
Restore
Restore a dump into a running, empty database:
gunzip -c clerq-2026-06-14.sql.gz | \
docker compose exec -T postgres psql -U clerq clerq
To restore onto a fresh instance, bring the stack up once so the database and schema exist, then load your dump over it.
Export your data
A Postgres dump is the whole instance in Postgres's own format. For a single
business in a portable, documented shape, use Export your data under
Settings -> Data. It downloads one JSON file - named
clerq-export-<business>-<date>.json - containing everything that business
owns:
- the business itself (settings, address, tax config, branding and working hours), its clients and their per-member rates, projects, tasks and time entries;
- invoices with their lines and the per-year numbering counters, and expenses including the inline receipt images;
- the client activity thread, team members (with each person's name and email), custom roles, per-member permission overrides and invitations.
The file carries a formatVersion and a kind of clerq.business-export, so a
reader can recognise it and re-read it later. This is the anti-lock-in
guarantee: your data leaves in a shape you can actually use, on demand, on Cloud
or self-hosted alike. Exporting needs the data.export permission (owners
and admins have it).
This is a per-business export, not an instance backup. For disaster recovery of a self-hosted instance, keep taking the Postgres dumps above - they capture every business and the auth tables in one shot.
Upgrade
Clerq is run from source, so an upgrade is a git pull and a rebuild:
# 1. take a fresh backup first (see above)
# 2. get the new code
git pull
# 3. rebuild and restart
docker compose up -d --build
On startup the one-shot migrate service runs any new migrations before the
app container starts, so the schema is always brought up to date for you.
Migrations are forward-only.
Always take a fresh backup immediately before upgrading a production instance, and read the release notes for any manual steps.
A self-hosted instance can tell you when a new release is out with a banner in the app - see Platform administration -> Update notifications.
Health checks
The app exposes a public health endpoint over tRPC that returns JSON without authentication - useful for uptime monitors and container health checks:
curl http://localhost:3000/api/trpc/health.ping
# {"result":{"data":{"json":{"ok":true,"time":"..."}}}}
A plain TCP or HTTP check against the app port works too. The database has its
own pg_isready health check built into the compose file.
Troubleshooting
The app container exits or restarts immediately. Almost always a missing
or wrong DATABASE_URL, or the database not being ready. Check the logs:
docker compose logs app
docker compose logs migrate
docker compose logs postgres
"DATABASE_URL is not set." The app could not find a connection string.
Under compose this is built from the POSTGRES_* variables - make sure your
.env is next to the compose file and the values are set.
"BETTER_AUTH_SECRET is not set; cannot sign invoice PDF links." Set a
BETTER_AUTH_SECRET. See
Environment variables.
A low-entropy secret warning in the logs. Your BETTER_AUTH_SECRET is too
short or too predictable. Generate a real one with openssl rand -base64 32
and restart. Note that changing it signs everyone out.
Sign-in redirects fail or loop after going public. BETTER_AUTH_URL does
not match the URL users actually reach the instance on. Set it to the exact
external origin, including https://, and restart.
Migrations did not apply. Re-run the one-shot step explicitly:
docker compose run --rm migrate