https://github.com/lazybytez/conba
A simple restic based container volume backup tool
https://github.com/lazybytez/conba
backup docker docker-backup docker-compose restic restic-backup
Last synced: 14 days ago
JSON representation
A simple restic based container volume backup tool
- Host: GitHub
- URL: https://github.com/lazybytez/conba
- Owner: lazybytez
- License: mit
- Created: 2026-04-03T12:57:38.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-06-04T20:35:50.000Z (23 days ago)
- Last Synced: 2026-06-04T21:24:15.879Z (23 days ago)
- Topics: backup, docker, docker-backup, docker-compose, restic, restic-backup
- Language: Go
- Homepage:
- Size: 332 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
Awesome Lists containing this project
README
# Conba
[![License][license-badge]][license-url]
[![CI][ci-badge]][ci-url]
[![Last Commit][commit-badge]][commit-url]
**Con**tainer **Ba**ckup — automated Docker volume backups powered by restic.
## Description
Conba is a Go CLI tool that wraps [restic](https://restic.net/) to provide automated,
configurable backups for Docker container volumes. It auto-discovers containers and their
volumes, applies filtering rules, snapshots each volume (optionally running a pre-backup
command and streaming its output instead), and manages snapshot retention — all driven by
a YAML config file with environment variable overrides and optional container labels.
## Features
| Feature | Description |
|---------|-------------|
| Auto-discovery | Finds all running containers and their volume mounts via Docker API |
| Label-driven config | Per-container filtering, retention, and pre-backup commands via Docker labels |
| Pre-backup commands | Run a command in a container and stream its stdout into a snapshot — replacing or running alongside volume backups (opt-in) |
| Flexible filtering | Include/exclude by name, ID, regex, or labels; opt-in-only mode |
| Retention management | Global policy with per-container overrides; wraps `restic forget --prune` |
| Tagged snapshots | Every snapshot tagged with container name, ID, volume name, and hostname |
| Environment overrides | All config values overridable via `CONBA_` prefixed env vars |
| Structured logging | Human-readable or JSON output at configurable levels |
## Requirements
- Docker (or compatible runtime with Docker socket)
- restic (installed separately for host binary; bundled in container image)
## Getting Started
Clone and build:
```sh
git clone https://github.com/lazybytez/conba.git
cd conba
make build
```
All Make targets run inside Docker containers — no local Go installation required.
Create a config file (`conba.yaml`):
```yaml
restic:
repository: "s3:s3.amazonaws.com/my-bucket"
password_file: "/run/secrets/restic-password"
runtime:
type: docker
docker:
host: "unix:///var/run/docker.sock"
discovery:
opt_in_only: false
retention:
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
keep_yearly: 0
logging:
level: "info"
format: "human"
```
Run a backup:
```sh
./bin/conba backup
```
### Running the container image locally
After `make docker/build`, run the built image with your local (gitignored)
config bind-mounted in. This is the recommended way to smoke-test conba
against the host's Docker daemon without installing the binary:
```sh
docker run --rm -it \
--hostname "$(hostname)" \
-v "$PWD/conba-config.test.yaml:/app/conba.yaml:ro" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /var/lib/docker/volumes:/var/lib/docker/volumes:ro \
-v /tmp/conba-restic-test-repo:/tmp/conba-restic-test-repo \
ghcr.io/lazybytez/conba:edge \
backup --dry-run
```
Drop `--dry-run` to execute the backup. `--hostname "$(hostname)"` makes
snapshots carry the real host's name instead of a random container ID
(conba tags every snapshot with the hostname). The Docker socket mount
lets conba discover running containers; `/var/lib/docker/volumes` exposes
the actual volume contents so they can be read for snapshotting;
`/tmp/conba-restic-test-repo` is the writable local restic repository
(matching `restic.repository` in the test config); and the config is
mounted to `/app/conba.yaml`, the default lookup path inside the image's
working directory.
### Backing up bind mounts
Two things to know about bind mounts:
1. **Container labels match the destination path.** Use the
container-side destination in `conba.exclude-mount-destinations`
(and other label values), not the host source. Destinations are
portable across hosts; sources are not.
2. **Conba opens the source path.** When conba runs in a container,
the host source of every bind mount you want backed up must be
visible inside conba's container — mount it at the same path.
Example: a service with `-v /srv/myapp/data:/var/lib/myapp/data` is
only backed up when conba's container also has `/srv/myapp/data`
mounted at `/srv/myapp/data`:
```sh
docker run --rm -it \
...existing mounts... \
-v /srv/myapp/data:/srv/myapp/data:ro \
ghcr.io/lazybytez/conba:edge backup
```
If the source isn't reachable, conba pre-flights, logs
`WARN: skipping /: source unreadable (...)`,
and continues with the remaining targets.
## Container Labels
Configure per-container behavior with Docker labels:
| Label | Values | Default | Description |
|-------|--------|---------|-------------|
| `conba.enabled` | `true`, `false` | — | Override include/exclude filters |
| `conba.retention` | `Nd,Nw,Nm,Ny` | global | Override the global `retention:` policy for this container. Suffix-tagged, comma-separated, order-agnostic, case-insensitive. Example: `conba.retention: "7d,4w,6m,2y"`. Suffixes: `d` daily, `w` weekly, `m` monthly, `y` yearly. Missing components default to 0. |
| `conba.exclude-volumes` | comma-separated | — | Comma-separated list matched against `Mount.Name`. For named volumes that's the volume name; for bind mounts it's the host source path (which is rarely portable across hosts — prefer `conba.exclude-mount-destinations` for bind mounts). |
| `conba.exclude-bind-mounts` | `true`, `false` | `false` | Set to `true` on a container to exclude all of its bind-mounted paths from backup. Named volumes on the same container are not affected. Default: false (bind mounts are eligible). |
| `conba.exclude-mount-destinations` | comma-separated | — | Comma-separated list of container-side destination paths. Any mount (bind or named volume) whose destination matches an entry exactly is excluded from backup. Example: `conba.exclude-mount-destinations: "/var/log,/etc/myapp/cache"`. |
| `conba.pre-backup.command` | shell command | — | Required to enable a pre-backup command for the container; the shell string executed inside the container, whose stdout is streamed into restic as the snapshot. Requires `pre_backup_commands.enabled: true` in config. |
| `conba.pre-backup.mode` | `replace`, `alongside` | `replace` | `replace` substitutes the stream snapshot for the container's volume snapshots; `alongside` produces the stream snapshot plus the volume snapshots. |
| `conba.pre-backup.filename` | filename | labeled container name | Filename used for restic's `--stdin-filename` (e.g. `mysql.sql`). |
| `conba.pre-backup.restore-command` | shell command | — | Restore-side command, run inside the labeled container (locked, no sidecar override) by `conba restore` for stream snapshots when `--to-command` is not provided. Requires `pre_backup_commands.enabled: true` in config. |
## Pre-backup commands
Stateful services like databases produce inconsistent on-disk files
unless quiesced or routed through the engine's own export tool. Conba
can run a shell command inside a container at backup time and stream
its stdout into restic as the snapshot — for example, `mysqldump`
piped straight into a restic snapshot tagged for the mysql container.
The feature is **off by default**. Label-driven command execution is a
qualitative change in conba's trust surface (anyone able to set labels
on a container can cause conba to execute arbitrary shell strings
inside it), so operators must opt in explicitly:
```yaml
pre_backup_commands:
enabled: true
```
When `pre_backup_commands.enabled` is `false` or absent (the default),
all `conba.pre-backup.*` labels are ignored and volume backups proceed
as usual.
### Example: consistent MySQL backups via mysqldump
Label the mysql container with the dump command and (optionally) a
filename for the stream:
```yaml
# compose.yaml
services:
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
volumes:
- mysql-data:/var/lib/mysql
labels:
conba.pre-backup.command: 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysqldump --all-databases -uroot'
conba.pre-backup.filename: "mysql.sql"
volumes:
mysql-data:
```
The `MYSQL_PWD` env var is preferred over `-p` because the
`-p` form puts the password on the argv where any `ps`
invocation in the container's PID namespace can read it; `MYSQL_PWD`
keeps it in the env.
Enable the feature in `conba.yaml`:
```yaml
pre_backup_commands:
enabled: true
```
At backup time, conba runs `mysqldump` inside the mysql container
through the container runtime's API and streams its stdout into a
single restic snapshot tagged `container=mysql` and `kind=stream`. Conba tags every
snapshot with a `kind` — `kind=volume` for volume snapshots and
`kind=stream` for command-output (stream) snapshots — an internal
classification tag conba writes to tell the two apart, not a label
you set. In the default `replace` mode, the on-disk
`mysql-data` volume is **not** backed up as a separate snapshot —
the dump is the canonical representation of the database's state, so
the inconsistent at-rest files are skipped. Switch to
`conba.pre-backup.mode: alongside` if the container also holds
volumes you want backed up directly (e.g. an uploads directory next
to the database).
### Example: restoring a MySQL backup
`conba restore` is one command that handles both volume and stream
snapshots; conba inspects the resolved snapshot's tags and picks
the right restic primitive (`restic restore` for volume snapshots,
`restic dump` piped into an in-container command for stream snapshots,
run through the Docker API — no `docker` CLI required).
Operators describe *what* to restore via flags. Use `conba snapshots`
to enumerate candidates and pass `--snapshot ` for a
point-in-time restore; without it, conba selects the latest
matching snapshot.
#### Volume restore
Restore the latest `mysql-data` volume snapshot to a sidecar
directory for inspection:
```sh
conba restore --container mysql --volume mysql-data --to /tmp/recovered
```
The operator owns the container lifecycle. Stop the mysql container
before overwriting the live volume; conba does not auto-stop, and
restoring into a path mounted by a running container is the
operator's risk to take. If the destination is non-empty, conba
refuses unless `--force` is passed.
#### Stream restore via CLI flag
Pipe the latest stream snapshot back into mysql via the standard
client:
```sh
conba restore --container mysql \
--to-command "MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\" mysql -uroot"
```
Stream restore requires the target container to be running (you
cannot exec a command into a stopped container). Conba refuses with
a clear error otherwise.
#### Stream restore via label
Add a `conba.pre-backup.restore-command` label alongside the
existing `conba.pre-backup.command` label so operators do not have
to retype the restore invocation:
```yaml
# compose.yaml
services:
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
volumes:
- mysql-data:/var/lib/mysql
labels:
conba.pre-backup.command: 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysqldump --all-databases -uroot'
conba.pre-backup.filename: "mysql.sql"
conba.pre-backup.restore-command: 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot'
volumes:
mysql-data:
```
With the label in place, the restore reduces to:
```sh
conba restore --container mysql
```
The label form is gated by the same `pre_backup_commands.enabled:
true` feature flag as the backup-side command. When the flag is
false or absent, the label is ignored. When both `--to-command` and
the label are set, the CLI flag wins.
## CLI Commands
```
conba backup # Discover, filter, and backup all matching volumes
conba backup --dry-run # Show what would be backed up without executing
conba restore # Restore a volume snapshot to a host path or pipe a stream snapshot back into a container
conba forget # Apply retention policies and prune
conba forget --dry-run # Show what would be forgotten without changes
conba run # One-shot init + backup + forget cycle (intended for CI/CD)
conba snapshots # List snapshots
conba diff # Show file differences between two snapshots
conba verify # Verify restic repository integrity
conba verify --read-data # Full data verification (slow)
conba version # Print version info
```
## Development
All build operations run inside Docker containers via Make:
```sh
make build # Build the binary
make test # Run tests with race detector
make lint # Run golangci-lint
make coverage # Run tests with coverage report
make fmt # Format code
make clean # Remove build artifacts
```
### End-to-end tests
The `test/e2e/` package exercises the compiled `conba` binary against a real
Docker daemon and a real restic filesystem repository. A small Docker Compose
stack (`test/e2e/compose.yaml`) provides MySQL plus two Alpine services as
backup targets. Run the full suite with:
```sh
make e2e
```
The target builds the test image, brings the compose fixture up, runs every
scenario inside the test image (with `/var/run/docker.sock` and
`/var/lib/docker/volumes` mounted), then unconditionally tears the fixture
down. Iterative loop: `make go/test-e2e/up` once, then `make go/test-e2e/run`
repeatedly. CI runs the same target on every PR via `.github/workflows/e2e.yml`
and publishes per-scenario pass/fail.
### Branching
| Branch | Purpose |
|--------|---------|
| `main` | Stable — all PRs target here |
| `feature/*` | New features |
| `fix/*` | Bug fixes |
### Commit Messages
Conventional commits enforced via [commitlint](https://commitlint.js.org/):
```
prefix(scope): subject
```
Prefixes: `feat`, `fix`, `build`, `chore`, `ci`, `docs`, `perf`, `refactor`, `revert`, `style`, `test`, `sec`
## Contributing
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
## Useful Links
[License][license-url] -
[Contributing](CONTRIBUTING.md) -
[Code of Conduct][codeofconduct-url] -
[Security](SECURITY.md) -
[Issues][issues-url] -
[Pull Requests][pulls-url]
###### Copyright (c) [Lazy Bytez][team-url]. All rights reserved | Licensed under the MIT license.
[license-badge]: https://img.shields.io/github/license/lazybytez/conba?style=for-the-badge&colorA=302D41&colorB=a6e3a1
[ci-badge]: https://img.shields.io/github/actions/workflow/status/lazybytez/conba/go.yml?style=for-the-badge&colorA=302D41&colorB=89b4fa&label=CI
[commit-badge]: https://img.shields.io/github/last-commit/lazybytez/conba?style=for-the-badge&colorA=302D41&colorB=cba6f7
[license-url]: https://github.com/lazybytez/conba/blob/main/LICENSE
[ci-url]: https://github.com/lazybytez/conba/actions/workflows/go.yml
[commit-url]: https://github.com/lazybytez/conba/commits/main
[codeofconduct-url]: https://github.com/lazybytez/.github/blob/main/docs/CODE_OF_CONDUCT.md
[issues-url]: https://github.com/lazybytez/conba/issues
[pulls-url]: https://github.com/lazybytez/conba/pulls
[team-url]: https://github.com/lazybytez