{"id":50825688,"url":"https://github.com/lazybytez/conba","last_synced_at":"2026-06-13T18:42:49.937Z","repository":{"id":349247519,"uuid":"1200468408","full_name":"lazybytez/conba","owner":"lazybytez","description":"A simple restic based container volume backup tool","archived":false,"fork":false,"pushed_at":"2026-06-04T20:35:50.000Z","size":340,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-04T21:24:15.879Z","etag":null,"topics":["backup","docker","docker-backup","docker-compose","restic","restic-backup"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lazybytez.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-03T12:57:38.000Z","updated_at":"2026-06-04T17:44:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lazybytez/conba","commit_stats":null,"previous_names":["lazybytez/conba"],"tags_count":0,"template":false,"template_full_name":"lazybytez/general-template","purl":"pkg:github/lazybytez/conba","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lazybytez%2Fconba","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lazybytez%2Fconba/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lazybytez%2Fconba/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lazybytez%2Fconba/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lazybytez","download_url":"https://codeload.github.com/lazybytez/conba/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lazybytez%2Fconba/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34296382,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backup","docker","docker-backup","docker-compose","restic","restic-backup"],"created_at":"2026-06-13T18:42:49.047Z","updated_at":"2026-06-13T18:42:49.924Z","avatar_url":"https://github.com/lazybytez.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# Conba\n\n[![License][license-badge]][license-url]\n[![CI][ci-badge]][ci-url]\n[![Last Commit][commit-badge]][commit-url]\n\n**Con**tainer **Ba**ckup — automated Docker volume backups powered by restic.\n\n\u003c/div\u003e\n\n## Description\n\nConba is a Go CLI tool that wraps [restic](https://restic.net/) to provide automated,\nconfigurable backups for Docker container volumes. It auto-discovers containers and their\nvolumes, applies filtering rules, snapshots each volume (optionally running a pre-backup\ncommand and streaming its output instead), and manages snapshot retention — all driven by\na YAML config file with environment variable overrides and optional container labels.\n\n## Features\n\n| Feature | Description |\n|---------|-------------|\n| Auto-discovery | Finds all running containers and their volume mounts via Docker API |\n| Label-driven config | Per-container filtering, retention, and pre-backup commands via Docker labels |\n| Pre-backup commands | Run a command in a container and stream its stdout into a snapshot — replacing or running alongside volume backups (opt-in) |\n| Flexible filtering | Include/exclude by name, ID, regex, or labels; opt-in-only mode |\n| Retention management | Global policy with per-container overrides; wraps `restic forget --prune` |\n| Tagged snapshots | Every snapshot tagged with container name, ID, volume name, and hostname |\n| Environment overrides | All config values overridable via `CONBA_` prefixed env vars |\n| Structured logging | Human-readable or JSON output at configurable levels |\n\n## Requirements\n\n- Docker (or compatible runtime with Docker socket)\n- restic (installed separately for host binary; bundled in container image)\n\n## Getting Started\n\nClone and build:\n\n```sh\ngit clone https://github.com/lazybytez/conba.git\ncd conba\nmake build\n```\n\nAll Make targets run inside Docker containers — no local Go installation required.\n\nCreate a config file (`conba.yaml`):\n\n```yaml\nrestic:\n  repository: \"s3:s3.amazonaws.com/my-bucket\"\n  password_file: \"/run/secrets/restic-password\"\n\nruntime:\n  type: docker\n  docker:\n    host: \"unix:///var/run/docker.sock\"\n\ndiscovery:\n  opt_in_only: false\n\nretention:\n  keep_daily: 7\n  keep_weekly: 4\n  keep_monthly: 6\n  keep_yearly: 0\n\nlogging:\n  level: \"info\"\n  format: \"human\"\n```\n\nRun a backup:\n\n```sh\n./bin/conba backup\n```\n\n### Running the container image locally\n\nAfter `make docker/build`, run the built image with your local (gitignored)\nconfig bind-mounted in. This is the recommended way to smoke-test conba\nagainst the host's Docker daemon without installing the binary:\n\n```sh\ndocker run --rm -it \\\n  --hostname \"$(hostname)\" \\\n  -v \"$PWD/conba-config.test.yaml:/app/conba.yaml:ro\" \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  -v /var/lib/docker/volumes:/var/lib/docker/volumes:ro \\\n  -v /tmp/conba-restic-test-repo:/tmp/conba-restic-test-repo \\\n  ghcr.io/lazybytez/conba:edge \\\n  backup --dry-run\n```\n\nDrop `--dry-run` to execute the backup. `--hostname \"$(hostname)\"` makes\nsnapshots carry the real host's name instead of a random container ID\n(conba tags every snapshot with the hostname). The Docker socket mount\nlets conba discover running containers; `/var/lib/docker/volumes` exposes\nthe actual volume contents so they can be read for snapshotting;\n`/tmp/conba-restic-test-repo` is the writable local restic repository\n(matching `restic.repository` in the test config); and the config is\nmounted to `/app/conba.yaml`, the default lookup path inside the image's\nworking directory.\n\n### Backing up bind mounts\n\nTwo things to know about bind mounts:\n\n1. **Container labels match the destination path.** Use the\n   container-side destination in `conba.exclude-mount-destinations`\n   (and other label values), not the host source. Destinations are\n   portable across hosts; sources are not.\n2. **Conba opens the source path.** When conba runs in a container,\n   the host source of every bind mount you want backed up must be\n   visible inside conba's container — mount it at the same path.\n\nExample: a service with `-v /srv/myapp/data:/var/lib/myapp/data` is\nonly backed up when conba's container also has `/srv/myapp/data`\nmounted at `/srv/myapp/data`:\n\n```sh\ndocker run --rm -it \\\n  ...existing mounts... \\\n  -v /srv/myapp/data:/srv/myapp/data:ro \\\n  ghcr.io/lazybytez/conba:edge backup\n```\n\nIf the source isn't reachable, conba pre-flights, logs\n`WARN: skipping \u003ccontainer\u003e/\u003cdestination\u003e: source unreadable (...)`,\nand continues with the remaining targets.\n\n## Container Labels\n\nConfigure per-container behavior with Docker labels:\n\n| Label | Values | Default | Description |\n|-------|--------|---------|-------------|\n| `conba.enabled` | `true`, `false` | — | Override include/exclude filters |\n| `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. |\n| `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). |\n| `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). |\n| `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\"`. |\n| `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. |\n| `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. |\n| `conba.pre-backup.filename` | filename | labeled container name | Filename used for restic's `--stdin-filename` (e.g. `mysql.sql`). |\n| `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. |\n\n## Pre-backup commands\n\nStateful services like databases produce inconsistent on-disk files\nunless quiesced or routed through the engine's own export tool. Conba\ncan run a shell command inside a container at backup time and stream\nits stdout into restic as the snapshot — for example, `mysqldump`\npiped straight into a restic snapshot tagged for the mysql container.\n\nThe feature is **off by default**. Label-driven command execution is a\nqualitative change in conba's trust surface (anyone able to set labels\non a container can cause conba to execute arbitrary shell strings\ninside it), so operators must opt in explicitly:\n\n```yaml\npre_backup_commands:\n  enabled: true\n```\n\nWhen `pre_backup_commands.enabled` is `false` or absent (the default),\nall `conba.pre-backup.*` labels are ignored and volume backups proceed\nas usual.\n\n### Example: consistent MySQL backups via mysqldump\n\nLabel the mysql container with the dump command and (optionally) a\nfilename for the stream:\n\n```yaml\n# compose.yaml\nservices:\n  mysql:\n    image: mysql:8\n    environment:\n      MYSQL_ROOT_PASSWORD: \"${MYSQL_ROOT_PASSWORD}\"\n    volumes:\n      - mysql-data:/var/lib/mysql\n    labels:\n      conba.pre-backup.command: 'MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\" mysqldump --all-databases -uroot'\n      conba.pre-backup.filename: \"mysql.sql\"\n\nvolumes:\n  mysql-data:\n```\n\nThe `MYSQL_PWD` env var is preferred over `-p\u003cpassword\u003e` because the\n`-p\u003cpassword\u003e` form puts the password on the argv where any `ps`\ninvocation in the container's PID namespace can read it; `MYSQL_PWD`\nkeeps it in the env.\n\nEnable the feature in `conba.yaml`:\n\n```yaml\npre_backup_commands:\n  enabled: true\n```\n\nAt backup time, conba runs `mysqldump` inside the mysql container\nthrough the container runtime's API and streams its stdout into a\nsingle restic snapshot tagged `container=mysql` and `kind=stream`. Conba tags every\nsnapshot with a `kind` — `kind=volume` for volume snapshots and\n`kind=stream` for command-output (stream) snapshots — an internal\nclassification tag conba writes to tell the two apart, not a label\nyou set. In the default `replace` mode, the on-disk\n`mysql-data` volume is **not** backed up as a separate snapshot —\nthe dump is the canonical representation of the database's state, so\nthe inconsistent at-rest files are skipped. Switch to\n`conba.pre-backup.mode: alongside` if the container also holds\nvolumes you want backed up directly (e.g. an uploads directory next\nto the database).\n\n### Example: restoring a MySQL backup\n\n`conba restore` is one command that handles both volume and stream\nsnapshots; conba inspects the resolved snapshot's tags and picks\nthe right restic primitive (`restic restore` for volume snapshots,\n`restic dump` piped into an in-container command for stream snapshots,\nrun through the Docker API — no `docker` CLI required).\nOperators describe *what* to restore via flags. Use `conba snapshots`\nto enumerate candidates and pass `--snapshot \u003cid\u003e` for a\npoint-in-time restore; without it, conba selects the latest\nmatching snapshot.\n\n#### Volume restore\n\nRestore the latest `mysql-data` volume snapshot to a sidecar\ndirectory for inspection:\n\n```sh\nconba restore --container mysql --volume mysql-data --to /tmp/recovered\n```\n\nThe operator owns the container lifecycle. Stop the mysql container\nbefore overwriting the live volume; conba does not auto-stop, and\nrestoring into a path mounted by a running container is the\noperator's risk to take. If the destination is non-empty, conba\nrefuses unless `--force` is passed.\n\n#### Stream restore via CLI flag\n\nPipe the latest stream snapshot back into mysql via the standard\nclient:\n\n```sh\nconba restore --container mysql \\\n  --to-command \"MYSQL_PWD=\\\"$MYSQL_ROOT_PASSWORD\\\" mysql -uroot\"\n```\n\nStream restore requires the target container to be running (you\ncannot exec a command into a stopped container). Conba refuses with\na clear error otherwise.\n\n#### Stream restore via label\n\nAdd a `conba.pre-backup.restore-command` label alongside the\nexisting `conba.pre-backup.command` label so operators do not have\nto retype the restore invocation:\n\n```yaml\n# compose.yaml\nservices:\n  mysql:\n    image: mysql:8\n    environment:\n      MYSQL_ROOT_PASSWORD: \"${MYSQL_ROOT_PASSWORD}\"\n    volumes:\n      - mysql-data:/var/lib/mysql\n    labels:\n      conba.pre-backup.command: 'MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\" mysqldump --all-databases -uroot'\n      conba.pre-backup.filename: \"mysql.sql\"\n      conba.pre-backup.restore-command: 'MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\" mysql -uroot'\n\nvolumes:\n  mysql-data:\n```\n\nWith the label in place, the restore reduces to:\n\n```sh\nconba restore --container mysql\n```\n\nThe label form is gated by the same `pre_backup_commands.enabled:\ntrue` feature flag as the backup-side command. When the flag is\nfalse or absent, the label is ignored. When both `--to-command` and\nthe label are set, the CLI flag wins.\n\n## CLI Commands\n\n```\nconba backup              # Discover, filter, and backup all matching volumes\nconba backup --dry-run    # Show what would be backed up without executing\nconba restore             # Restore a volume snapshot to a host path or pipe a stream snapshot back into a container\nconba forget              # Apply retention policies and prune\nconba forget --dry-run    # Show what would be forgotten without changes\nconba run                 # One-shot init + backup + forget cycle (intended for CI/CD)\nconba snapshots           # List snapshots\nconba diff \u003ca\u003e \u003cb\u003e        # Show file differences between two snapshots\nconba verify              # Verify restic repository integrity\nconba verify --read-data  # Full data verification (slow)\nconba version             # Print version info\n```\n\n## Development\n\nAll build operations run inside Docker containers via Make:\n\n```sh\nmake build       # Build the binary\nmake test        # Run tests with race detector\nmake lint        # Run golangci-lint\nmake coverage    # Run tests with coverage report\nmake fmt         # Format code\nmake clean       # Remove build artifacts\n```\n\n### End-to-end tests\n\nThe `test/e2e/` package exercises the compiled `conba` binary against a real\nDocker daemon and a real restic filesystem repository. A small Docker Compose\nstack (`test/e2e/compose.yaml`) provides MySQL plus two Alpine services as\nbackup targets. Run the full suite with:\n\n```sh\nmake e2e\n```\n\nThe target builds the test image, brings the compose fixture up, runs every\nscenario inside the test image (with `/var/run/docker.sock` and\n`/var/lib/docker/volumes` mounted), then unconditionally tears the fixture\ndown. Iterative loop: `make go/test-e2e/up` once, then `make go/test-e2e/run`\nrepeatedly. CI runs the same target on every PR via `.github/workflows/e2e.yml`\nand publishes per-scenario pass/fail.\n\n### Branching\n\n| Branch | Purpose |\n|--------|---------|\n| `main` | Stable — all PRs target here |\n| `feature/*` | New features |\n| `fix/*` | Bug fixes |\n\n### Commit Messages\n\nConventional commits enforced via [commitlint](https://commitlint.js.org/):\n\n```\nprefix(scope): subject\n```\n\nPrefixes: `feat`, `fix`, `build`, `chore`, `ci`, `docs`, `perf`, `refactor`, `revert`, `style`, `test`, `sec`\n\n## Contributing\n\nContributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## Useful Links\n\n[License][license-url] -\n[Contributing](CONTRIBUTING.md) -\n[Code of Conduct][codeofconduct-url] -\n[Security](SECURITY.md) -\n[Issues][issues-url] -\n[Pull Requests][pulls-url]\n\n\u003chr\u003e\n\n###### Copyright (c) [Lazy Bytez][team-url]. All rights reserved | Licensed under the MIT license.\n\n\u003c!-- Badges --\u003e\n\n[license-badge]: https://img.shields.io/github/license/lazybytez/conba?style=for-the-badge\u0026colorA=302D41\u0026colorB=a6e3a1\n[ci-badge]: https://img.shields.io/github/actions/workflow/status/lazybytez/conba/go.yml?style=for-the-badge\u0026colorA=302D41\u0026colorB=89b4fa\u0026label=CI\n[commit-badge]: https://img.shields.io/github/last-commit/lazybytez/conba?style=for-the-badge\u0026colorA=302D41\u0026colorB=cba6f7\n\n\u003c!-- Links --\u003e\n\n[license-url]: https://github.com/lazybytez/conba/blob/main/LICENSE\n[ci-url]: https://github.com/lazybytez/conba/actions/workflows/go.yml\n[commit-url]: https://github.com/lazybytez/conba/commits/main\n[codeofconduct-url]: https://github.com/lazybytez/.github/blob/main/docs/CODE_OF_CONDUCT.md\n[issues-url]: https://github.com/lazybytez/conba/issues\n[pulls-url]: https://github.com/lazybytez/conba/pulls\n[team-url]: https://github.com/lazybytez\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flazybytez%2Fconba","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flazybytez%2Fconba","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flazybytez%2Fconba/lists"}