# Backup

> What to capture, the exact commands, and how to prove a backup is restorable.

> For the complete documentation index, see [llms.txt](https://helpdesk.orangescrum.com/llms.txt).

Source: https://helpdesk.orangescrum.com/guide/self-hosted/operate/backup

---
A complete backup is **four things**: the application data on the host, the
Docker volumes, logical database dumps, and the compose files plus secrets. Miss
any one and the restore is partial.

> **An untested backup is not a backup**
>
> Restore into a scratch instance at least once. Most backup failures are
> discovered during the first real restore, which is the worst possible time.

> **Run these on the source server**
>
> Every command below runs on the server you are backing up. Confirm container
> names with `docker ps` first — they vary with the compose project name.

## 1. Application data on the host

The application root outside Docker — typically `/data` — holds files the
containers do not.

```bash
mkdir -p /backup/$(date +%F)

tar -czf /backup/$(date +%F)/app-data.tar.gz /data

# Confirm the archive is not empty
tar -tzf /backup/$(date +%F)/app-data.tar.gz | head -20
```

Also capture host config (`/etc/app/`) and any cron entries
(`/etc/cron.d/`) — those drive digests and reminders.

## 2. Docker volumes

Each volume is archived by mounting it into a throwaway Alpine container.

```bash
# Uploaded files and attachments
docker run --rm \
  -v orangescrum-ee_orangescrum-app-files:/data \
  -v /backup/$(date +%F):/backup \
  alpine tar -czf /backup/app-files.tar.gz -C /data .

# Application configuration
docker run --rm \
  -v orangescrum-ee_orangescrum-app-config:/data \
  -v /backup/$(date +%F):/backup \
  alpine tar -czf /backup/app-config.tar.gz -C /data .

# Main database volume
docker run --rm \
  -v orangescrum-ee_orangescrum-postgres-data:/data \
  -v /backup/$(date +%F):/backup \
  alpine tar -czf /backup/postgres-data.tar.gz -C /data .

# Reports (Superset) database volume
docker run --rm \
  -v orangescrum-ee_orangescrum-reports-data:/data \
  -v /backup/$(date +%F):/backup \
  alpine tar -czf /backup/reports-data.tar.gz -C /data .
```

> **Check the volume prefix**
>
> Compose prefixes volume names with the project name — `orangescrum-ee_` above.
> Confirm with `docker volume ls`. A wrong prefix produces an archive of an empty
> volume, and nothing warns you.

File volumes can be captured while the stack runs. Database volumes ideally
should not — which is why the next step exists.

## 3. Logical database dumps — do not skip

A volume-level copy of a **running** database can be inconsistent. `pg_dump`
gives you a guaranteed clean, restorable backup. Take both.

```bash
# Confirm the container names first
docker ps --format '{{.Names}}' | grep postgres

# Main application database
docker exec orangescrum-ee-orangescrum-postgresdb-1 \
  pg_dump -U orangescrum postgres \
  > /backup/$(date +%F)/main-db.sql

# Reports / Superset database
docker exec orangescrum-ee-orangescrum-reports-db-1 \
  pg_dump -U postgres superset \
  > /backup/$(date +%F)/reports-db.sql

# Verify the dumps are non-empty
ls -lh /backup/$(date +%F)/*.sql
```

> **Check the dump size every time**
>
> A failed `pg_dump` still creates the file — just a near-empty one. If your
> backup suddenly shrinks, that is the signal. Alert on it.

## 4. Compose files and secrets

```bash
cp docker-compose.yml /backup/$(date +%F)/
cp osreports.Dockerfile /backup/$(date +%F)/   # if present

# Secrets — store this archive encrypted
tar -czf /backup/$(date +%F)/secrets.tar.gz ./secrets/
```

> **The secrets archive contains live passwords**
>
> Encrypt it at rest and restrict who can read it. Never place it in version
> control or an unencrypted object store.

## Bundle and ship it off the machine

```bash
tar -czf orangescrum-backup-$(date +%F).tar.gz /backup/$(date +%F)/

# Transfer — rsync resumes if interrupted
rsync -avz --progress \
  orangescrum-backup-$(date +%F).tar.gz \
  user@BACKUP_HOST:/backups/
```

> **A backup on the same host is not a backup**
>
> It does not survive losing the host, which is the scenario you are insuring
> against.

## What a complete set looks like

| File | Contents |
| --- | --- |
| `app-data.tar.gz` | Host application root (`/data`) |
| `app-files.tar.gz` | Attachments volume |
| `app-config.tar.gz` | Configuration volume |
| `postgres-data.tar.gz` | Main database volume |
| `reports-data.tar.gz` | Superset database volume |
| `main-db.sql` | Logical dump, main database |
| `reports-db.sql` | Logical dump, Superset database |
| `secrets.tar.gz` | Service credentials |
| `docker-compose.yml` | Stack definition |
| `osreports.Dockerfile` | Reports image build |

## Automate it

```
0 2 * * * /usr/local/bin/orangescrum-backup.sh >> /var/log/orangescrum-backup.log 2>&1
```

Then watch the things that actually fail:

- **Backup freshness**: Alert if last night's archive is missing.

- **Backup size**: A sudden drop means a dump failed and wrote nothing.

- **Off-host copy**: Confirm the transfer succeeded, not just the archive.

- **Retention**: Keep enough history to survive a problem noticed late.

## Prove it works

Quarterly, restore into a throwaway instance and sign in. It costs an hour and
tells you whether your backups are real.

- [Restore procedure](https://helpdesk.orangescrum.com/guide/self-hosted/operate/migration): The full restore, written as a server migration.
