Configuration
Generate the backup config
Run:
bundle exec kamal-backup init
init creates config/kamal-backup.yml if it is missing, then prints the accessory block to add to config/deploy.yml. It does not edit config/deploy.yml, and it does not create config/kamal-backup.local.yml.
The generated backup config looks like this:
app: your-app
accessory: backup
databases:
- name: app
adapter: postgres
url: postgres://your-app@your-db:5432/your_app_production
password:
secret: DATABASE_PASSWORD
paths:
- /data/storage
restic:
repository: s3:https://s3.example.com/your-app-backups
password:
secret: RESTIC_PASSWORD
init_if_missing: true
backup:
schedule: 1d
Edit that file for production. It is the main backup configuration: app name, database sources, restic repository, file paths, and schedule.
File backups are opt-in. kamal-backup snapshots files only when paths is explicitly configured; it never infers storage or another file path from Rails. Omit paths (or set paths: []) for a database-only backup.
kamal-backup.yml uses the grouped shape shown above. Older flat YAML keys such as database_adapter, backup_paths, and restic_repository are rejected so configuration stays explicit. See Upgrading when moving from 0.2.
Default options
accessory: the Kamal accessory name. The default isbackup.app: the app tag used on restic snapshots.databases: one or more PostgreSQL, MySQL/MariaDB, or SQLite databases to back up.paths: optional filesystem paths to snapshot from mounted volumes. Entries can be strings or mappings withpathandexclude. No files are backed up when this is omitted or empty.restic.repository: the restic repository location, such as S3-compatible storage, SFTP, rclone, a restic REST server, or a filesystem path.restic.password.secret: the Kamal secret env var that contains the restic password.restic.rest.usernameandrestic.rest.password: optional restic REST server credentials. These becomeRESTIC_REST_USERNAMEandRESTIC_REST_PASSWORD.restic.init_if_missing: runrestic initwhen the repository has not been initialized yet.backup.schedule: how often the accessory runs backups.1dmeans once per day.backup.enabled: whether the accessory takes scheduled backups. Defaults totrue. Set it tofalseto bring an accessory up that can restore, list and check, but will never write a backup of its own. See Migrating to a new host.
For MySQL, change the database settings:
databases:
- name: app
adapter: mysql
url: mysql2://app@app-mysql:3306/app_production
password:
secret: DATABASE_PASSWORD
For SQLite, point at the database file inside the accessory:
databases:
- name: app
adapter: sqlite
path: /data/storage/production.sqlite3
That path should be the live SQLite database file as mounted into the backup accessory. The SQLite adapter creates its own temporary backup file before sending it to restic.
When a configured SQLite database file lives under a configured file backup path, kamal-backup automatically excludes that database file plus its -wal and -shm sidecar files from the restic file snapshot. The SQLite database backup still runs separately through sqlite3 .backup.
You can also declare path-level excludes explicitly. These only apply to the restic file backup, not to database dump backups:
databases:
- name: app
adapter: sqlite
path: /rails/storage/production.sqlite3
paths:
- path: /rails/storage
exclude:
- /rails/storage/*.sqlite3
- /rails/storage/*.sqlite3-wal
- /rails/storage/*.sqlite3-shm
For a live SQLite database in WAL mode, mount the storage volume read-write in the backup accessory so SQLite can open the database, WAL, and shared-memory files normally:
volumes:
- "your_app_storage:/data/storage"
- "your_app_backup_state:/var/lib/kamal-backup"
If you require the backup accessory to have no write access to app storage, do not point it at a live WAL database over a read-only mount. Have the writer create a WAL-less snapshot, then point the SQLite database path at that snapshot. That is an advanced hardening tradeoff, not the normal SQLite setup.
Add the accessory
Copy the accessory block printed by init into your Kamal deploy config, then mount the generated backup config with files:.
accessories:
backup:
image: ghcr.io/crmne/kamal-backup:latest
host: chatwithwork.com
files:
- config/kamal-backup.yml:/app/config/kamal-backup.yml:ro
env:
secret:
- DATABASE_PASSWORD
- RESTIC_PASSWORD
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
volumes:
- "chatwithwork_storage:/data/storage"
- "chatwithwork_backup_state:/var/lib/kamal-backup"
The files: line is what keeps non-secret backup settings out of environment variables. Kamal uploads config/kamal-backup.yml and mounts it read-only into the accessory.
The storage volume is writable so production file restores and live SQLite WAL backups can work. For a backup-only
accessory, append :ro; remove it and reboot the accessory before running restore production.
Secrets
Keep secrets in Kamal secrets:
RESTIC_PASSWORD=...
DATABASE_PASSWORD=...
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
If the repository URL contains credentials, declare it as a secret reference instead:
restic:
repository:
secret: RESTIC_REPOSITORY
For a restic REST server, prefer keeping credentials out of the repository URL:
restic:
repository: rest:https://backup.example.com/prod
rest:
username:
secret: RESTIC_REST_USER
password:
secret: RESTIC_REST_PASSWORD
Repository backends
The accessory passes restic’s supported backend environment variables through to restic. Choose the repository prefix and supply its credentials as Kamal secrets:
| Repository | Prefix or form | Typical configuration |
|---|---|---|
| Filesystem | /path/to/repository |
Mount persistent storage at that path. |
| REST server | rest:https://… |
RESTIC_REST_USERNAME, RESTIC_REST_PASSWORD |
| S3-compatible | s3:https://… or s3:region:bucket |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
| SFTP | sftp:user@host:/path |
Dedicated SSH key and verified known_hosts |
| Backblaze B2 | b2:bucket:path |
B2_ACCOUNT_ID, B2_ACCOUNT_KEY |
| Azure Blob Storage | azure:container:/path |
The applicable AZURE_* variables |
| Google Cloud Storage | gs:bucket:/path |
The applicable GOOGLE_* variables |
| OpenStack Swift | swift:container:/path |
The applicable OS_*, ST_*, or HP_* variables |
| rclone | rclone:remote:path |
An rclone config file or RCLONE_CONFIG_<REMOTE>_* variables |
Consult restic’s repository guide
for the backend-specific variables. Never put credentials directly in config/kamal-backup.yml.
Rclone repositories
The accessory includes rclone, and restic officially supports using it to reach services outside restic’s native
backends. Once a remote named archive works in rclone, use it directly:
restic:
repository: rclone:archive:kamal-backup/your-app
password:
secret: RESTIC_PASSWORD
For an existing rclone config, mount only that file and tell rclone where it is:
accessories:
backup:
env:
clear:
RCLONE_CONFIG: /run/kamal-backup/rclone.conf
secret:
- RCLONE_CONFIG_PASS
volumes:
- "/etc/kamal-backup/rclone.conf:/run/kamal-backup/rclone.conf:ro"
RCLONE_CONFIG_PASS is needed only when the config file is encrypted. Keep the config file outside the app
repository when it contains tokens or credentials. You can instead configure a remote entirely through Kamal env,
using rclone’s RCLONE_CONFIG_<REMOTE>_TYPE and backend-specific variables; kamal-backup passes every
RCLONE_* variable through to restic and redacts secret values in its own output.
Restic starts and stops rclone serve restic --stdio itself—do not run a separate rclone daemon. See restic’s
rclone backend documentation
and the official rclone backend announcement.
SFTP repositories
The accessory image includes an SSH client, so restic can use an SFTP repository:
restic:
repository: sftp:backup@backups.example.com:/srv/restic/your-app
password:
secret: RESTIC_PASSWORD
Restic’s SFTP backend needs non-interactive SSH authentication. Create a dedicated key for this accessory, authorize
its public key for a restricted account on the repository host, and bind-mount only that private key and a verified
known_hosts file:
accessories:
backup:
volumes:
- "/etc/kamal-backup/ssh/id_ed25519:/root/.ssh/id_ed25519:ro"
- "/etc/kamal-backup/ssh/known_hosts:/root/.ssh/known_hosts:ro"
Create those files on every Kamal host that can run the accessory, and verify the repository host’s fingerprint before
adding it to known_hosts. Do not mount the Kamal host’s entire .ssh directory: a dedicated key limits what a
compromised accessory can reach. SSH authentication setup and key rotation remain the operator’s responsibility.
RESTIC_PASSWORD encrypts the restic repository; it is not the SSH account password. Password-prompt authentication
is unsuitable for unattended backups, so the image does not include sshpass.
If you do not want the restic password value in the process environment, point restic at a mounted file instead:
restic:
password:
file: /run/secrets/restic-password
The same works for the repository string when needed:
restic:
repository_file: /run/secrets/restic-repository
Validate before boot
Run this before booting or rebooting the accessory:
bundle exec kamal-backup validate
With a normal config/deploy.yml, validate checks the backup accessory config before the accessory has to be running. It catches missing app, database, and restic settings, invalid configured backup paths, and required Kamal secrets that resolve to empty values.
Local restores
restore local and drill local infer:
- production source settings from
config/kamal-backup.yml - local database settings from
config/database.yml - local state under
tmp/kamal-backup
File paths are never inferred. If production paths are configured, add config/kamal-backup.local.yml with the local targets in the same order. Database-only backups do not need local file paths.
databases:
- name: app
adapter: postgres
url: postgres://localhost/chatwithwork_development
paths:
- storage
state:
path: tmp/kamal-backup
Useful options
These options are supported but not included in the generated default config:
restic:
check_after_backup: true
check_read_data_subset: 5%
forget_after_backup: true
retention:
keep_last: 7
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
keep_yearly: 2
restic.forget_after_backup defaults to enabled unless explicitly set to a falsey value such as false, 0, no, n, or off.
You can also apply the same retention policy manually with kamal-backup prune. Pruning runs restic forget --prune for each configured database and file snapshot group, so database snapshots and file snapshots retain their own history. If a stale lock blocks prune, run kamal-backup unlock, then retry.
Environment variables can still override YAML values when you need an emergency override, but the clean setup is YAML for configuration and Kamal secrets for secrets.