# Migrate to a new server

> Restore a full backup onto fresh infrastructure with data intact, and verify before cutting over.

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

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

---
This is the restore procedure, written as a server move. The same steps recover
an instance after a failure — the only difference is whether the old server is
still around.

> **Everything here runs on the NEW server**
>
> Unless a step says otherwise. Start from a complete backup — see
> [Backup](https://helpdesk.orangescrum.com/guide/self-hosted/operate/backup).

## 1. Prepare the new server

| Requirement | Install | Minimum |
| --- | --- | --- |
| Docker Engine | `curl -fsSL https://get.docker.com \| sh` | 24.x |
| Docker Compose plugin | `apt install docker-compose-plugin` | 2.x |
| curl | `apt install curl` | any |
| rsync / scp | `apt install rsync openssh-client` | any |

```bash
docker --version
docker compose version
```

Meet the [prerequisites](https://helpdesk.orangescrum.com/guide/self-hosted/get-started/prerequisites) too —
migrating onto an undersized box just moves the problem.

## 2. Extract the backup

```bash
cd /home/user
tar -xzf orangescrum-backup-YYYY-MM-DD.tar.gz

ls -lh backup/YYYY-MM-DD/
```

Confirm all ten files are present before going further.

## 3. Restore host application data

```bash
mkdir -p /data
tar -xzf backup/YYYY-MM-DD/app-data.tar.gz -C /
ls -lh /data
```

## 4. Prepare the project directory

```bash
mkdir -p /opt/orangescrum
cd /opt/orangescrum

cp /home/user/backup/YYYY-MM-DD/docker-compose.yml .
cp /home/user/backup/YYYY-MM-DD/osreports.Dockerfile .   # if present

# Secrets
tar -xzf /home/user/backup/YYYY-MM-DD/secrets.tar.gz -C /opt/orangescrum/
ls secrets/
```

Verify every `.txt` file is present — a missing secret shows up later as a
container that restarts forever.

## 5. Create the volumes and restore into them

Create each volume first, then populate it.

```bash
docker volume create orangescrum-ee_orangescrum-app-files
docker volume create orangescrum-ee_orangescrum-app-config
docker volume create orangescrum-ee_orangescrum-postgres-data
docker volume create orangescrum-ee_orangescrum-reports-data

for v in app-files app-config postgres-data reports-data; do
  docker run --rm \
    -v orangescrum-ee_orangescrum-$v:/data \
    -v /home/user/backup/YYYY-MM-DD:/backup \
    alpine tar -xzf /backup/$v.tar.gz -C /data
done
```

> **Volume names must match the compose project**
>
> If the prefix differs from the source server, the stack creates its own empty
> volumes and starts with no data. Confirm with `docker volume ls` before
> starting.

## 6. Start the stack

```bash
cd /opt/orangescrum

docker compose pull
docker compose up -d

docker compose ps
docker compose logs -f --tail=50
```

Allow one to two minutes for health checks. The reports service waits for its
database to be healthy before it starts.

## 7. If the database did not come back — restore from the dumps

The volume restore is the fast path; the SQL dumps are the reliable fallback.

```bash
docker compose ps orangescrum-postgresdb   # wait for healthy

docker exec -i orangescrum-ee-orangescrum-postgresdb-1 \
  psql -U orangescrum postgres \
  < /home/user/backup/YYYY-MM-DD/main-db.sql

docker exec -i orangescrum-ee-orangescrum-reports-db-1 \
  psql -U postgres superset \
  < /home/user/backup/YYYY-MM-DD/reports-db.sql
```

## 8. Verify before you cut over

```bash
docker compose ps

curl -f http://localhost:8080     # application
curl -f http://localhost:8088     # reports

docker compose logs orangescrum-app --tail=30
docker compose logs orangescrum-postgresdb --tail=30
```

Then confirm the data really arrived:

```bash
docker exec -it orangescrum-ee-orangescrum-postgresdb-1 \
  psql -U orangescrum postgres -c '\dt'

docker exec -it orangescrum-ee-orangescrum-postgresdb-1 \
  psql -U orangescrum postgres -c 'SELECT count(*) FROM users;'
```

### Verification checklist

**Work through every item before switching DNS**

    - All four containers report **healthy**
    - Application answers on 8080
    - Reports answer on 8088
    - You can log in with existing credentials
    - Projects, tasks and users are visible
    - **A file attachment downloads** — a database-only restore looks perfect until someone clicks a file
    - The reports dashboard loads existing dashboards
    - `/data` on the host is intact
    - Scheduled jobs (digests, reminders) are present in cron

## 9. Cut over

**Update DNS or the load balancer**

    Point at the new server's address.

**Reissue or move TLS certificates**

    A migration with a stale certificate presents as a total outage.

**Keep the source server running**

    Until verification is fully complete. It is your rollback.

**Re-check background jobs**

    Cron lives outside Docker and is the thing most often lost in a move.

## Troubleshooting

| Symptom | Resolution |
| --- | --- |
| Container keeps restarting | `docker compose logs <service>` — usually a wrong password in a secrets file |
| Database connection refused | Ensure the database container is healthy before the app starts |
| Port not accessible | `ufw allow 8080/tcp`, `ufw allow 8088/tcp`, or fix the proxy |
| Volume looks empty after restore | The tar restore failed or the prefix was wrong: `docker run --rm -v <volume>:/data alpine ls /data` |
| Permission denied on `/data` | Re-apply ownership: `chown -R www-data:www-data /data` |
| Reports service will not start | It waits on its database — give it ~60s, then check its logs |

- [Upgrading](https://helpdesk.orangescrum.com/guide/self-hosted/operate/upgrade): Moving to a newer Orangescrum version.
