81 lines
3.8 KiB
Markdown
81 lines
3.8 KiB
Markdown
# UniFi Backup Restore Pitfalls (v10.0.162, Jul 2026)
|
|
|
|
## The Core Problem
|
|
|
|
UniFi `.unf` backup files are **encrypted BSON**. They cannot be read by:
|
|
- `zipfile` (Python) — throws `BadZipFile`
|
|
- `bsondump` (MongoDB) — reports "invalid BSONSize"
|
|
- `mongorestore` — can't parse the encryption
|
|
|
|
The ONLY way to restore a `.unf` is through the UniFi `ace.jar restore` command, which decrypts and imports the data into a running MongoDB.
|
|
|
|
## Failed Restore Approaches
|
|
|
|
### 1. CLI: `java -jar ace.jar restore <file>` (FAILED)
|
|
|
|
**Error:** `NoSuchBeanDefinitionException: No qualifying bean of type 'com.ubnt.service.system.status.o0OO'`
|
|
|
|
**Root cause:** `ace.jar restore` instantiates the FULL Spring application context, not just a MongoDB connection. It requires all UniFi service beans to be available. Running it in isolation (even with just MongoDB) fails because the Spring context expects the complete application environment.
|
|
|
|
**Attempted workarounds that also failed:**
|
|
- SIGSTOP the running UniFi Java process, then run restore → Spring context collision
|
|
- SIGKILL the process → entrypoint restarts it
|
|
- One-off Docker container with just mongod + ace.jar → same Spring bean missing
|
|
|
|
### 2. API: `POST /api/s/default/cmd/backup` (FAILED)
|
|
|
|
**Error:** `{"meta":{"rc":"error","msg":"api.err.NoSiteContext"}}`
|
|
|
|
**Root cause:** In UniFi 10.x, even with valid authentication (`unifises` cookie from `/api/login` → `{"meta":{"rc":"ok"}}`), the backup/restore API endpoints require a site context. `/api/self` returns admin details correctly, but any site-scoped endpoint fails.
|
|
|
|
**Prerequisites that were configured correctly and still failed:**
|
|
- Admin with `super_site_permissions: ["super"]`, `is_super: true`, `super_site_role: "super"`
|
|
- Privilege records with ObjectId `admin_id` and `site_id` for both "super" and "default" sites
|
|
- Role: `SUPER_ADMIN`, permissions: `["ALL"]`
|
|
|
|
**Attempted endpoints that all return NoSiteContext or Invalid:**
|
|
- `/api/s/default/cmd/backup` (with `cmd: "list-backups"` or `cmd: "restore"`)
|
|
- `/api/s/super/cmd/backup`
|
|
- `/api/s/default/cmd/system`
|
|
- `/api/system/backup`
|
|
- `/api/system/backup/list`
|
|
- `/api/system/backup/restore`
|
|
|
|
## What DOES Work
|
|
|
|
### Fresh deploy + re-adopt devices
|
|
|
|
When the controller is deployed fresh (no backup restore), devices that were previously adopted to another controller will try to inform but fail with "inform decryption failed." To re-adopt:
|
|
|
|
1. SSH into each UniFi device
|
|
2. Run: `set-inform http://<controller-ip>:8080/inform`
|
|
3. The device appears in the controller UI as "pending adoption"
|
|
4. Click "Adopt" in the UI
|
|
|
|
### Backup restore via the UI Setup Wizard
|
|
|
|
On FIRST boot of `jacobalberty/unifi`, the setup wizard at `https://<host>:8443` offers a "Restore from Backup" option. This is the only reliable path to restore a `.unf` file on v10.x.
|
|
|
|
### What to check before deploying to avoid this
|
|
|
|
1. **Always export a `.unf` before decommissioning** the old controller
|
|
2. **Store the `.unf` in S3** — don't rely on it surviving on the server
|
|
3. **Note the old controller version** — major version jumps (9.x → 10.x) add restore risk
|
|
4. **Document device SSH credentials** — needed for re-adoption if restore fails
|
|
|
|
## Dual-Storage Architecture Trap
|
|
|
|
The `jacobalberty/unifi` Docker image uses TWO storage locations that can diverge:
|
|
|
|
```
|
|
Host /opt/unifi → Container /config (bind mount: system.properties, certs, old backups)
|
|
Docker volume → Container /unifi (anonymous: MongoDB data, live backups, sites)
|
|
```
|
|
|
|
The container symlinks `/usr/lib/unifi/data` → `/unifi/data`. When running a one-off container for restore, you MUST use `--volumes-from` to access the anonymous volume. Simply bind-mounting `/opt/unifi:/config` only gives you the stale config files, not the live MongoDB.
|
|
|
|
To inspect the anonymous volume path:
|
|
```bash
|
|
docker inspect unifi-controller --format '{{json .Mounts}}' | python3 -m json.tool
|
|
```
|