Dropbox-like desktop app that syncs a local folder to a Raspberry Pi over Cloudflare tunnel or LAN. Per-user storage, admin user management, and secure credential storage.
- Backend: Python (FastAPI) in Docker on Raspberry Pi. Storage under
/mnt/shared_storage/brandyBox/<email>/. JWT auth, user CRUD (admin), file list/upload/download. - Client (primary): Tauri + React in
client-tauri/– modern desktop app (Windows, Linux, Mac) with robust sync engine. Useshttps://brandybox.brandstaetter.rocksvia Cloudflare tunnel, orhttp://192.168.0.150:8081when on LAN. Tray and sync work out of the box on Linux (incl. Wayland/KDE). - Client (fallback): Python/Tk client in
client/– deprecated, use only if Tauri cannot be built or run. Same config format and keyring; under Linux the venv-based install may be needed for correct tray behavior.
The backend image is published to GitHub Container Registry (GHCR). You can run it with a single Docker command—no need to clone the repository.
-
Prerequisites on the Pi
- Docker installed (e.g.
curl -fsSL https://get.docker.com | shthensudo usermod -aG docker $USER; log out and back in). - Your HDD/storage mounted so that
/mnt/shared_storage/brandyBoxexists (create it if needed:sudo mkdir -p /mnt/shared_storage/brandyBox && sudo chown pi:pi /mnt/shared_storage/brandyBoxor your Pi user). - Cloudflare tunnel (or another way) pointing at the Pi, e.g. to port 8081.
- Docker installed (e.g.
-
Create a config directory and
.env(no clone: download the example from the repo):mkdir -p ~/brandybox-backend && cd ~/brandybox-backend curl -sL https://raw.githubusercontent.com/markusbrand/brandyBox/master/backend/.env.example -o .env # Edit .env and set BRANDYBOX_JWT_SECRET, SMTP, admin, etc. (no quotes needed for values)
Set at least:
BRANDYBOX_JWT_SECRET(e.g.openssl rand -hex 32), SMTP vars,BRANDYBOX_ADMIN_EMAIL,BRANDYBOX_ADMIN_INITIAL_PASSWORD, andBRANDYBOX_CORS_ORIGINS(e.g.https://brandybox.brandstaetter.rocks). Bcrypt limits passwords to 72 bytes. -
Run the backend (one Docker command):
docker run -d \ --name brandybox-backend \ --restart unless-stopped \ -p 8081:8080 \ -v brandybox_data:/data \ -v /mnt/shared_storage/brandyBox:/mnt/shared_storage/brandyBox \ --env-file .env \ ghcr.io/markusbrand/brandybox-backend:latest
The service listens on port 8081. If the image is private, run
docker login ghcr.iofirst (username: GitHub user, password: PAT withread:packages). To use port 8080 instead, change-p 8081:8080to-p 8080:8080. -
Check it's running
docker ps curl http://localhost:8081/health
You should see
{"status":"ok"}. From another machine usehttp://<pi-ip>:8081/health; via the tunnel,https://brandybox.brandstaetter.rocks/health.- If the container is Exited or curl fails, check logs:
docker logs brandybox-backend. Common cause: missingBRANDYBOX_JWT_SECRETin.env. - If you get "Empty reply from server", wait 10–15 seconds after starting (startup runs DB init and admin bootstrap), then try again.
- Updates:
docker pull ghcr.io/markusbrand/brandybox-backend:latest, thendocker stop brandybox-backend && docker rm brandybox-backendand run thedocker runcommand again (yourbrandybox_datavolume and.envare preserved).
- If the container is Exited or curl fails, check logs:
-
Optional: Install from clone (build from source)
If you prefer to build the image locally or use the webhook listener:git clone https://github.com/markusbrand/brandyBox.git cd brandyBox/backend cp .env.example .env # Edit .env, then: docker compose up -d --build
To use the GHCR image from the clone instead of building:
docker compose -f docker-compose.yml -f docker-compose.ghcr.yml up -d. -
Optional: Automatic updates via GitHub webhook
When GitHub Actions finishes building the backend image, a webhook can trigger an update on the Pi so the new image is pulled and the container restarted without manual SSH.- On the Pi: A small Flask app (
backend/webhook_listener.py) listens for GitHub webhook POSTs (e.g. on port 9000). It verifies the request withX-Hub-Signature-256using a secret, and on successfulworkflow_runcompletion it runsbackend/update_brandybox.sh, which runsdocker compose -f docker-compose.yml -f docker-compose.ghcr.yml pull && … up -d. - Secret: Set
GITHUB_WEBHOOK_SECRETin the environment when starting the webhook listener (e.g. in a systemd unit or a small.envthat is not committed). Use the same value in GitHub: repo → Settings → Webhooks → Add webhook → Payload URL:https://deploy.brandstaetter.rocks/webhook(Cloudflare tunnel to the Pi listener on port 9000), Content type:application/json, Secret: your secret. Under “Which events would you like to trigger this webhook?” choose Workflow runs (or “Let me select… → Workflow runs). The listener only acts whenworkflow_runhasaction: completedandconclusion: success. - Run the listener: e.g.
cd ~/brandyBox/backend && GITHUB_WEBHOOK_SECRET='your-secret' python webhook_listener.py(or run it under systemd/gunicorn). Expose port 9000 via a Cloudflare tunnel (e.g. tohttps://deploy.brandstaetter.rocks/webhook) so GitHub can reach it. - Cron (optional): To start the webhook listener after a reboot, add a cron job for the Pi user: run
crontab -eand add:The listener loads@reboot cd /home/pi/brandyBox/backend && nohup python3 webhook_listener.py >> webhook.log 2>&1 &
GITHUB_WEBHOOK_SECRETfrombackend/.env, so the secret does not need to be in the cron line. Log output is appended tobackend/webhook.log. Alternatively, a cron job can runupdate_brandybox.shperiodically (e.g. daily) as a fallback if webhooks are not used.
See Backend overview for the script and listener layout.
- On the Pi: A small Flask app (
User files are stored under BRANDYBOX_STORAGE_BASE_PATH (default /mnt/shared_storage/brandyBox). Each user gets a subfolder (e.g. admin@example.com). Ensure that path exists on the host and is writable by the container; the docker run command above (or docker-compose.yml when using the clone) mounts it into the container. If sync fails (red tray icon), check backend logs and ensure the mount is correct.
On first start, the backend creates an admin user from BRANDYBOX_ADMIN_EMAIL and BRANDYBOX_ADMIN_INITIAL_PASSWORD. Use that account in the desktop client; admins can create and delete users (passwords are sent by email).
The Tauri client (client-tauri/) is the recommended desktop app. The Python client (client/) is deprecated and kept as fallback only.
- Open Releases and download the zip for your system:
- Windows:
BrandyBox-<version>-Windows-x64.zip - Linux:
BrandyBox-<version>-Linux-x64.zip - macOS:
BrandyBox-<version>-macOS-arm64.zipor-macOS-x64.zip
- Windows:
- Unzip the file.
- Run the app:
- Windows: Double-click
BrandyBox.exein the unzipped folder. - macOS: Open the folder and run the app (or drag it to Applications).
- Linux: Open a terminal in the unzipped folder and run
./BrandyBox. To add a menu entry, see Installers.
- Windows: Double-click
Linux (Garuda/KDE): Tauri uses native tray APIs; no venv needed. If you encounter issues, see Client troubleshooting.
Tauri – development (run from source):
cd client-tauri
npm install
npm run tauri devTauri – build from source (create your own zip):
cd client-tauri && npm install && npm run tauri:build. Output under src-tauri/target/release/bundle/. See client-tauri/README.md.
Python client (deprecated, fallback only):
Use only if Tauri cannot be built or run. Development: cd client && pip install -e . && cd .. && python -m brandybox.main. Linux venv install (for correct tray): prerequisites sudo pacman -S python-gobject libappindicator-gtk3, then python -m venv .venv --system-site-packages, source .venv/bin/activate, cd client && pip install -e ., ./assets/installers/linux_install.sh --venv. PyInstaller build: pyinstaller client/brandybox.spec. See Client troubleshooting.
- Run the app; log in with email and password (or use stored credentials).
- Default sync folder is
~/brandyBox(e.g./home/markus/brandyBox). If it already exists, that folder is used. Sync does not run until you have confirmed a folder (open Settings once and close, or choose another folder). - 404 on login: If the client shows "404 Not Found" for
…/api/auth/login, the backend URL may be wrong. Check thatcurl https://brandybox.brandstaetter.rocks/healthreturns{"status":"ok"}. If/healthworks but login still 404s, your Cloudflare Tunnel may be using a path prefix; setBRANDYBOX_BASE_URL(e.g.https://brandybox.brandstaetter.rocks/backend) and run the app again. - Open Settings: If you have never set a sync folder, the Settings window opens automatically (showing default ~/brandyBox); close it or choose another folder so sync can start. You can also run "Brandy Box Settings" from your app menu (Linux install adds this desktop entry) or run
BrandyBox --settingsto open Settings without the tray. Left-clicking the tray icon is supposed to open Settings too, but on some Linux setups the tray menu is broken (grey circle); use "Brandy Box Settings" instead. - Quit the app: Right-click tray icon → Quit (if the menu opens). If the tray menu is broken, run "Quit Brandy Box" from your app menu (Linux install adds this), or run
killall BrandyBoxin a terminal. - Tray icon / menu on Linux: For the full tray (icon + right‑click menu), install PyGObject and AppIndicator:
sudo pacman -S python-gobject libappindicator-gtk3(Arch/Garuda). The app then uses the AppIndicator backend. On Garuda/KDE, use the venv-based install (see “Install on Linux” above or./assets/installers/linux_install.sh --venv) so the menu runspython -m brandybox.mainwith the systemgi; the standalone PyInstaller binary falls back to the XOrg backend (square icon, no context menu). If you use a venv, create it withpython -m venv .venv --system-site-packagesso the venv can use the systemgimodule. - Tray icon shows sync state: synced (blue), syncing (amber), warning (amber – some uploads skipped), error (red). If the icon turns red, hover to see the error; if amber after sync, some files were skipped (e.g. removed during sync). Expired tokens are refreshed automatically.
- Option “Start when I log in” in Settings (no admin required).
- Installers
- E2E autonomous sync test – runs the client-tauri app; build first with
cd client-tauri && npm run tauri:build, thenpython -m tests.e2e.run_autonomous_sync(requires test credentials and backend). - Development docs:
pip install mkdocs && mkdocs servethen open http://127.0.0.1:8000, ormkdocs buildforsite/ - Full QA: From repo root run
./scripts/run-qa.sh(client-tauri tests, backend pytest, mkdocs build). Requires.venvwithpytestandmkdocs(e.g.pip install -r backend/requirements.txt mkdocs).
Single venv for backend tests and docs: From repo root, install backend deps (and optional mkdocs) into the project venv so python -m pytest backend and run-qa.sh work without changing into backend/:
python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -r backend/requirements.txt mkdocs
cd backend && pytest # or: pytest backend (from root)Local client-tauri and E2E: Install Rust and Node.js, then build the Tauri client and run E2E (see E2E README):
cd client-tauri && npm ci && npm run tauri:build
# Set BRANDYBOX_ADMIN_EMAIL and BRANDYBOX_ADMIN_PASSWORD in repo-root .env, then:
python -m tests.e2e.run_autonomous_sync- No secrets in repo: Passwords, JWT secret, and SMTP credentials are never committed. Backend reads from environment (use
backend/.envfrombackend/.env.example;.envis gitignored). - Safe to publish: The repo can be made public on GitHub; no credentials or API keys are in source. E2E tests use env vars
BRANDYBOX_TEST_EMAILandBRANDYBOX_TEST_PASSWORD(set locally only). - Client stores only refresh token and email in OS keyring.
- Auth and file endpoints are rate-limited; path traversal is blocked; CORS is restricted.
Copyright 2026 Markus Brand.
This project is open source under the Apache License 2.0. See LICENSE for the full text. You may use, modify, and distribute the software under the terms of that license, with attribution as described in NOTICE.