All apps · 0 apps
ucg-config-backup
Docker app from Tomjoad's Repository
Overview
Readme
View on GitHubucg-config-backup Docker Container
A self-contained container that backs up the configuration of a Ubiquiti UniFi Cloud Gateway Ultra — or any other UniFi OS console (UDM, UDM-Pro, UDM-SE, Cloud Key Gen2+, ...) — on a timer, without needing the console to be connected to Ubiquiti's cloud.
Instead of relying on UniFi OS "Automated System Backups" (which only run
when the console is cloud-connected and upload the backup off-site), this
container calls the same local API the Download Backup button in the
Network app uses: it logs in with a local admin account, triggers
cmd/backup, and saves the resulting .unf file to a mounted volume.
Table of contents
- How it works
- Prerequisites on the UCG Ultra
- Supported architectures
- Version tags
- Usage
- Parameters
- Environment variables
- Webhook notifications
- Volumes / paths
- Security notes
- Troubleshooting
- Support info
- Building locally
- Versions
- License
How it works
On each run (scheduled via CRON_SCHEDULE, plus an optional immediate run
at container start via RUN_ON_START) the container:
- Logs in at
/api/auth/loginwith a local admin account (username + password, no cloud SSO, no 2FA) and captures the CSRF token. - Triggers a backup via the proxied Network API (
cmd/backup). - Downloads the returned one-time backup URL and saves it as
ucg-backup-<timestamp>.unfinto/backups. - Deletes local backups older than
RETENTION_DAYS, and optionally posts a notification toWEBHOOK_URL.
The credentials are read from the container environment at runtime and are never written to the image or to disk.
Prerequisites on the UCG Ultra
Create a dedicated local admin account for the backups (not a cloud/SSO account, and without two-factor authentication — both would block this simple login flow):
UniFi OS UI → Settings → Admins & Users → Invite Admin → enable
"Restrict to local access only" (or the equivalent for your firmware),
give it the Administrator role with full management rights on the
Network application (required for cmd/backup), and do not enable 2FA
for this account.
Supported architectures
Simply pulling ghcr.io/tom-joad/ucg-config-backup:latest retrieves the
correct image for your architecture.
| Architecture | Available | Tag |
|---|---|---|
| x86-64 | ✅ | latest / <version> |
| arm64 | ✅ | latest / <version> |
Version tags
| Tag | Available | Description |
|---|---|---|
| latest | ✅ | Latest release |
| <version> | ✅ | A specific release, e.g. 2.0.0, or 2.0 for the latest patch of a minor |
| <sha> | ✅ | Immutable build of a specific commit |
Images are only built from v* tags. They are signed with cosign (releases after 2.0.0 need a cosign 3.x client to
verify; 2.0.0 and older also verify with 2.x). To verify one:
cosign verify ghcr.io/tom-joad/ucg-config-backup:latest \
--certificate-identity-regexp 'https://github.com/Tom-Joad/ucg-config-backup/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Usage
Here are some example snippets to help you get started creating a container.
docker-compose (recommended)
---
services:
ucg-config-backup:
image: ghcr.io/tom-joad/ucg-config-backup:latest
container_name: ucg-config-backup
environment:
- UCG_HOST=192.168.1.1
- UCG_USERNAME=backup-bot
- UCG_PASSWORD=change-me
- CRON_SCHEDULE=0 3 * * *
- RETENTION_DAYS=30
- PUID=1000
- PGID=1000
- UMASK=022
- TZ=Europe/Berlin
volumes:
- /path/to/config:/config
- /path/to/backups:/backups
restart: unless-stopped
This repo ships a ready-to-use docker-compose.yml — edit the values
under environment: to match your setup, then run docker compose up -d.
docker cli
See the docker run reference for more info.
docker run -d \
--name=ucg-config-backup \
-e UCG_HOST=192.168.1.1 \
-e UCG_USERNAME=backup-bot \
-e UCG_PASSWORD=change-me \
-e PUID=1000 \
-e PGID=1000 \
-e UMASK=022 \
-e TZ=Europe/Berlin \
-v /path/to/config:/config \
-v /path/to/backups:/backups \
--restart unless-stopped \
ghcr.io/tom-joad/ucg-config-backup:latest
Unraid
You can add this container in one of two ways.
Option A — from the pre-filled template (recommended):
- Copy
unraid/ucg-config-backup.xmlonto the Unraid flash share, e.g. via the network share to\\<UNRAID-IP>\flash\config\plugins\dockerMan\templates-user\(or from the Unraid terminal into/boot/config/plugins/dockerMan/templates-user/). - Docker tab → Add Container → open the Template dropdown →
select
ucg-config-backup. All fields are pre-filled (config and backup path,PUID=99,PGID=100,UMASK=002, environment variables). With these values the backups belong tonobody:usersand can be edited and deleted over SMB. - Set
UCG_HOST,UCG_USERNAME,UCG_PASSWORD(see Prerequisites); adjust the rest as needed. - Apply.
Option B — add it manually:
- Docker tab → Add Container, then toggle Advanced View (top right) so you can add custom paths and variables.
- Fill in:
- Name:
ucg-config-backup - Repository:
ghcr.io/tom-joad/ucg-config-backup:latest - Network Type:
Bridge
- Name:
- Path Mappings: Container Path
/backups→ Host Path e.g./mnt/user/appdata/ucg-config-backup/backups(or a share you already back up elsewhere), and/config→/mnt/user/appdata/ucg-config-backup/config. - Variables: add
UCG_HOST,UCG_USERNAME,UCG_PASSWORD,PUID=99,PGID=100,UMASK=002, and any optional variables from the table below (at minimum setTZ). - Apply, then check Docker → container icon → Logs to confirm the first backup ran cleanly.
This container has no web UI and runs headless — it writes .unf files to
the mounted /backups path and exits each run's work back to the
scheduler.
Synology (Container Manager / Docker)
The image is hosted on GitHub Container Registry (ghcr.io), not Docker
Hub, so DSM's built-in registry search won't find it — you need to add the
registry first.
- Open Container Manager (DSM 7.2+) or Docker (older DSM) →
Registry → settings gear icon → Add, and add:
- Name: anything, e.g.
ghcr - URL:
https://ghcr.io
- Name: anything, e.g.
- Go to Registry, search for
tom-joad/ucg-config-backup, and download thelatesttag — or, if search doesn't surface it, use Image → Add → Add From URL withghcr.io/tom-joad/ucg-config-backup:latestdirectly. - Image → select the downloaded image → Run to open the container
wizard:
- Port Settings: none needed — this container exposes no ports.
- Volume: add a folder mapping for a shared folder (e.g.
docker/ucg-config-backup/backups) →/backups, and one (e.g.docker/ucg-config-backup/config) →/config. - Environment: add
UCG_HOST,UCG_USERNAME,UCG_PASSWORD,PUID,PGID(the ID of your DSM user, seeid <user>over SSH),TZ, and any optional variables below. - Enable Enable auto-restart.
- Start the container, then confirm via Container → Details →
Log that the first backup ran cleanly. Finished
.unffiles appear in the shared folder you mapped to/backups.
You can also schedule DSM's own Hyper Backup or a Task Scheduler job to copy that shared folder off the NAS, so the UniFi config ends up in your existing 3-2-1 backup rotation.
Parameters
Container images are configured using parameters passed at runtime (such as those above). The usual linuxserver.io parameters apply:
| Parameter | Function |
|---|---|
-e PUID=1000 |
User ID the backup runs as, and the owner of the backup files |
-e PGID=1000 |
Group ID of the same |
-e UMASK=022 |
Umask of the backup files (002 lets the group write) |
-e TZ=Europe/Berlin |
Timezone for the schedule and log timestamps |
-v /config |
The container's own state (the crontab) |
-v /backups |
The downloaded .unf backup files |
Don't use --user or --init; the image runs s6-overlay as PID 1 and
drops to PUID:PGID itself.
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
UCG_HOST |
✅ | – | IP or hostname of the UCG Ultra |
UCG_USERNAME |
✅ | – | Local admin username (see Prerequisites) |
UCG_PASSWORD |
✅ | – | Password for that account |
UCG_SITE |
default |
UniFi site name | |
VERIFY_SSL |
false |
Verify the console's TLS certificate (UniFi OS ships a self-signed cert by default) | |
CRON_SCHEDULE |
0 3 * * * |
Cron expression for the backup run | |
RETENTION_DAYS |
30 |
Delete local backups older than N days (0 = keep forever) |
|
RUN_ON_START |
true |
Run a backup immediately when the container starts | |
NOTIFY_ON_SUCCESS |
false |
Also send a webhook on success, not just on failure | |
WEBHOOK_URL |
– | URL to POST run notifications to (e.g. ntfy.sh, Healthchecks.io, Home Assistant) — treat it as a secret, see Webhook notifications | |
WEBHOOK_FORMAT |
text |
text for a plain-text body, json for structured fields (Home Assistant) |
|
PUID / PGID |
911 |
User and group ID of the backup files, see Parameters | |
UMASK |
022 |
Umask of the backup files | |
TZ |
UTC |
Timezone for the schedule and log timestamps |
Webhook notifications
If WEBHOOK_URL is set, the container sends a POST after a failed run —
and after a successful one too with NOTIFY_ON_SUCCESS=true.
WEBHOOK_FORMAT picks the body:
text(default) —UCG backup OK: <filename>orUCG backup FAILED: <error>as a plain-text body. Works with ntfy.sh and Healthchecks.io.json— anapplication/jsonbody with a fixed set of fields. Use this for Home Assistant: its webhook trigger only parses JSON and form bodies and silently drops a plain-text one.
{
"status": "ok",
"message": "ucg-backup-20260911-030014.unf",
"filename": "ucg-backup-20260911-030014.unf",
"size_bytes": 2410496,
"duration_s": 12.4,
"host": "192.168.1.1",
"site": "default",
"retention_days": 30,
"backups_kept": 30,
"timestamp": "2026-09-11T03:00:26+02:00",
"version": "2.1.0"
}
| Field | Description |
|---|---|
status |
Always ok or failed (lowercase) — the value to trigger on |
message |
Human-readable part: the filename on success, the error on failure |
filename |
Name of the saved .unf file |
size_bytes |
Size of the saved file — use it to catch empty or suspiciously small backups that still report ok |
duration_s |
Run duration in seconds |
host, site |
UCG_HOST and UCG_SITE of the run |
retention_days |
Effective RETENTION_DAYS |
backups_kept |
Number of ucg-backup-*.unf files in /backups after retention |
timestamp |
ISO 8601 with the offset of TZ |
version |
Container version |
Every key is always present; values that aren't known (e.g. filename
when the login failed) are null.
A failing webhook never fails the backup run: connection errors and
5xx answers are retried twice (after 2 s and 5 s), then the run carries
on. Logs only ever show the scheme and host of WEBHOOK_URL, since its
path usually is the secret (HA webhook id, ntfy topic, Healthchecks UUID).
Home Assistant
Set WEBHOOK_FORMAT=json and point WEBHOOK_URL at a webhook trigger,
e.g. http://homeassistant.local:8123/api/webhook/ucg-backup-change-me.
This automation pushes a notification whenever a run fails:
automation:
- alias: "UCG backup failed"
triggers:
- trigger: webhook
webhook_id: ucg-backup-change-me
allowed_methods: [POST]
local_only: true
conditions:
- condition: template
value_template: "{{ trigger.json.status == 'failed' }}"
actions:
- action: notify.notify
data:
title: "UCG backup failed"
message: "{{ trigger.json.message }}"
With NOTIFY_ON_SUCCESS=true every run reports in, so Home Assistant can
also track trigger.json.timestamp as a heartbeat and warn on an unusual
size_bytes. Pick a long random webhook id — anyone who knows it can
trigger the automation.
Volumes / paths
| Path | Contents |
|---|---|
/backups |
Downloaded .unf backup files, named ucg-backup-<timestamp>.unf, owned by PUID:PGID |
/config |
The container's own state: the crontab in /config/crontabs/root is regenerated from CRON_SCHEDULE at every start |
/backups alone is enough to keep all output. If the host folder for
/backups or /config doesn't exist yet or belongs to root, the
container hands it to PUID:PGID at start (not recursively); folders that
already belong to someone else are left alone.
Security notes
- Credentials are passed as environment variables and read from the
container environment at runtime (s6
with-contenv, kept in a tmpfs) — the password is never written to the image or to a file on disk. - The backup runs as the unprivileged
abcuser (PUID:PGID), not as root. Only the cron daemon itself and the start-up scripts run as root. VERIFY_SSL=falseis the default because UniFi OS consoles ship a self-signed certificate; the login still only travels across your own LAN. If you've installed a trusted certificate on the console, setVERIFY_SSL=true.- Use a dedicated local admin account scoped to what backups need — don't reuse your primary login.
- The
docker-compose.ymlholds the password inline — if you commit a customized copy to your own repo, keep that repo private.
Troubleshooting
Login rejected (401)— wrong username/password, or the account is a cloud/SSO account rather than a local one.No CSRF token received after login— usually means 2FA is enabled on the account; this flow doesn't support 2FA. Use a dedicated account without it.403on thecmd/backupcall (login succeeded) — most often insufficient permissions: the account needs full Administrator / Full Management rights on the Network application, not "Limited Admin" or "View Only". Raise its role under Admins & Users. Less commonly it's a rotating CSRF token between login and the backup call — the script automatically adopts anx-updated-csrf-tokenfrom UniFi OS if present.Unexpected backup response, no download URL— the API response shape can differ slightly between firmware versions; check the full log (docker logs ucg-config-backup) and verifyUCG_SITE(the site name isdefaultunless you renamed it).
Support info
- Shell access while the container is running:
docker exec -it ucg-config-backup /bin/bash - Follow the logs in realtime:
docker logs -f ucg-config-backup - Trigger an on-demand backup without waiting for the schedule:
docker exec ucg-config-backup /usr/local/bin/ucg-backup
Building locally
git clone https://github.com/Tom-Joad/ucg-config-backup.git
cd ucg-config-backup
docker build \
--no-cache \
--pull \
-t ghcr.io/tom-joad/ucg-config-backup:latest .
Versions
- 03.10.2026: — 2.1.0: images are signed with cosign 3 (verifying needs a cosign 3.x client), base image updated to Alpine 3.24, build actions updated.
- 03.10.2026: — 2.0.0: rebuilt on the linuxserver.io Alpine base
image (s6-overlay):
PUID/PGID/UMASK, backups no longer owned by root, new/configvolume. See the changelog for the upgrade steps. - 11.09.2026: — 1.1.0:
WEBHOOK_FORMAT=jsonfor structured notifications (Home Assistant), webhook retries, webhook URL no longer logged, and configuration errors are now reported via the webhook too. - 19.07.2026: — Initial release: local-API backup flow, cron scheduling, retention, webhook notifications, multi-arch image, and Unraid/Synology documentation.
License
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/tom-joad/ucg-config-backup:latestRuntime arguments
- Network
bridge- Shell
bash- Privileged
- false
- Extra Params
--security-opt no-new-privileges
Template configuration
Where the .unf backup files are stored
- Target
- /backups
- Default
- /mnt/user/appdata/ucg-config-backup/backups
The container's own state (crontab)
- Target
- /config
- Default
- /mnt/user/appdata/ucg-config-backup/config
User that owns the backups (nobody)
- Default
- 99
Group for the backups (users)
- Default
- 100
002: new files are writable for the users group, e.g. over SMB
- Default
- 002
Time zone for the schedule and the log
- Default
- Europe/Berlin
IP address or host name of the UCG Ultra / UniFi OS console
Local admin user dedicated to backups (no cloud/SSO account, no 2FA)
Password of that user
UniFi site name
- Default
- default
Check the console's TLS certificate (UniFi OS ships a self-signed one)
- Default
- false
When the backup runs (cron: minute hour day month weekday)
- Default
- 0 3 * * *
Delete local backups older than N days (0 = keep everything)
- Default
- 30
Run a backup right after the container starts
- Default
- true
Also notify the webhook on success, not only on failure
- Default
- false
Optional: URL for POST notifications (ntfy.sh, Healthchecks.io, Home Assistant webhook). Usually contains a secret
text (plain text, ntfy.sh/Healthchecks.io) or json (structured fields, Home Assistant)
- Default
- text