Conba is a Go CLI tool that wraps restic 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.
| 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 |
- Docker (or compatible runtime with Docker socket)
- restic (installed separately for host binary; bundled in container image)
Clone and build:
git clone https://github.com/lazybytez/conba.git
cd conba
make buildAll Make targets run inside Docker containers — no local Go installation required.
Create a config file (conba.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:
./bin/conba backupAfter 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:
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-runDrop --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.
Two things to know about bind mounts:
- 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. - 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:
docker run --rm -it \
...existing mounts... \
-v /srv/myapp/data:/srv/myapp/data:ro \
ghcr.io/lazybytez/conba:edge backupIf the source isn't reachable, conba pre-flights, logs
WARN: skipping <container>/<destination>: source unreadable (...),
and continues with the remaining targets.
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. |
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:
pre_backup_commands:
enabled: trueWhen pre_backup_commands.enabled is false or absent (the default),
all conba.pre-backup.* labels are ignored and volume backups proceed
as usual.
Label the mysql container with the dump command and (optionally) a filename for the stream:
# 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<password> because the
-p<password> 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:
pre_backup_commands:
enabled: trueAt 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).
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 <id> for a
point-in-time restore; without it, conba selects the latest
matching snapshot.
Restore the latest mysql-data volume snapshot to a sidecar
directory for inspection:
conba restore --container mysql --volume mysql-data --to /tmp/recoveredThe 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.
Pipe the latest stream snapshot back into mysql via the standard client:
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.
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:
# 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:
conba restore --container mysqlThe 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.
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 <a> <b> # 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
All build operations run inside Docker containers via Make:
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 artifactsThe 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:
make e2eThe 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.
| Branch | Purpose |
|---|---|
main |
Stable — all PRs target here |
feature/* |
New features |
fix/* |
Bug fixes |
Conventional commits enforced via commitlint:
prefix(scope): subject
Prefixes: feat, fix, build, chore, ci, docs, perf, refactor, revert, style, test, sec
Contributions are welcome. See CONTRIBUTING.md.
License - Contributing - Code of Conduct - Security - Issues - Pull Requests