Full backup solution for your docker containers, with automatic database dumps, and volume backup.
  • Ruby 83.8%
  • JavaScript 7.4%
  • Shell 3.2%
  • HTML 2.7%
  • CSS 2.2%
  • Other 0.6%
Find a file
James be25887ace
All checks were successful
Build, Push & Deploy / test (push) Has been skipped
Build, Push & Deploy / build-and-push (push) Successful in 18s
Build, Push & Deploy / deploy (Media) (push) Successful in 6s
Build, Push & Deploy / deploy (citadel) (push) Successful in 5s
Merge pull request 'chore: stop tracking runtime state (SQLite DB, RSpec examples.txt)' (#19) from chore/gitignore-runtime-state into main
Reviewed-on: #19
2026-07-30 17:18:46 +00:00
.forgejo ci: run tests on PRs, deploy only on push to main 2026-07-24 17:05:21 -04:00
app chore: stop tracking runtime state (SQLite DB, RSpec examples.txt) 2026-07-30 13:17:31 -04:00
dev feat: add Redis backup support, running backups display, and auth system v0.3.0 2026-01-24 20:36:55 -05:00
docs docs(cluster): 2-node validation harness + runbook 2026-07-27 15:56:01 -04:00
hooks docs: update Docker registry references from Docker Hub to Forgejo 2026-03-02 14:25:27 -05:00
screenshots docs: rewrite documentation for thin-node cluster + refresh screenshots 2026-07-28 16:16:15 -04:00
.dockerignore Initial Commit 2025-04-14 09:39:37 -04:00
.gitignore chore: stop tracking runtime state (SQLite DB, RSpec examples.txt) 2026-07-30 13:17:31 -04:00
.tool-versions Initial Commit 2025-04-14 09:39:37 -04:00
API_DOCUMENTATION.md docs: rewrite documentation for thin-node cluster + refresh screenshots 2026-07-28 16:16:15 -04:00
CHANGELOG.md fix(backup): don't fail volume backups on warning-level tar exits 2026-07-30 11:39:04 -04:00
CLAUDE.md docs: rewrite documentation for thin-node cluster + refresh screenshots 2026-07-28 16:16:15 -04:00
DEVELOPMENT.md docs: rewrite documentation for thin-node cluster + refresh screenshots 2026-07-28 16:16:15 -04:00
docker-compose.cluster.yml docs(cluster): 2-node validation harness + runbook 2026-07-27 15:56:01 -04:00
docker-compose.dev.yml feat: migrate UI to ERB views and add compression/secrets/strategy modules 2026-07-23 15:20:56 -04:00
docker-compose.yml docs: update Docker registry references from Docker Hub to Forgejo 2026-03-02 14:25:27 -05:00
Dockerfile feat(cluster): ADR 0001 + Phase A1 — headless node (no web server) 2026-07-27 11:49:13 -04:00
entrypoint.sh feat: add Redis backup support, running backups display, and auth system v0.3.0 2026-01-24 20:36:55 -05:00
LICENSE Initial commit 2025-04-05 13:58:02 +00:00
README.md docs: rewrite documentation for thin-node cluster + refresh screenshots 2026-07-28 16:16:15 -04:00
SECURITY.md docs: rewrite documentation for thin-node cluster + refresh screenshots 2026-07-28 16:16:15 -04:00
TROUBLESHOOTING.md fix(backup): don't fail volume backups on warning-level tar exits 2026-07-30 11:39:04 -04:00

Baktainer

A Docker-native backup & restore system for your database containers and volumes.

Baktainer watches a Docker daemon for containers you've labelled for backup, and on a cron schedule dumps each one's database and/or its volumes to compressed (optionally encrypted) archives — with a web dashboard, REST API, notifications, retention, and one-click restore. Run it standalone on a single host, or as a thin-node cluster that backs up many Docker hosts and manages them all from one dashboard.

Baktainer dashboard

Features

  • Database engines — PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, and Redis
  • Volume backups — Docker volumes and bind mounts, standalone or alongside a database
  • Thin-node cluster — one dashboard across many Docker hosts; headless worker nodes enroll with a cryptographic identity and are approved from the UI
  • Scheduling — cron-based backups per instance
  • Container discovery — opt containers in with Docker labels; schema-based label validation with helpful errors
  • Web dashboard & REST API — real-time status, per-stack backup grid, history, and restore
  • Authentication — local users (Argon2id) with roles, plus OIDC/SSO
  • Notifications — Slack, Discord, Microsoft Teams, generic webhooks, and logs
  • Backup rotation — automatic cleanup by age, count, or free disk space
  • Encryption — AES-256-GCM for backup files
  • Compression — gzip (default) or zstd
  • Hooks — pre/post-backup scripts for custom workflows (S3, Restic, …)
  • Restore & browse — download, inspect, and restore any backup from the dashboard
  • Auto-import — imports existing .meta backups on first start
  • SSL/TLS — secure connections to a remote Docker API
  • High performance — multi-threaded backups with dynamic scaling and streaming for large databases

Installation

⚠️ Security notice: Baktainer needs access to the Docker socket, which grants significant privileges. Review SECURITY.md before deploying, and consider a Docker socket proxy.

services:
  baktainer:
    image: ruby-code.com/james/baktainer:latest
    container_name: baktainer
    restart: unless-stopped
    ports:
      - "8080:8080"                                    # Dashboard + REST API
    volumes:
      - ./backups:/backups
      - ./data:/data                                   # SQLite: history + users
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - BT_CRON=0 0 * * *                              # Daily at midnight
      - BT_DOCKER_URL=unix:///var/run/docker.sock
      - BT_THREADS=4
      - BT_LOG_LEVEL=info
      - BT_HEALTH_SERVER_ENABLED=true
      - BT_COMPRESS=true
      - BT_ROTATION_ENABLED=true
      - BT_RETENTION_DAYS=30
      # - BT_ENCRYPTION_ENABLED=true
      # - BT_ENCRYPTION_KEY=your-hex-key
      # - BT_NOTIFICATION_CHANNELS=slack,log
      # - BT_SLACK_WEBHOOK_URL=https://hooks.slack.com/...

Then browse to http://localhost:8080. With authentication enabled (the default) you'll be walked through a one-time admin setup.

Quick start

  1. Add labels to a database container you want backed up (see Docker labels):

    services:
      db:
        image: postgres:17
        environment:
          POSTGRES_DB: appdb
          POSTGRES_USER: app
          POSTGRES_PASSWORD: secret
        labels:
          - baktainer.backup=true
          - baktainer.db.engine=postgres
          - baktainer.db.name=appdb
          - baktainer.db.user=app
          - baktainer.db.password=secret
          - baktainer.name=myapp
    
  2. Start Baktainer with the compose file above.

  3. Validate your setup without writing anything: docker exec baktainer ruby app.rb --dry-run.

  4. Trigger a backup immediately: docker exec baktainer ruby app.rb --now, or click Run All Backups on the dashboard.

Docker labels

Containers opt in via labels. Only baktainer.backup and baktainer.db.engine are always required.

Core labels

Label Description Required
baktainer.backup Set true to enable backup for this container Yes
baktainer.db.engine postgres, mysql, mariadb, sqlite, mongodb, redis, or volume Yes
baktainer.db.name Database name(s), comma-separated (Redis: DB number 015) All except SQLite / volume
baktainer.db.user Database username MySQL, MariaDB, PostgreSQL (Redis for ACL)
baktainer.db.password Database password MySQL, MariaDB, PostgreSQL, Redis
baktainer.db.all true to dump all databases Optional (Postgres/MySQL)

Optional labels

Label Description Default
baktainer.name Backup name used in filenames and the UI Database name
baktainer.stack Group backups by stack in the UI com.docker.compose.project, else name
baktainer.volumes.backup true to also back up the container's volumes/bind mounts false
baktainer.compress Per-container compression override (true/false) BT_COMPRESS
baktainer.backup.encrypt Per-container encryption (true/false) BT_ENCRYPTION_ENABLED
baktainer.backup.priority low, normal, high, or critical normal
baktainer.backup.retention.days Days to keep this container's backups BT_RETENTION_DAYS
baktainer.backup.retention.count Max backups to keep for this container BT_RETENTION_COUNT
baktainer.command Custom backup command none

Environment variables

Booleans accept true/false, 1/0, yes/no, on/off.

Core

Variable Description Default
BT_CRON Cron expression for scheduled backups 0 0 * * *
BT_THREADS Worker threads for backups (max 50) 4
BT_LOG_LEVEL debug, info, warn, error info
BT_DOCKER_URL Docker API URL unix:///var/run/docker.sock
BT_BACKUP_DIR Directory for backup files /backups
BT_BACKUP_TIMEOUT Overall backup timeout (seconds) 1800

Backup, compression & rotation

Variable Description Default
BT_COMPRESS Enable compression true
BT_COMPRESSION_TYPE gzip or zstd gzip
BT_COMPRESSION_LEVEL Compression level engine default
BT_ROTATION_ENABLED Enable automatic rotation true
BT_RETENTION_DAYS Days to keep backups (0 = unlimited, max 365) 30
BT_RETENTION_COUNT Max backups per container (0 = unlimited) 0
BT_MIN_FREE_SPACE_GB Trigger cleanup below this free space 10

Encryption

Variable Description Default
BT_ENCRYPTION_ENABLED Enable AES-256-GCM encryption false
BT_ENCRYPTION_KEY Encryption key (hex or base64) none
BT_ENCRYPTION_KEY_FILE Path to a key file none
BT_ENCRYPTION_PASSPHRASE Passphrase for key derivation none
BT_KEY_ROTATION_ENABLED Enable key rotation false

Health / dashboard

Variable Description Default
BT_HEALTH_SERVER_ENABLED Serve the dashboard + API (must be true) unset
BT_HEALTH_PORT Dashboard port 8080
BT_HEALTH_BIND Bind address 0.0.0.0
BT_HEALTH_CHECK_TOKEN Token for external health checks none

Authentication

Variable Description Default
BT_AUTH_ENABLED Require login for the dashboard/API true
BT_AUTH_SECRET Session signing secret (auto-generated if unset) auto
BT_DB_PATH SQLite database path /data/baktainer.db
BT_SESSION_TIMEOUT Session lifetime (seconds) 86400
BT_PASSWORD_MIN_LENGTH Minimum password length (min 8) 12
BT_MAX_LOGIN_ATTEMPTS Failed logins before lockout 5
BT_LOCKOUT_DURATION Lockout duration (seconds) 900

OIDC / SSO

Variable Description Default
BT_OIDC_ENABLED Enable OIDC login false
BT_OIDC_ISSUER Issuer URL none
BT_OIDC_CLIENT_ID / BT_OIDC_CLIENT_SECRET Client credentials none
BT_OIDC_REDIRECT_URI Callback URL none
BT_OIDC_SCOPES Requested scopes openid,profile,email
BT_OIDC_ADMIN_GROUP Group/claim that grants the admin role none

Notifications

Variable Description Default
BT_NOTIFICATION_CHANNELS Comma-separated channels (log always available) log
BT_NOTIFY_SUCCESS / BT_NOTIFY_FAILURES / BT_NOTIFY_WARNINGS / BT_NOTIFY_HEALTH Per-event toggles false/true/true/true
BT_SLACK_WEBHOOK_URL / BT_DISCORD_WEBHOOK_URL / BT_TEAMS_WEBHOOK_URL / BT_WEBHOOK_URL Channel webhook URLs none

Hooks

Variable Description Default
BT_HOOKS_ENABLED Run pre/post-backup hook scripts true
BT_HOOKS_DIR Directory containing hook scripts /hooks

SSL to the Docker daemon

Variable Description Default
BT_SSL Enable SSL for a remote Docker API false
BT_CA / BT_CERT / BT_KEY CA / client cert / client key (path or data) none

PostgreSQL tuning

Variable Description Default
BT_PG_LOCK_TIMEOUT pg_dump lock wait timeout 60s
BT_PG_STATEMENT_TIMEOUT Statement timeout (0 = disabled) 0
BT_PG_VERBOSE Verbose pg_dump output false

Cluster

See Thin-node cluster.

Variable Description Default
BT_CLUSTER_ROLE standalone, main, or node standalone
BT_MAIN_URL Base URL of the main instance (required on a node) none
BT_NODE_NAME Display name for this instance hostname
BT_CLUSTER_JOIN_TOKEN Optional shared enrollment secret none
BT_CLUSTER_AUTO_APPROVE Auto-approve nodes presenting a valid join secret false
BT_HEARTBEAT_INTERVAL Node check-in / offline interval basis (seconds) 30
BT_CLUSTER_POLL_IDLE Idle check-in cadence (seconds) 15
BT_CLUSTER_POLL_ACTIVE Fast cadence right after commands (seconds) 1
BT_CLUSTER_IDENTITY_PATH Node keypair identity file /data/identity.json
BT_CLUSTER_SPOOL_PATH Node event spool file /data/cluster-spool.jsonl
BT_MAX_TRANSFERS Max concurrent node→main download relays 3

Usage

Dry-run validation

Validate labels and backup tooling without writing any files:

docker exec baktainer ruby app.rb --dry-run
============================================================
DRY-RUN MODE REPORT
============================================================

Discovered 3 containers for backup:

✓ mysql-prod: mysql backup
  → Would backup 'production_db' to: /backups/2026-07-28/mysql-prod-1785268200.sql

✗ redis-cache: redis backup
  → Issues found:
    • Backup command 'redis-cli' not found in container

------------------------------------------------------------
Configuration validation: 2 passed, 1 failed
============================================================

Command-line options

docker exec baktainer ruby app.rb --help             # Show help
docker exec baktainer ruby app.rb --now              # Back up now, ignoring the schedule
docker exec baktainer ruby app.rb --dry-run          # Validate configuration
docker exec baktainer ruby app.rb --import-backups    # Import legacy .meta files (optional DIR)

Backup file layout

/backups/<YYYY-MM-DD>/<name>-<timestamp>.sql[.gz][.enc]
/backups/<YYYY-MM-DD>/<name>-volumes-<timestamp>.tar.gz[.enc]

<name> is baktainer.name (or the database name), <timestamp> is a Unix timestamp. Compression adds .gz; encryption adds .enc.

Web dashboard

With BT_HEALTH_SERVER_ENABLED=true, the dashboard shows live backup status grouped by compose stack, per-node tabs, running backups, and a cluster overview. Every screen supports light and dark themes.

Drill into any container or stack for its full backup history, statistics, and one-click download / restore / delete:

Container backup history and restore

Health & API endpoints

Endpoint Description
GET / Interactive dashboard
GET /health Health check (200 healthy / 503 unhealthy)
GET /status System status and metrics (JSON)
GET /metrics Prometheus-format metrics
GET /config Effective configuration (secrets redacted)
GET /api/backups Backup history
POST /api/backups/run Trigger backups
GET /api/nodes Cluster node status (main only)

Prometheus

scrape_configs:
  - job_name: baktainer
    metrics_path: /metrics
    static_configs:
      - targets: ['baktainer:8080']

Thin-node cluster

Baktainer can span many Docker hosts. One main instance runs the dashboard, database, and coordinator; each node is a headless agent that backs up its own host and reports to main. This is the model described in ADR 0001.

How it works

  • Nodes dial main; main never dials a node. All communication is node-initiated, so nodes can sit behind NAT. Put TLS in front of main (a reverse proxy) — it's the only instance that needs to be reachable.
  • Nodes are headless. A node runs no web UI and no database — just the scheduler, backup engine, hooks, and a small signed HTTP client. It persists only its keypair identity and a local event spool.
  • Cryptographic identity + approval. On first boot a node generates an EC P-256 keypair and signs every request. Its identity is the fingerprint of its public key. An unknown node enrolls as pending and must be approved from the Nodes page (trust-on-first-use). Approve, revoke, or remove nodes at any time.
  • Instant browsing, live transfers. Nodes push backup events to main in real time (buffered in a spool that replays after any outage) and reconcile a full file manifest, so browsing a node's backups is instant. Downloads and restores are relayed live through main over the node's own connection.

Approving a node

Each node enrolls with a key fingerprint and first-seen address; an operator approves it from the dashboard.

Cluster nodes

Once approved, every instance appears in the fleet view with live health, disk usage, and last-backup status.

Node detail with backup file browser

Drill into any node for its status, disk, backup history, and a browsable file list with download / restore / delete.

Deploying a cluster

Main (dashboard + coordinator):

services:
  baktainer-main:
    image: ruby-code.com/james/baktainer:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./data:/data                                   # SQLite: state, history, node registry
      - ./backups:/backups
      # - /var/run/docker.sock:/var/run/docker.sock    # only if main also backs up local containers
    environment:
      - BT_CLUSTER_ROLE=main
      - BT_NODE_NAME=main
      - BT_HEALTH_SERVER_ENABLED=true
      - BT_AUTH_ENABLED=true
      # - BT_CLUSTER_JOIN_TOKEN=shared-secret          # optional enrollment gate

Node (headless worker on another host):

services:
  baktainer-node:
    image: ruby-code.com/james/baktainer:latest
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./backups:/backups
      - ./data:/data                                   # identity.json + event spool
    environment:
      - BT_CLUSTER_ROLE=node
      - BT_MAIN_URL=https://baktainer.example.com
      - BT_NODE_NAME=edge-1
      - BT_CRON=0 2 * * *
      # - BT_CLUSTER_JOIN_TOKEN=shared-secret          # must match main

A node has no published ports — it never accepts inbound connections. After it starts, open the main's Nodes page and approve it. To validate a cluster locally end-to-end, use the two-node harness: docker compose -f docker-compose.cluster.yml up --build (main UI on http://localhost:9080). See docs/thin-node-harness-runbook.md.

Authentication

Baktainer secures the dashboard and API with local users and/or OIDC. On first start you'll create an admin through a setup wizard.

First-time setup wizard   Login screen

Local users & roles

Passwords are hashed with Argon2id. Three roles are available:

Role Permissions
admin Full access: manage users, delete backups, change settings
operator Run and restore backups, view everything
viewer Read-only dashboard and history

Admins manage users from the Users page (or the /api/users REST endpoints).

User management

OIDC / SSO

environment:
  - BT_OIDC_ENABLED=true
  - BT_OIDC_ISSUER=https://your-idp.com
  - BT_OIDC_CLIENT_ID=baktainer
  - BT_OIDC_CLIENT_SECRET=your-secret
  - BT_OIDC_REDIRECT_URI=https://baktainer.example.com/auth/oidc/callback
  - BT_OIDC_ADMIN_GROUP=baktainer-admins

To disable authentication entirely (development / trusted networks only): BT_AUTH_ENABLED=false.

Database engines

PostgreSQL / MySQL / MariaDB

labels:
  - baktainer.backup=true
  - baktainer.db.engine=postgres      # or mysql, mariadb
  - baktainer.db.name=appdb           # or use baktainer.db.all=true
  - baktainer.db.user=app
  - baktainer.db.password=secret

SQLite

labels:
  - baktainer.backup=true
  - baktainer.db.engine=sqlite
  - baktainer.db.name=/data/app.db    # path to the database file inside the container

Redis

Backs up an RDB snapshot via redis-cli --rdb. Supports legacy password auth and ACL auth (Redis 6+), and a specific DB number (015).

labels:
  - baktainer.backup=true
  - baktainer.db.engine=redis
  - baktainer.db.password=mysecret    # optional
  # - baktainer.db.user=myuser        # optional, ACL auth
  # - baktainer.db.name=3             # optional, DB number (default 0)
  - baktainer.name=my-redis

MongoDB

Dumps a binary archive via mongodump --db <name> --archive. Credentials are optional (omit them for an unauthenticated instance).

labels:
  - baktainer.backup=true
  - baktainer.db.engine=mongodb
  - baktainer.db.name=appdb
  - baktainer.db.user=app        # optional
  - baktainer.db.password=secret # optional
  - baktainer.name=my-mongo

Volume backups

Back up Docker volumes and bind mounts — on their own, or alongside a database. Mounts are auto-detected; tmpfs mounts are skipped.

Volumes only:

labels:
  - baktainer.backup=true
  - baktainer.db.engine=volume
  - baktainer.volumes.backup=true
  - baktainer.name=my-app

Database + volumes (produces a <name> database artifact and a <name>-volumes archive):

labels:
  - baktainer.backup=true
  - baktainer.db.engine=mysql
  - baktainer.db.name=wordpress
  - baktainer.db.user=wp
  - baktainer.db.password=secret
  - baktainer.volumes.backup=true

Notifications

environment:
  - BT_NOTIFICATION_CHANNELS=slack,discord,log
  - BT_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
  - BT_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
  - BT_NOTIFY_FAILURES=true
  - BT_NOTIFY_SUCCESS=false

Channels: Slack, Discord, Microsoft Teams, generic webhook, and logs.

Encryption, compression & rotation

environment:
  - BT_ENCRYPTION_ENABLED=true
  - BT_ENCRYPTION_KEY=your-256-bit-hex-key   # or BT_ENCRYPTION_KEY_FILE / BT_ENCRYPTION_PASSPHRASE
  - BT_COMPRESS=true
  - BT_COMPRESSION_TYPE=gzip                  # or zstd
  - BT_ROTATION_ENABLED=true
  - BT_RETENTION_DAYS=30
  - BT_RETENTION_COUNT=0                       # per-container cap (0 = unlimited)
  - BT_MIN_FREE_SPACE_GB=10

Encrypted files use AES-256-GCM with authenticated integrity and a .enc extension. Rotation cleans up by age, per-container count, and low free disk, and prunes empty date directories.

Hooks

Place executable scripts in the hooks directory (default /hooks); Baktainer runs pre-backup.sh before and post-backup.sh after each backup.

Hooks receive context via environment variables: BAKTAINER_CONTAINER_NAME, BAKTAINER_BACKUP_FILE, BAKTAINER_BACKUP_SIZE, BAKTAINER_BACKUP_STATUS (success/failed), BAKTAINER_ENGINE, BAKTAINER_TIMESTAMP.

#!/bin/bash
# /hooks/post-backup.sh — sync successful backups to S3
if [ "$BAKTAINER_BACKUP_STATUS" = "success" ]; then
  aws s3 cp "$BAKTAINER_BACKUP_FILE" "s3://my-bucket/backups/"
fi
volumes:
  - ./hooks:/hooks
environment:
  - BT_HOOKS_ENABLED=true
  - BT_HOOKS_DIR=/hooks

Importing existing backups

If you have backups from older versions (with .meta files), import them so they appear in the dashboard with full history. The import is idempotent.

docker exec baktainer ruby app.rb --import-backups            # from /backups
docker exec baktainer ruby app.rb --import-backups /path      # from a specific directory

Backup history lives in the SQLite database at /data/baktainer.db — mount /data to a volume so it (and your users) persist across restarts.

Upgrading

From v0.1.x

v0.2.0 added persistent storage and authentication:

  1. Add a /data volume mount for the SQLite database.
  2. If you customized it, rename BT_AUTH_DB_PATHBT_DB_PATH.
  3. If upgrading from an auth preview, rename the DB file: mv ./data/baktainer_auth.db ./data/baktainer.db.

Existing backup files are imported automatically on first start.

Documentation

Development

A complete local development stack with sample databases is included:

docker compose -f docker-compose.dev.yml up --build      # dashboard on http://localhost:8080
docker compose -f docker-compose.dev.yml exec baktainer-dev ruby app.rb --dry-run

See DEVELOPMENT.md for details, and run the test suite with cd app && bundle exec rspec.

License

See LICENSE.