Skip to content

Docker Installation

Docker is the easiest way to run BamDude. One command and you're done.


🚀 Quick Start

mkdir bamdude && cd bamdude
curl -O https://raw.githubusercontent.com/kainpl/bamdude/main/docker-compose.yml
docker compose up -d
git clone https://github.com/kainpl/bamdude.git
cd bamdude
docker compose up -d --build

Open http://localhost:8000 in your browser.


Configuration

services:
  bamdude:
    image: ghcr.io/kainpl/bamdude:latest
    build: .
    container_name: bamdude
    # Drop volume permission warnings: match the host user that owns
    # /var/lib/docker/volumes — usually 1000:1000 on Debian / Ubuntu.
    # Discover with: id -u && id -g
    user: "${PUID:-1000}:${PGID:-1000}"
    # Allow binding to privileged ports (322 RTSP, 990 FTPS) as non-root.
    cap_add:
      - NET_BIND_SERVICE
    # Linux only — Docker Desktop on macOS / Windows doesn't support host mode.
    # On those, comment this out and use the bridge-mode block below instead.
    network_mode: host
    volumes:
      - bamdude_data:/app/data
      - bamdude_logs:/app/logs
      # Share virtual-printer certs with a parallel native install if you have one.
      - ./virtual_printer:/app/data/virtual_printer
    environment:
      - TZ=${TZ:-Europe/Kyiv}
      - PORT=${PORT:-8000}
    restart: unless-stopped

volumes:
  bamdude_data:
  bamdude_logs:

docker-compose.yml (bridge mode — macOS / Windows / strict networking)

Docker Desktop on macOS / Windows doesn't support network_mode: host, and some hardened Linux setups prefer not to use it either. With bridge mode you have to map every port the printer talks to and every port the virtual printer listens on. Auto-discovery of physical printers stops working — add them manually by IP from the UI.

services:
  bamdude:
    image: ghcr.io/kainpl/bamdude:latest
    container_name: bamdude
    user: "${PUID:-1000}:${PGID:-1000}"
    cap_add:
      - NET_BIND_SERVICE
    ports:
      - "${PORT:-8000}:8000"            # Web UI + REST + WebSocket
      - "322:322"                        # Virtual-printer RTSP camera proxy
      - "990:990"                        # Virtual-printer FTPS control
      - "3000:3000"                      # Virtual-printer bind/detect
      - "3002:3002"                      # Virtual-printer bind/detect alt
      - "6000:6000"                      # Virtual-printer file tunnel
      - "8883:8883"                      # Virtual-printer MQTT
      - "2024-2026:2024-2026"            # Virtual-printer A1 / P1S range
      - "50000-50100:50000-50100"        # Virtual-printer FTP PASV data
    volumes:
      - bamdude_data:/app/data
      - bamdude_logs:/app/logs
    environment:
      - TZ=${TZ:-Europe/Kyiv}
      - PORT=${PORT:-8000}
      # Required for FTP PASV to work behind NAT — set to the Docker host's
      # LAN IP. The slicer needs this to open the data connection.
      - VIRTUAL_PRINTER_PASV_ADDRESS=${VIRTUAL_PRINTER_PASV_ADDRESS:-}
    restart: unless-stopped

volumes:
  bamdude_data:
  bamdude_logs:

Environment Variables

Variable Default Description
TZ UTC Your timezone (e.g., America/New_York)
PORT 8000 Port BamDude runs on
DEBUG false Enable debug logging
LOG_LEVEL INFO Log level: DEBUG, INFO, WARNING, ERROR
LOG_TO_FILE true Write logs to /app/logs/bamdude.log
DATABASE_URL unset (SQLite) A URL such as postgresql+asyncpg://user:pass@host:5432/bamdude uses your own server; for a separate PostgreSQL container use the shipped docker-compose.postgres.yml override — mind the host-networking note in PostgreSQL Support. embedded (the PostgreSQL bundled with BamDude) is not available in Docker: the image runs as root and initdb refuses to run as root.
EMBEDDED_PG_PORT picked once, remembered Pin the bundled server's port (e.g. 6432).
TRUSTED_PROXY_IPS empty Comma-separated reverse-proxy IPs trusted for X-Forwarded-For (set this when fronting BamDude with nginx / Caddy / Traefik)
AUTH_REFRESH_COOKIE_SECURE unset (auto) Force the refresh-cookie Secure flag. Auto-detect from request scheme by default.
MFA_ENCRYPTION_KEY unset URL-safe base64 Fernet key for at-rest encryption of TOTP / OIDC secrets.
APP_URL http://localhost:5173 Public-facing base URL — used in password-reset / MFA emails, OIDC callbacks, and the Obico cached-frame URL. The external_url setting in Settings → System overrides this.
PUID / PGID 1000 / 1000 UID / GID the container runs as. Match the owner of your mounted volumes to avoid permission errors.
VIRTUAL_PRINTER_PASV_ADDRESS unset Override the FTP-PASV IP advertised by the virtual printer. Required in bridge mode (set to the Docker host's LAN IP); leave unset in host-mode.
JWT_SECRET_KEY auto-generated, persisted Don't change on a running install -- it invalidates all issued tokens.
USE_SYSTEM_TRUST_STORE unset (off) Opt-in. Set to any non-empty value (e.g. true) to make the container trust self-signed certificates mounted into /usr/local/share/ca-certificates. See Trusting a self-signed certificate below.

See Installation > Environment Variables for the full list including optional integrations.

Trusting a self-signed certificate

Some integrations live on HTTPS endpoints with self-signed certificates — most commonly a local Home Assistant instance, but the same applies to OIDC providers or any HTTPS client BamDude talks to. Rather than disabling TLS verification (which would weaken every connection), BamDude can add your own certificate(s) to the container's trust store.

  1. Mount the host directory holding your .crt file(s) into /usr/local/share/ca-certificates
  2. Set USE_SYSTEM_TRUST_STORE=true

On container start the entrypoint runs update-ca-certificates --fresh and exports SSL_CERT_DIR=/etc/ssl/certs, so the whole Python stack (Home Assistant integration, OIDC, any HTTPS client) trusts the certificate from then on.

services:
  bamdude:
    image: ghcr.io/kainpl/bamdude:latest
    container_name: bamdude
    network_mode: host
    volumes:
      - bamdude_data:/app/data
      - bamdude_logs:/app/logs
      # Drop your self-signed .crt file(s) into this host directory.
      - /path/to/certs:/usr/local/share/ca-certificates
    environment:
      - TZ=${TZ:-Europe/Kyiv}
      - USE_SYSTEM_TRUST_STORE=true
    restart: unless-stopped

volumes:
  bamdude_data:
  bamdude_logs:

Fails loudly when misconfigured

The flag is off by default. When you set it but mount no .crt files, the container exits with an error rather than starting silently — an empty mount would otherwise look like the flag did nothing. It also requires a root container: if you've set a non-root user: / PUID/PGID, the entrypoint can't write to the system trust store and exits with an error. Run the trust-store update as root (the default) — the entrypoint still drops privileges afterwards for the app itself.


Data Persistence

The shipped compose file mounts two named volumes plus a host-bound subdir:

Mount Type Holds
bamdude_data:/app/data named volume bamdude.db (SQLite), archive/ (3MFs + thumbnails), library/ (file manager), certs/ (per-VP TLS material), uploads, backups
bamdude_logs:/app/logs named volume bamdude.log rotated app logs
./virtual_printer:/app/data/virtual_printer bind-mount Per-VP slicer certificates (shared with a parallel native install if you have one)

Docker Compose v2 prefixes named volumes with the project name (the basename of the directory the compose file lives in), so the actual volume on disk is e.g. bamdude_bamdude_data, not bamdude_data. List all of them with docker volume ls.

Backup

To back up your data, copy the volume contents (or use the built-in Backup & Restore feature in Settings → Backup, which packages everything into a single zip). The application-level backup is preferred — it captures encryption-key metadata and scheduled-backup state that a raw tar of the volume leaves behind.

Renaming the compose folder = new empty volumes

If you upgrade by renaming ~/bamdude to ~/bamdude-old and unpacking a fresh checkout in its place, Docker Compose creates a fresh, empty bamdude_bamdude_data and your real data sits in the old bamdude-old_bamdude_data volume. This is the most common cause of "new container started empty after upgrade". See Upgrading & Migration → Data persistence for every scenario and its fix (named-volume namespacing, bind-mount path drift, container-layer-only data, PUID/PGID mismatch, accidental down -v, GUI-manager namespacing).


Updating

docker compose pull && docker compose up -d
cd bamdude && git pull && docker compose build --pull && docker compose up -d

Advanced Setups

Reverse Proxy (Nginx)

server {
    listen 443 ssl http2;
    server_name bamdude.yourdomain.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://localhost:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 86400;
    }
}

WebSocket Support

Make sure your reverse proxy supports WebSocket connections -- required for real-time printer updates.

Network Mode Host

Host network mode is required for printer discovery and camera streaming on Linux:

services:
  bamdude:
    network_mode: host

macOS / Windows

Docker Desktop on macOS and Windows requires port mapping instead of host mode. Copy the bridge-mode compose block above — mapping just ports: ["8000:8000"] is enough for the web UI but breaks printer discovery, the virtual printer, and FTP archive downloads. Add physical printers manually by IP from the UI.

DEBUG=true on first boot of a big install

Setting DEBUG=true causes BamDude to re-run the latest migration on every boot. With several thousand archives that means walking every 3MF on disk before the API comes up — startup goes from seconds to minutes. Switch DEBUG off after the migration cycle settles.

Signal notifications sidecar

The Signal CLI API notification provider talks to a signal-cli-rest-api server. If you do not run one already, the shipped docker-compose.signal.yml override adds it next to BamDude; docker-install.sh offers it as a question (or --signal). By hand, list the override in .env:

COMPOSE_FILE=docker-compose.yml:docker-compose.signal.yml
SIGNAL_API_PORT=8081

(With the PostgreSQL sidecar too, list all three files.) Then docker compose up -d. Two things remain yours to do:

  1. Link a number. Open http://127.0.0.1:8081/v1/qrcodelink?device_name=BamDude in a browser on the server (from another machine: ssh -L 8081:127.0.0.1:8081 user@server, then the same URL on your laptop) and scan the QR code in Signal → Settings → Linked devices. Registering a brand-new number is possible too, but needs SMS/voice verification and usually a captcha — see Notifications.
  2. Create the provider under Settings → Notifications → Add → Signal CLI API, with the linked number as the sender and the URL for your platform:
Platform Signal API URL
Linux, host networking (the default) http://127.0.0.1:8081
Docker Desktop, bridge networking http://signal-api:8080

Loopback only

signal-cli-rest-api has no authentication of its own — whoever reaches the port can send from your number. The override publishes it on 127.0.0.1 only; do not change that to 0.0.0.0. The loopback publish is kept on Docker Desktop as well, because your browser needs it for the QR page.

The account's keys live in the bamdude_signal volume and are not part of BamDude's backup — lose the volume and you link again. The sidecar runs signal-cli in its json-rpc mode (one long-lived daemon, about 160 MB resident before an account is linked) and is pinned to a release rather than latest, because a signal-cli upgrade can require re-linking.


Troubleshooting

Container Won't Start

docker compose logs bamdude

Can't Connect to Printer

docker compose exec bamdude ping YOUR_PRINTER_IP

If using bridge network mode, try network_mode: host.


🏁 Next Steps

Originally based on Bambuddy documentation.