Docs
grovs.io Start for free

Backups

Back up a Compose installation on a schedule, restore it step by step and prove the restore works.

A complete backup is the .env file plus three data stores. Without the .env encryption keys, a database backup cannot be read back. Run the commands on this page from the install directory: ./grovs for the installer, /opt/grovs as root on a cloud VM.

What to back up

ItemLives inContainsMethod
.envInstall directoryEvery secret, including the at-rest encryption keysFile copy, stored encrypted
PostgreSQLpg_data volumeThe system of record: projects, links, users, purchasespg_dump
ClickHouseclickhouse_data volumeEvents and analyticsClickHouse BACKUP
Uploadsstorage volumeUploaded images and exportstar of /app/storage. Back up the bucket instead when ACTIVE_STORAGE_SERVICE=amazon.
Redisredis_data volumeQueues and buffered eventsOptional. Only events not yet processed are at risk.
Certificatescaddy_data volumeCertificates issued by the standalone proxyOptional. Caddy issues them again.

A disk or volume snapshot of a running database is not a reliable database backup. Use the dump commands below.

Backup script

Save this as backup.sh in the install directory. It writes a timestamped folder under backups/ and deletes folders older than 14 days.

Bash
#!/usr/bin/env bash
# Back up Grovs: .env, PostgreSQL, ClickHouse and uploads. Run from the install directory.
set -euo pipefail
umask 077
cd "$(dirname "$0")"
STAMP="$(date +%Y%m%d-%H%M%S)"
DEST="backups/$STAMP"
mkdir -p "$DEST"
 
cp .env "$DEST/env"
docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' > "$DEST/postgres.dump"
docker compose exec -T clickhouse clickhouse-client -q "BACKUP DATABASE grovs_production TO File('$STAMP.zip')"
docker compose cp "clickhouse:/var/lib/clickhouse/backups/$STAMP.zip" "$DEST/clickhouse.zip"
docker compose exec -T clickhouse rm "/var/lib/clickhouse/backups/$STAMP.zip"
docker compose exec -T web tar -C /app/storage -czf - . > "$DEST/storage.tar.gz"
 
find backups -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} +
echo "Backup written to $DEST"
Bash
chmod 700 backup.sh
./backup.sh

All three dumps run while Grovs is serving traffic. ClickHouse writes its archive inside its own volume before the copy, so keep free disk space for one ClickHouse backup. For a local trial, run export COMPOSE_FILE=docker-compose.yml:docker-compose.local.yml first.

Schedule backups

Run the script daily from root's crontab, then copy the folder off the server. A backup on the same disk does not survive the loss of that disk.

Bash
0 3 * * * /opt/grovs/backup.sh >> /var/log/grovs-backup.log 2>&1
30 3 * * * rsync -a /opt/grovs/backups/ [email protected]:grovs-backups/

Replace the rsync target with your own storage. Each folder holds a copy of .env, so keep the destination private and encrypted.

Restore

Restore onto a new server, or onto the same server to undo a failed change. The restored .env must be the one saved with the backup: it pins GROVS_VERSION and holds the keys for the encrypted data.

1
Prepare the install directory

On a new server, get the stack files and put the saved .env in place. Do not run setup.sh; it would generate new secrets for an empty directory.

Bash
git clone https://github.com/grovs-io/self-host.git grovs && cd grovs
cp /path/to/backup/env .env
chmod 600 .env

On the existing server, stop the application instead:

Bash
docker compose --profile standalone stop dashboard web worker-1 worker-2
2
Start the data stores

Start only the databases, then set B to the backup folder you are restoring:

Bash
docker compose up -d --wait postgres redis clickhouse
B=/path/to/backup
3
Restore PostgreSQL

Recreate the database empty, then load the dump:

Bash
docker compose exec -T postgres sh -c 'dropdb -U "$POSTGRES_USER" --force "$POSTGRES_DB" && createdb -U "$POSTGRES_USER" "$POSTGRES_DB"'
docker compose exec -T postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --no-owner --exit-on-error' < "$B/postgres.dump"
4
Restore ClickHouse

ClickHouse reads backups from a folder inside its volume. Create it as the clickhouse user, copy the archive in, then replace the database:

Bash
docker compose exec -T -u clickhouse clickhouse mkdir -p /var/lib/clickhouse/backups
docker compose cp "$B/clickhouse.zip" clickhouse:/var/lib/clickhouse/backups/restore.zip
docker compose exec -T clickhouse clickhouse-client -q "DROP DATABASE IF EXISTS grovs_production SYNC"
docker compose exec -T clickhouse clickhouse-client -q "RESTORE DATABASE grovs_production FROM File('restore.zip')"
docker compose exec -T clickhouse rm /var/lib/clickhouse/backups/restore.zip
5
Restore uploads
Bash
docker compose run --rm --no-deps -T web tar -C /app/storage -xzf - < "$B/storage.tar.gz"
6
Start Grovs
Bash
docker compose --profile standalone up -d
docker compose --profile standalone ps -a

migrate runs again and exits with code 0 against the restored data. On a new server, point DNS at it before you send traffic.

Test your restore

Restore the latest backup onto a separate server or VM on a regular schedule, then check:

  1. Every container is running, and migrate exited with code 0.
  2. You can log in with an existing dashboard account. The bootstrap password in .env only works if nobody changed it.
  3. Your projects and links are there, and a link opens.
  4. Analytics show the history you expect.
  5. Uploaded images load.

Use a test domain for the test server so it does not take traffic from production.

Platforms

Railway, Render, Easypanel and CapRover use managed databases or object storage. They need the same coverage: PostgreSQL, ClickHouse, the bucket and the environment secrets. Use each platform's backup tools, and keep a private copy of the environment.

Before you remove a server

Back up the databases, uploads and .env outside that server. Deleting a server or disk can destroy the data. Check your provider's console for retained disks, IPs and backups that keep billing.

Last updated 2026-10-26
Was this page helpful?