Add the Tailscale CLI to the production image and document how to
enable Let's Encrypt cert provisioning for virtual printers from a
Docker-deployed Bambuddy.
- Dockerfile installs `tailscale` from the official Debian repo. Only
the CLI is used at runtime; tailscaled itself stays on the host.
The binary is harmless if the socket isn't mounted — the code logs
an actionable hint and falls back to self-signed certs.
- docker-compose.yml adds a commented-out volume mount for
/var/run/tailscale/tailscaled.sock with inline setup instructions.
- tailscale.py's docker-socket hint now also fires when the binary is
present but the daemon socket is unreachable (i.e. the new Docker
pattern), not just when the binary is missing, so users get the
actionable "mount the socket" message instead of opaque CLI stderr.
Enabling the integration on a Docker host:
1. `curl -fsSL https://tailscale.com/install.sh | sh` on host
2. `sudo tailscale up`
3. `sudo tailscale set --operator=<user>` for the container PUID
4. Uncomment the tailscaled.sock mount in docker-compose.yml
5. `docker compose up -d --force-recreate`
6. Flip the Tailscale toggle on the VP card
Picks up ffmpeg 5 → 7 (HEVC/AV1 improvements), OpenSSL 3.0 → 3.3, and
two more years of APT package freshness. Frontend-builder stays on
Bookworm until the Node.js image team publishes Trixie variants.
The SpoolBuddy remote-update flow always pulled `main` on the remote
device when Bambuddy ran under Docker, regardless of which branch the
image was built from. Root cause: the Dockerfile COPYs only backend/
and static/, and .dockerignore excluded .git entirely, so the container
had no git metadata anywhere. detect_current_branch() silently fell
through its file-read path and returned the GIT_BRANCH env-var default
of "main".
The old subprocess-based implementation had the same bug but it was
masked twice: no .git in the image AND no `git` binary in the image,
so git rev-parse raised FileNotFoundError, the bare except swallowed
it, and the fallback kicked in.
Let the one file we actually need (.git/HEAD — ~20 bytes containing
`ref: refs/heads/<branch>`) through the .dockerignore filter and COPY
it into the image at /app/.git/HEAD. detect_current_branch() already
reads exactly that path, so no Python code changes are needed. Bind-
mount development setups are unaffected — the bind mount overlays the
baked-in file with the live repo's .git/HEAD.
Verified with a throwaway alpine build using the same .dockerignore
pattern: .git/HEAD passes through, decoy .git/refs and .git/objects
entries are excluded, and COPY writes the expected content into the
image.
Follow-up to the asyncssh migration. asyncssh.connect() internally
calls getpass.getuser() for ~/.ssh/config host matching, regardless
of the explicit `username=` passed for the remote login. Under an
arbitrary Docker PUID with no /etc/passwd entry, getpass.getuser()
raises "No username set in the environment" (OSError in Python 3.13+,
previously a bare KeyError).
Fix: set LOGNAME=bambuddy, USER=bambuddy, HOME=/app in the Dockerfile.
getpass.getuser() tries env vars before pwd.getpwuid(), so the lookup
never touches the passwd database and works for any PUID the operator
picks — no helper code, no image rebuild for different UIDs.
Also pass config=[] to asyncssh.connect() so it does not try to load
~/.ssh/config (whose default path needs a resolvable home directory).
An earlier draft of this fix added a Python helper that caught the
KeyError and injected LOGNAME at module import. That was both more
code than needed and broken on Python 3.13, which wraps the KeyError
in an OSError the helper didn't catch — so the module import itself
crashed, producing a 500 on /spoolbuddy/devices/{id}/update. Reverted
in favour of the one-line ENV fix.
The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket. Install script updated with SSH access, sudoers entry,
and --ssh-pubkey flag for pairing.
- Add ports 6000 (file transfer) and 322 (RTSP camera) to Dockerfile
EXPOSE and docker-compose.yml bridge mode port mapping
- Update migration doc with new proxy mode port requirements
- Regenerate proxy-mode-diagram.png with all proxied ports
When running multiple virtual printers with different access codes on
separate bind IPs, FTP connections were always routed to the wrong VP.
Root cause: the iptables REDIRECT rule (990→9990) rewrites the
destination IP to the incoming interface's primary address. With Linux's
weak host model (arp_filter=0), packets for secondary IPs arrive on the
primary interface, and REDIRECT sends them all to the first VP's FTP
server. MQTT was unaffected because port 8883 had no redirect.
Fix: FTP server now binds directly to port 990 (standard implicit FTPS),
eliminating the iptables redirect entirely. Requires CAP_NET_BIND_SERVICE
(already set in the systemd service file and Docker image).
Also removed a global asyncio set_exception_handler() in the MQTT server
that was overwritten by each VP instance, causing spurious "Unhandled
exception in client_connected_cb" errors on startup.
Changes:
- FTP_PORT: 9990 → 990 (ftp_server.py)
- Removed set_exception_handler() from MQTT server
- Updated Dockerfile, docker-compose.yml port mappings
- Deprecated --redirect-990 in install script
- Updated wiki: removed iptables instructions for all platforms
- Added migration guide (docs/migration-vp-ftp-port.md)
- Added unit tests for port constant and no-global-state invariant
Fix P2S camera stream dropping and snapshot capture race (#661)
P2S firmware's TLS renegotiation is rejected by Debian's hardened GnuTLS
defaults, causing ffmpeg RTSP sessions to drop after ~3 seconds. Add
GnuTLS config allowing unsafe renegotiation and legacy ciphers. Also add
ffmpeg fast-start flags, reduce reconnect delay from 1.0s to 0.2s,
remove double rate-limiting on external camera streams, and fix orphan
cleanup killing snapshot capture ffmpeg processes (exit code -9).
Or as a single combined commit:
Fix P2S camera streaming, snapshot race, and energy stats (#661, #695)
Camera: P2S firmware's TLS renegotiation rejected by Debian's hardened
GnuTLS defaults, dropping RTSP sessions after ~3s. Add GnuTLS compat
config, ffmpeg fast-start flags, reduce reconnect delay to 0.2s, remove
external camera double rate-limiting, and register snapshot ffmpeg PIDs
with the orphan tracker to prevent SIGKILL during capture.
The Docker image (python:3.13-slim) didn't include iproute2, so the
`ip` command wasn't available. The code fell back to ioctl-based
enumeration which can only return one IP per interface — aliases like
eth0:1 were completely invisible. Added iproute2 to the Dockerfile.
cap_add: NET_BIND_SERVICE in docker-compose.yml doesn't reliably
propagate to the Python process when combined with the user: directive
(depends on ambient capability support in the container runtime).
Set the file capability directly on the Python binary via setcap in
the Dockerfile, which the kernel honors regardless of runtime config.
Multiple Virtual Printers:
- Each VP gets a dedicated bind IP with independent FTP, MQTT, SSDP, and Bind services
- New VirtualPrinter DB model, CRUD API (/api/virtual-printers), React UI
- VirtualPrinterList, VirtualPrinterCard, VirtualPrinterAddDialog components
- Per-instance TLS certificates (shared CA), 11 printer models, all 4 modes
- Auto-incremented serial suffixes, network interface override per VP
Dual Bind/Detect Ports (#445):
- Listen on both ports 3000 and 3002 for slicer bind/detect handshake
- Different BambuStudio/OrcaSlicer versions use different ports
- Applies to BindServer (server mode) and SlicerProxyManager (proxy mode)
- Updated Dockerfile, docker-compose.yml, firewall rules in wiki
Also:
- Rewrote VP test suite for new multi-instance architecture (75 tests)
- Rewritten "How it works" section with 3-step workflow explanation
- Updated all 5 locales (en, de, ja, fr, it)
- Updated wiki and website for multi-VP + dual ports
- New multi-VP screenshot
Recent BambuStudio/OrcaSlicer updates require a bind/detect handshake on
port 3000 before connecting via MQTT/FTP. Without this, slicers cannot
discover or connect to the virtual printer in any mode.
- Add BindServer for server modes (immediate/review/print_queue)
- Add TCPProxy for raw TCP forwarding (proxy mode)
- Update Dockerfile (EXPOSE 3000) and docker-compose.yml (bridge port)
- Add 10 new tests for BindServer protocol and integration
Enable Bambu Studio on a remote network to print through BamBuddy
acting as a TLS-terminating proxy for both MQTT and FTP connections.
- Add TLSProxy base class and FTPTLSProxy with PASV response rewriting,
EPSV→PASV translation, PROT P/C tracking, and one-shot data proxies
- Add SlicerProxyManager to coordinate per-slicer MQTT + FTP proxy pairs
- Support additional SAN IPs in certificate generation for proxy mode
- Broadcast SSDP on LAN B so slicers discover the proxy as a printer
- Narrow FTP passive port range to 50000-50100 with retry logic
- Expose proxy ports (8883, 9990, 50000-50100) in Dockerfile
- Document passive port range in docker-compose.yml
- Add user directive to docker-compose.yml using PUID/PGID env vars
- Allows container to run as host user, fixing permission issues with
bind-mounted volumes (e.g., ./virtual_printer)
- Add chmod 777 to /app/data and /app/logs in Dockerfile for non-root compatibility
- Usage: PUID=$(id -u) PGID=$(id -g) docker compose up -d
Note: Existing named volumes (bambuddy_logs, bambuddy_data) created by previous
root containers may need to be removed or have permissions fixed manually.
Manually configure AMS slots for third-party or generic filaments:
1. Hover over an AMS slot on the printer card
2. Click the menu button (:material-dots-vertical:) that appears
3. Select **Configure Slot**
4. Choose a filament preset from your Bambu Studio cloud presets
5. Select a matching K profile (pressure advance calibration)
6. Optionally set a custom color using the color picker
7. Click **Configure Slot** to apply
**Color Picker Features:**
- Enter custom hex codes or color names (e.g., "brown", "FF8800")
- Live preview of selected color
- Expandable color picker in Configure AMS Slot modal:
- 8 basic colors shown by default
- 24 additional colors available via expand button
- Tests for ConfigureAmsSlotModal component
- Tests for AMS change callback
- Updated README with AMS slot configuration feature
- Wiki documentation for Configure AMS Slot feature
- Multi plate issue where plate names showed incorrect. #93
- Items from Queue end up as "source" files in archive. #107
- Added env variable support to change network port. #108
Docs -> https://wiki.bambuddy.cool/getting-started/docker/?h=port#custom-port
- Added DEBIAN_FRONTEND=noninteractive to suppress debconf warnings
- Added --root-user-action=ignore to pip install
- Added ffmpeg to Docker image
- Increased chunkSizeWarningLimit to 3000 for Vite build
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Test Summary:
- Build tests: 3 passed (image build, backend imports, static files)
- Backend unit tests: 378 passed (9 docker tests excluded)
- Frontend unit tests: 137 passed
- Integration tests: 9 passed (health, API endpoints, persistence, WebSocket)
Changes made to fix the Docker test suite:
1. Added curl to the production Dockerfile for integration tests
2. Removed deprecated version attribute from docker-compose.test.yml
3. Added --pull flag to all build commands to ensure fresh images
4. Added explicit build step before starting integration container
5. Fixed WebSocket test to accept 200 as a valid response
6. Excluded docker-marked tests from backend unit test runs (-m "not docker")