feat(executors): back up Portainer's own state
Portainer keeps every stack definition, endpoint, user and access-control rule in a BoltDB inside the portainer_data Docker volume. That volume sits under /var/lib/docker/volumes/, and the daily config backup covers ~/docker-data and code-server-config only — so the thing that defines all 24 stacks was the one thing not backed up. Calls Portainer's /api/backup rather than tarring the volume. BoltDB is a single memory-mapped file, so copying it while Portainer writes can capture a torn page; the API serialises a consistent snapshot. A 200 whose body is not a readable archive is treated as failure. An archive that will not open is worse than a missing one, because it looks like a backup until the day it is needed. Writing that check found a real gap in it: a truncated tar.gz raises EOFError, which is neither TarError nor OSError, so the first version of the guard let it through. Archives contain TLS certificates and private keys and are written 0600. Retention only ever deletes files matching the exact name this executor writes, so an unrelated archive left in the same directory survives. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -114,6 +114,7 @@ modules that actually exist:
|
||||
- `gitea_release_cleanup_executor`: drop old Gitea releases, keeping the newest N
|
||||
- `postgres_retention_executor`: delete rows past a retention window (see below)
|
||||
- `docker_prune_executor`: reclaim Docker disk usage (see below)
|
||||
- `portainer_backup_executor`: archive Portainer's own state via its backup API (see below)
|
||||
- `example_executor`: demo/test
|
||||
|
||||
There is **no `shell` or `python` executor**. Earlier revisions of this document
|
||||
@@ -137,6 +138,31 @@ identifier pattern because they cannot be bound as query parameters.
|
||||
}
|
||||
```
|
||||
|
||||
#### `portainer_backup_executor`
|
||||
|
||||
Portainer keeps every stack definition, endpoint, user and access-control rule in
|
||||
a BoltDB inside the `portainer_data` Docker volume, which lives under
|
||||
`/var/lib/docker/volumes/` and is **not** covered by the daily config backup.
|
||||
This calls Portainer's `/api/backup` rather than tarring the volume: BoltDB is a
|
||||
single memory-mapped file, so copying it live can capture a torn page.
|
||||
|
||||
The archive contains TLS certificates and private keys and is written `0600`. A
|
||||
200 response whose body is not a readable archive is treated as a failure — an
|
||||
archive that will not open is worse than a missing one, because it looks like a
|
||||
backup until the day it is needed.
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "${PORTAINER_URL}",
|
||||
"api_key": "${PORTAINER_API_KEY}",
|
||||
"output_dir": "/backups/portainer",
|
||||
"retention_days": 30
|
||||
}
|
||||
```
|
||||
|
||||
Portainer runs host-networked, so a container name does not resolve; use the
|
||||
host address. Requires `/mnt/media/backups/portainer` mounted into the container.
|
||||
|
||||
#### `docker_prune_executor`
|
||||
|
||||
Uses the docker socket already mounted into the container. Only the two stages
|
||||
|
||||
Reference in New Issue
Block a user