Skip to content

Backup & Restore

Three independent paths protect your install: an on-demand ZIP from the UI, a scheduled local-disk job that keeps the last N snapshots, and a Git push that archives printer profiles to GitHub or GitLab.


What's in a Backup ZIP

The on-demand and scheduled local backups produce the same ZIP layout. Top-level entries:

Entry Contents
bamdude.db Full database in portable SQLite format, including migration history. Works for restore onto SQLite or PostgreSQL.
archive/ Print files and thumbnails, one folder per run, plus the 3MF download staging.
library/ File Manager storage: files, thumbnails, MakerWorld covers.
virtual_printer/ Virtual-printer working files.
queue-sources/ Verified immutable copies owned by ready queued jobs. The ZIP includes exactly the files named by its database snapshot; unfinished staging/ files are excluded.
plate_calibration/ Plate-detection reference frames.
icons/ Custom icons.
projects/ Order attachments.
products/ Product attachments.
certs/ Virtual-printer TLS certificates and keys.
.mfa_encryption_key Encryption key, when the source install stores it in a file.
zigbee/zigbee.db Zigbee driver database, including network state, when present.
.install_id Existing anonymous telemetry identity, when present.
backup-manifest.json File sizes, SHA-256 checksums and the complete directory list, including empty directories.

The ZIP contains sensitive database values and key files. API response filtering does not remove secrets from this database backup. Keep it private. If the encryption key is supplied only through MFA_ENCRYPTION_KEY, configure the same key on the destination; an environment-only key is not added to the ZIP.

PostgreSQL export reads one consistent database snapshot and preserves column types, defaults, constraints, indexes and migration history. Archive search is rebuilt for the destination backend. SQLite backups include committed data still in its WAL. An export or ZIP-writing failure does not publish a partially written backup over an existing file.

The database snapshot does not freeze the accompanying directories. For a complete copy of files while they are changing, pause uploads, deletion and other file-changing work during backup. Logs, runtime caches, temporary files and the application itself are excluded. An unreadable file, a detected source change or a failed copy fails the backup; it never reports success with skipped files. This includes an existing but unreadable Zigbee database. The configured archive and plate-calibration paths are used in both directions. Symlinks, Windows junctions and special files are rejected rather than followed.


Manual Backup

  1. Settings → System → Backup & Restore
  2. Click Create Backup
  3. Browser downloads bamdude-backup-YYYYMMDD-HHMMSS.zip

The ZIP is streamed from a temp file rather than buffered in memory, so multi-gigabyte backups don't OOM the process. The temp file is deleted automatically once the response finishes.

API: GET /api/v1/settings/backup (requires settings:backup).


Scheduled Local Backups

Set under Settings → System → Local Backup Schedule. The scheduler ticks once per minute and fires due jobs into the same ZIP builder the manual button uses, then prunes older backups beyond the retention limit.

Setting Default Notes
local_backup_enabled false Master switch.
local_backup_schedule daily hourly, daily, or weekly.
local_backup_time 03:00 HH:MM for daily/weekly runs (server-local time). Hourly ignores this.
local_backup_retention 5 Keep the most recent N backups; older ones auto-prune. Range 1–100.
local_backup_path empty Output directory. Empty = data/backups/.

The settings page shows last-run timestamp + outcome (success / failed), the next scheduled run, and a list of currently retained backups with file sizes. Manual "Create Backup" runs are stored in the same directory and counted toward retention.

Legacy filenames from older BamDude backups remain supported. This does not provide migration from the separate upstream Bambuddy application.

When the output folder is not writable

BamDude probes the folder with a real write when you save the path and when the backup card opens, so an unusable path is caught there rather than at 03:00 for a week. The card then names the cause and gives you the fix with your own path already filled in.

The one that catches people out is the systemd sandbox. The service unit ships ProtectSystem=strict, which mounts everything outside ReadWritePaths=<install> <data> <logs> read-only inside the service's own mount namespace. A NAS share you mounted yourself and can write to from your shell is not one of those three, so the write fails with EROFS ("Read-only file system") — which looks like a permission problem and is not one. Reads are unaffected, so the UI happily lists existing backups from the share while being unable to write a new one.

Grant the service access with a drop-in, which also survives a reinstall:

sudo systemctl edit bamdude
[Service]
ReadWritePaths=/mnt/your-nas-share
sudo systemctl restart bamdude

Reinstalling backs the old unit up as bamdude.service.bak-<timestamp> and carries any extra ReadWritePaths forward, so a carve-out you added by hand no longer disappears on the next update.

In Docker the failure is quieter: a host path that was never bind-mounted is still writable — the write lands in the container's ephemeral layer and vanishes on the next docker compose up. BamDude compares the folder's device against the container root and warns, with the compose snippet that mounts it properly.


Git Backup (Profiles to GitHub / GitLab)

Distinct from the ZIP flow. Settings → System → Git Backup pushes selected printer-profile data to a GitHub or GitLab repository — useful for off-site profile sync, multi-host farm coordination, and PR-based change history of your printer settings.

Configuration

Setting Notes
Provider github, gitlab, gitea, or forgejo.
Repository URL Full clone URL (HTTPS form).
Access Token Personal Access Token. Stored encrypted at rest.
Branch Target branch (default main).
API base URL Self-hosted GitLab only.
Schedule hourly / daily / weekly, or off.

Provider setup walkthroughs

  1. Create a GitHub repository (private is fine).
  2. Generate a Personal Access Token (PAT):
    • Go to GitHub Personal Access Tokens.
    • Click Generate new token → Generate new token (classic).
    • Choose your expiration (No expiration is recommended for unattended scheduled backups).
    • Under Select scopes, check repo (required for repository access and commits).
  3. Configure in BamDude:
    • Settings → Backup & Restore → Git Backup.
    • Provider: github.
    • Repository URL: e.g. https://github.com/username/bamdude-backup.
    • Enter the PAT.
    • Click Test Connection.

Fine-grained tokens

Instead of classic tokens, you can use GitHub's fine-grained tokens. Grant Read access to Metadata — Read and Write access to code is automatically included on creation.

  1. Create a GitLab repository (private is fine).
  2. Generate a Personal Access Token:
    • Go to GitLab Personal Access Tokens.
    • Click Add new token (Legacy / classic shape).
    • Under scopes, check api (required for repository access and commits).
  3. Configure in BamDude:
    • Provider: gitlab.
    • For self-hosted GitLab, also fill API base URL.
    • Repository URL: e.g. https://gitlab.com/username/bamdude-backup.
    • Enter the PAT.
    • Click Test Connection.

Project Access Tokens

Project Access Tokens also work — grant the api and write_repository scopes, otherwise commits will fail with access errors.

  1. Create a new repository (private is fine).
  2. Generate a Personal Access Token:
    • Settings → Applications in your Gitea profile.
    • Under Access Tokens, name the token.
    • Set scope to All (public, private, and limited).
    • Select Read and write under repository permissions.
    • Click Generate token.
  3. Configure in BamDude:
    • Provider: gitea.
    • Repository URL: e.g. https://gitea.example.com/username/bamdude-backup (the URL validator accepts plain http:// for local self-hosted instances on the same shape).
    • Specify the correct Branch (main, master, etc.).
    • Enter the PAT.
    • Click Test Connection.

Instances hosted under a path prefix

If your Gitea sits under a ROOT_URL prefix rather than at the root of the server — https://your-server/gitea — paste the repository URL exactly as your browser shows it: https://your-server/gitea/username/bamdude-backup. The last two segments are read as owner and repository, and everything before them is kept, so the API is addressed at https://your-server/gitea/api/v1. Any depth of prefix works. Root-hosted instances are unchanged.

Path steps of . or .. are refused — they would point the backup at somewhere on the server you did not name.

Gitea-shape API divergences from GitHub (handled internally)

BamDude's GiteaBackend overrides three GitHub-incompatible response shapes Gitea introduced over time: list-shaped GET /git/refs/heads/{branch} response (single match still returns an array), the empty-repo Git Data API write refusal (every blob POST 404 until a commit exists — bootstrap routes through the Contents API in one transaction), and the wrapped Commit schema in Gitea 1.24+ (commit.tree.sha instead of GitHub's flat tree.sha). All transparent to operators — listed here only as a reference for self-hosted deployments noting Gitea version compatibility (1.18+ verified, 1.24+ verified).

  1. Create a new repository (private is fine).
  2. Generate a Personal Access Token:
    • Settings → Applications in your Forgejo profile.
    • Under Manage Access Tokens, name the token.
    • Click Generate Token.
  3. Configure in BamDude:
    • Provider: forgejo.
    • Repository URL: e.g. https://forgejo.example.com/username/bamdude-backup (plain http:// accepted on the same shape for local instances).
    • Enter the PAT.
    • Click Test Connection.

API-compatible with Gitea

Forgejo's API is currently /api/v1-compatible with Gitea, and BamDude's ForgejoBackend inherits all of GiteaBackend's behaviour — including the path-prefix handling described under Gitea, so https://your-server/forge/username/bamdude-backup works the same way. If the two projects diverge in a future Forgejo release, override-by-override patches in forgejo.py will surface here.

Bambu Cloud login required for K-profiles + Cloud profiles

Backing up Cloud profiles and K-profiles requires an active Bambu Cloud login. Sign in via Profiles → Cloud Profiles before scheduling a Git backup that includes those categories — otherwise the relevant directories will be empty in the repo.

What gets pushed

Toggle each independently. Defaults are tuned for "back up the things most operators want, leave the noisy/large things off":

Category Description Default
K-profiles Per-printer pressure-advance profiles (organized by serial number). On
Cloud profiles Filament, printer, and process profiles from Bambu Cloud. On
Spools Full inventory dump (rows + usage history). On
Archives (metadata) Print history metadata — filament, temperatures, times, costs, energy (no 3MF / no thumbnails). On
App settings Application settings table (sensitive fields excluded). Off
Archives (3MF + thumbnails) Bulk 3MF + thumbnail file content — bumps repo size by ~50–500 MB per 100 prints. Off

Only changed files generate commits — a no-op run is recorded as skipped.

:material-folder-tree: Repository structure

After a successful run, the repo looks like:

repo/
├── backup_metadata.json
├── kprofiles/
│   └── {serial_number}/
│       ├── 0.2.json
│       ├── 0.4.json
│       └── ...
├── cloud_profiles/
│   ├── filament.json
│   ├── printer.json
│   └── process.json
├── settings/
│   └── app_settings.json
├── spools/
│   ├── inventory.json
│   └── usage_history.json
└── archives/
    └── print_history.json

The flat structure makes partial restore unambiguous — you can pull just kprofiles/{serial}/ for one printer, or just spools/inventory.json for inventory recovery, without touching the rest.

Status panel

The settings page shows the live status:

  • Last backup — timestamp, status (success / failed / skipped), commit SHA, and message.
  • Next scheduled run — when the scheduler will fire next.
  • Log table — historical runs with trigger (manual / scheduled), duration, and any error message.
  • Run Now button — fires an immediate push regardless of schedule.

Push frequency, content checkboxes, and credentials can all be edited live without restarting BamDude.


Restoring a Backup ZIP

  1. Keep a backup of the current installation if you may need to return to it. Use the same or a newer BamDude version than the one that created the backup.
  2. While BamDude is running, open Settings → System → Restore and upload the ZIP. Placing a ZIP in the data directory does not start a restore automatically.
  3. BamDude checks ZIP paths, CRC, the manifest (when present), the application database and any included Zigbee database before stopping background services. Unsafe entries, corrupt or missing files, missing application tables and unknown migration versions are rejected before changing live data.
  4. Restore prepares and verifies all incoming files on their destination filesystems. It retains the old contents, replaces the files, then replaces the database. Directory roots and Docker mount points stay in place. Pending migrations run after the replacement; recorded migrations are not replayed.
  5. Restart BamDude after restore, then verify printers, archive search, File Manager files and sign-in. If restore failed after services were paused, restart before returning to normal work as well.

A preparation or file-replacement failure aborts before the database swap. If replacement of the database fails, all replaced directories, the MFA key, telemetry identity and Zigbee database/sidecars are rolled back. PostgreSQL schema, rows, sequences and foreign keys still use one database transaction. Zigbee is shut down before its database is replaced; an unsuccessful shutdown aborts restore.

New backups preserve empty directories: restoring one removes old contents there. Older ZIPs without a manifest remain supported; a directory or optional file absent from an older ZIP leaves the destination unchanged. Manual backups, scheduled backups and restores cannot overlap in one running BamDude process (API returns HTTP 409 for a second request).

Allow space for the extracted ZIP in the system temporary directory and for the incoming files alongside the existing files on every destination volume. Old contents are retained until the database replacement succeeds. If a file lock or external write also prevents rollback, BamDude reports failure and retains recovery copies in .bamdude-restore-* directories named in server logs: keep them for recovery. A failure to remove staging after a successful restore is logged and leaves the restored files usable.

Perform restore during a maintenance window, without uploads or other file-changing work. This is a reversible file replacement, not an operating-system snapshot or a distributed transaction: power loss, external writers and loss of the database connection during commit can still require recovery. A later migration failure keeps the newly restored database and its matching files together.

API: POST /api/v1/settings/restore (multipart file=…, requires settings:restore).

Cross-backend restore

The portable SQLite dump means you can:

  • Take a backup from a SQLite install → restore onto PostgreSQL (the loader migrates rows).
  • Take a backup from a PostgreSQL install → restore onto SQLite (DB was already exported as SQLite).
  • Take a backup from PG → restore onto a fresh PG (loader re-imports SQLite into PG).

Restore replaces the destination data; it does not merge databases or silently skip conflicting rows. Schema mismatches during export fail explicitly rather than omitting unknown data. Legacy BamDude SQLite backups retain their own schema and migration level during PostgreSQL import.


Bulk archive export

The Backup ZIP already includes files stored under archive/. To export only selected print archives instead of the installation database:

  1. Go to Archives.
  2. Click Export.
  3. Tick Include 3MF files in the export modal.
  4. Optionally narrow by date range, printer, or status.
  5. Download the resulting ZIP.

Useful for hand-off to another print farm, archival into cold storage, or one-time migration without dragging the full database along.


Manual SQLite / PostgreSQL backup

If you want a CLI / scripted backup outside BamDude's UI flow — e.g. for inclusion in a wider system backup, or PostgreSQL-specific point-in-time recovery — go directly to the database engine:

Stop BamDude first to ensure a consistent snapshot, then:

# Plain copy (fastest)
cp /path/to/bamdude.db bamdude_$(date +%Y%m%d).db

# SQL dump (portable across versions)
sqlite3 /path/to/bamdude.db ".dump" > bamdude.sql

# Restore from SQL dump
sqlite3 new_bamdude.db < bamdude.sql

Connect using your DATABASE_URL:

# Custom-format dump (recommended — supports parallel restore + selective restore)
pg_dump -Fc bamdude > bamdude.backup
# or with explicit DSN:
pg_dump -Fc "postgresql://user:pass@host:5432/bamdude" > bamdude.backup

# Restore (drops + recreates objects on import)
pg_restore -d bamdude bamdude.backup
# or with explicit DSN:
pg_restore --clean --if-exists \
    -d "postgresql://user:pass@host:5432/bamdude" bamdude.backup

BamDude's built-in backup is easier

The Settings → Backup page produces portable backups that work across both SQLite and PostgreSQL. Use manual pg_dump only when you need PostgreSQL-specific features like point-in-time recovery, logical-replication snapshotting, or integration with an existing PG backup pipeline.

Stop BamDude before raw file copy

Direct cp of bamdude.db while BamDude is running can capture an inconsistent WAL state. The portable Settings → Backup flow handles this safely — manual file copy needs the process stopped first.


Recovery scenarios

Three common shapes the recovery flow takes:

Lost database

DB is corrupted, deleted, or otherwise unrecoverable:

  1. Stop BamDude.
  2. Remove the corrupted bamdude.db (or drop the PostgreSQL database).
  3. Start BamDude — it creates a fresh empty DB on first boot.
  4. Settings → System → Restore → upload your latest backup ZIP.
  5. BamDude replaces the empty DB with the restored one and runs pending migrations.

New installation

Moving to a new server / new Docker host:

  1. Install BamDude on the new host (Docker compose, bare metal, whichever).
  2. Boot once so the data directory is created and the setup-gate is sitting on setup_required.
  3. Copy your backup ZIP onto the new host.
  4. Settings → System → Restore → upload the ZIP — note the same setup-gate whitelists /restore-style flow when no admin exists yet, but in practice the easiest path is to complete setup with a placeholder admin first, then restore (which replaces the placeholder with your real users).

Data migration

Migrating between database backends, between OS hosts, or moving Docker volumes:

  1. Take a backup on the old install (Settings → Backup → Create Backup).
  2. Stand up BamDude on the new host.
  3. Restore from the backup ZIP — BamDude's portable SQLite layer translates SQLite ↔ PostgreSQL automatically (see "Cross-backend restore" above).
  4. Verify printers reconnect, profiles are present, archives load. Then decommission the old host.

Backup file size guidance

Rough sizing so you can plan storage:

Profile Approximate size Contents
Small < 50 MB DB only — no archives, no 3MFs, no library files.
Medium 100–500 MB DB + archive metadata + thumbnails (no 3MFs).
Large 1–50 GB DB + full 3MF + thumbnails + library files + timelapses.

If you have many timelapse videos, large profile is the right model — periodic cleanup of old timelapses (or excluding archive/ from a separate full-data backup) is the easiest way to keep the ZIP manageable.


Best practices

  • Daily for production — combine Scheduled Local Backups with daily frequency (e.g. 03:00) and retention=7 to keep a rolling week.
  • Off-site at least one — store one snapshot somewhere not on the BamDude host: NAS share, cloud storage (Dropbox / Google Drive / S3 via rclone), or an external USB drive that's rotated weekly. Hardware loss only hurts you when both copies are on the same hardware.
  • Periodic restore drill — every few months, take a backup ZIP and try restoring it onto a throwaway BamDude install. A backup you've never restored is a backup that might not work.
  • Backup before upgrade — the UPDATING.md protocol recommends a fresh manual backup before every minor-version upgrade. Migrations are idempotent and one-shot but a downgrade has no automatic path.
  • Date-suffix manual backups — when grabbing a manual backup before a risky change, name it for what triggered it (bamdude-pre-0.5.0-upgrade.zip) so you find it later.

Docker volume bind-mount example

For Docker users, mount the backup output directory as a volume so backups persist outside the container — and ideally onto a NAS share for off-site coverage.

The path is fixed — the mount is the knob

Backups are always written to backups/ inside the data directory. There is no environment variable that moves them: point the volume at /app/data/backups and mount whatever host path you like on the other side.

services:
  bamdude:
    image: ghcr.io/kainpl/bamdude:latest
    container_name: bamdude
    network_mode: host
    volumes:
      - bamdude_data:/app/data
      - bamdude_logs:/app/logs
      - ./backups:/app/data/backups          # local relative path
      # or
      - /mnt/nas/bamdude-backups:/app/data/backups   # NAS / network share
    environment:
      - TZ=Europe/Kyiv
    restart: unless-stopped

volumes:
  bamdude_data:
  bamdude_logs:

Or with docker run:

docker run -d \
  --network host \
  -v bamdude_data:/app/data \
  -v bamdude_logs:/app/logs \
  -v /mnt/nas/bamdude-backups:/app/data/backups \
  -e TZ=Europe/Kyiv \
  --name bamdude \
  --restart unless-stopped \
  ghcr.io/kainpl/bamdude:latest

BACKUP_DIR overrides the default data/backups/ path inside the container — use it when your bind mount lands somewhere other than /app/data/backups.

NAS / Samba / NFS

Point the bind mount at a NAS share, Samba mount, or NFS path for automatic off-site backups without any extra scripts. Combined with the retention-based rotation, you get a hands-off off-site backup pipeline.


Tips

Off-site coverage

Combine Scheduled Local Backups (full data, on-disk) with Git Backup (profiles, off-site) — the local one survives a software wipe, the git one survives a hardware loss.

Backup before upgrade

UPDATING.md recommends a fresh manual backup before every minor-version upgrade. Migrations are idempotent and one-shot but a downgrade has no automatic path.

Originally based on Bambuddy documentation.