Features β’ Quick Start β’ Documentation β’ API Docs β’ Contributing
A self-hosted backup manager with a modern web UI for 35 standard source types including NAS, Git, databases, cloud storage, Docker and Supabase. Runs on ARM64 and AMD64 Docker hosts, including Synology NAS.
π Web UI available in English and German.
In Augsburg, nobody says "Could you please go and fetch that?". They say "Hol ma!" β short for "hol mal" / "holen wir": go get it, or let's go get it. One word, no fuss, and it gets done.
That is the whole job description. Holma goes out to your NAS, your GitHub repositories, your Supabase projects, your databases and your cloud drives, fetches everything home, and puts it on the shelf with a date on it β three versions deep, oldest one out when a new one comes in.
It does not ask twice, and it does not bring back half the fridge. Passt scho.
Click to expand
| Platform | RAM | Architecture |
|---|---|---|
| Raspberry Pi 3/4/5 | 2 GB+ | ARM64 (64-bit OS) |
| Synology NAS | 2 GB+ | x86_64/ARM64 |
| Linux Server | 2 GB+ | x86_64/ARM64 |
| Docker Host | 2 GB+ | x86_64/ARM64 |
Hardware is detected automatically and resources are tuned to match.
Docker: 20.10+
Compose: 2.0+
- π NAS: SMB credentials
- π GitHub: Personal Access Token
- π Cloud: OAuth2 credentials or API keys
- π SSH: private key for rsync
Note
Requires Docker 20.10+ and 2 GB+ RAM. Runs on Raspberry Pi, Synology NAS, Linux servers, or any Docker host.
curl -fsSL https://raw.githubusercontent.com/hehljo/Holma/main/install.sh | bashThe installer creates a persistent .env file and generates the required SECRET_KEY for you.
Important
Every installation requires a SECRET_KEY of at least 32 characters. Keep it persistent and unchanged across restarts, updates and restores. It secures authentication and encrypts stored credentials.
# 1. Clone the repository
git clone https://github.com/hehljo/Holma.git
cd Holma
# 2. Create a persistent environment file
cp config/example.env .env
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
nano .env # Set SECRET_KEY to the generated value
# 3. Start the services
docker compose up -d
# 4. Open the Web UI on your trusted local network and create the first account
open http://localhost:3000First login: choose your own username and enter the password twice in the Web UI. All Unicode characters (including Γ and emoji) are allowed; only an empty password is rejected. There are no length or composition requirements, so choose a secure password yourself. No default account or password is created. Keep the app accessible only to your trusted local network during initial setup: the first visitor can claim the owner account while the database is empty. Existing installations keep their accounts. All source credentials are managed via the Web UI.
Warning
Deploying a repository stack through Portainer? Select docker-compose.portainer.yml explicitly. Portainer may prefill docker-compose.yml; replace it before deploying. The regular file builds images locally and uses different default ports and volume paths. The Portainer file pulls the prebuilt GHCR images and provides the Portainer-specific defaults.
Tip
Holma detects your hardware automatically and adjusts resources (workers, RAM limits, parallel tasks) on its own.
Step-by-step guide
sudo mkdir -p /volume1/docker/holma/{config,data,logs,backup}[!IMPORTANT] Every Holma installation requires this key. Store it securely and keep it unchanged across updates and restores; without it the backend will not start, and changing it makes stored credentials unreadable.
python3 -c "import secrets; print(secrets.token_urlsafe(32))"Portainer β Stacks β Add Stack:
| Field | Value |
|---|---|
| Name | holma |
| Build method | Repository |
| Repository URL | https://github.com/hehljo/Holma |
| Repository reference | refs/heads/main |
| Compose path | docker-compose.portainer.yml |
[!CAUTION] Before deploying, verify the Compose path is exactly
docker-compose.portainer.ymlβnot Portainer's prefilleddocker-compose.yml. The latter triggers a local image build and uses different port and volume defaults.
Private repo? β enable Authentication β username: your GitHub user β password: Personal Access Token (classic, scope:
repo)
Environment variables (advanced mode):
SECRET_KEY=your_generated_key
PLATFORM_PROFILE=auto
API_PORT=5050
FRONTEND_PORT=3080
Important:
SECRET_KEYis mandatory. Portainer does not automatically load a.envfile from the repository, so add this key as a stack environment variable in Portainer.PLATFORM_PROFILE,API_PORTandFRONTEND_PORTare optional; Compose defaults apply when they are omitted.The
.envfile itself is not needed for this Portainer setup, but theSECRET_KEYnormally stored in it is. Store the key securely and keep it unchanged across updates and restores; otherwise, saved credentials can no longer be decrypted. Add other credentials (GitHub, Supabase, etc.) later in the Web UI.
β Deploy the stack
http://diskstation-ip:3080
First-time setup: choose your own username and enter the password twice in the Web UI. All Unicode characters are accepted, but empty passwords are not; password strength is your responsibility. No default admin password exists. Keep the app on your trusted local network until the first account is created: whoever visits first can claim the owner account while the database is empty. Do not expose an uninitialized installation to the internet or untrusted VPN users.
Existing installations: persisted accounts remain unchanged; the setup form is unavailable once an account exists. If the password is lost, open the backend container console and run python -m app.account_cli list-users, then python -m app.account_cli reset-password YOUR_USERNAME. The new password is entered interactively and previous login/automation tokens are revoked. No email server or internet access is required. Do not delete the data volume to reset an account.
- Settings β Credentials β enter GitHub token, NAS passwords etc. (stored encrypted)
- Sources β Add Source β configure backup sources
- Start backup β Dashboard β Start Backup
In Portainer β stack holma β Update the stack β Re-pull image and redeploy
- Ports: DSM occupies 5000/5001 β use
API_PORT=5050andFRONTEND_PORT=3080 - Autostart after reboot: handled automatically via
restart: unless-stopped - Permissions: if you hit permission errors, run
sudo chown -R 1000:1000 /volume1/docker/holma/ - Persistent data: everything under
/volume1/docker/holma/survives updates
Step-by-step guide
curl -fsSL https://get.docker.com | bash
sudo usermod -aG docker $USER
# Log in again so the group change takes effectcd /opt
sudo git clone https://github.com/hehljo/Holma.git
sudo chown -R $USER:$USER Holma
cd Holma
cp config/example.env .env
cp config/sources-example.json config/sources.json[!IMPORTANT]
SECRET_KEYis required for this installation. Put the generated value in.envand keep it unchanged across updates and restores.
# Generate a value, then paste it unchanged into .env
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
nano .envSECRET_KEY=paste_the_generated_value_here
BACKUP_BASE_PATH=/mnt/backupsdocker compose up -d
docker compose pshttp://server-ip:3000
Step-by-step guide
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com | bash
sudo usermod -aG docker pi
sudo rebootcd /opt
sudo git clone https://github.com/hehljo/Holma.git
sudo chown -R pi:pi Holma
cd Holma
cp config/example.env .env
cp config/sources-example.json config/sources.json[!IMPORTANT]
SECRET_KEYis required for this installation. Put the generated value in.envand keep it unchanged across updates and restores.
# Generate a value, then paste it unchanged into .env
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
nano .envSECRET_KEY=paste_the_generated_value_here
BACKUP_BASE_PATH=/mnt/backup
# Pi 3 with limited RAM: tune the limits
# BACKEND_MEMORY_LIMIT=512M
# BACKEND_CPU_LIMIT=1.5
# FRONTEND_MEMORY_LIMIT=128Mdocker compose up -dhttp://raspberrypi.local:3000
Plug in a USB drive β backup starts automatically:
sudo python3 scripts/create-api-token.py
sudo ./scripts/install-systemd.shDetailed guide: USB Auto-Trigger β
For any platform with Docker
[!IMPORTANT]
SECRET_KEYis mandatory for this installation as well. Generic Docker Compose deployments normally provide it in.env. Keep the same value for the lifetime of the installation; changing it makes stored credentials unreadable.
git clone https://github.com/hehljo/Holma.git
cd Holma
cp config/example.env .env
cp config/sources-example.json config/sources.json
# Generate SECRET_KEY, then paste the output into .env
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
nano .env
# Start
docker compose up -d
# Web UI: http://localhost:3000[!CAUTION] Set the Compose path to
docker-compose.portainer.yml. Do not leave Portainer'sdocker-compose.ymldefault selected; it builds locally and has different port and volume defaults.
In Portainer β Stacks β Add Stack β Repository:
- Repository URL:
https://github.com/hehljo/Holma - Compose path:
docker-compose.portainer.yml(uses prebuilt GHCR images, no build needed) - Under Environment variables, set a persistent, randomly generated
SECRET_KEY(at least 32 characters). Portainer does not automatically load the repository's.envfile. - Optionally set
API_PORT,FRONTEND_PORTor other supported overrides; otherwise Compose defaults apply. - Deploy
The .env file itself is optional for this Portainer method; SECRET_KEY is not. Keep the same key across redeployments and restores.
| Variable | Default | Description |
|---|---|---|
PLATFORM_PROFILE |
auto |
auto, raspberrypi, synology, server |
BACKEND_CPU_LIMIT |
2.0 |
CPU limit for the backend |
BACKEND_MEMORY_LIMIT |
1G |
RAM limit for the backend |
FRONTEND_CPU_LIMIT |
1.0 |
CPU limit for the frontend |
FRONTEND_MEMORY_LIMIT |
256M |
RAM limit for the frontend |
MAX_PARALLEL_TASKS |
auto |
Parallel backup tasks (auto = based on RAM) |
Important
SECRET_KEY is mandatory. Without it, the app refuses to start.
Generate one: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
Required environment variables:
| Variable | Required | Description |
|---|---|---|
SECRET_KEY |
Yes | Random string for JWT + credential encryption |
API_PORT |
No (5000) | Port for the backend API |
FRONTEND_PORT |
No (3000) | Port for the Web UI |
PLATFORM_PROFILE |
No (auto) | auto, raspberrypi, synology, server |
Credentials (GitHub token, NAS passwords, Supabase keys etc.) are not set via environment variables β manage them through the Web UI under Settings β Credentials. They are stored AES-encrypted in the database.
First login: on a trusted local network, choose a username and enter the password twice in the Web UI. Existing accounts remain valid. For offline recovery, use docker compose exec backend python -m app.account_cli list-users and docker compose exec -it backend python -m app.account_cli reset-password YOUR_USERNAME.
Note
All your data survives a reinstall!
Holma uses Docker volumes for persistent storage. During updates or reinstalls, the following data is preserved automatically:
Persistent directories:
./config/ # β
All backup sources (sources.json)
# β
rclone configuration
# β
Notification settings
./data/ # β
Database (users, history, settings)
# β
Backup logs
# β
Metadata
./logs/ # β
Application logs
/mnt/backup/ # β
Your backup data (configurable)Benefits:
- π Safe updates:
docker compose pull && docker compose up -d - πΎ Backup-friendly: just back up
./config/and./data/ - π Migration: copy folders β fresh install β done!
- β‘ Rollback: start an older container version with no data loss
Create a full backup:
# Save Holma configuration
cd /opt/Holma
tar -czf holma-config-$(date +%Y%m%d).tar.gz config/ data/ .env
# Copy to a safe location
cp holma-config-*.tar.gz /mnt/external-drive/Restore after a fresh install:
# Fresh install
cd /opt
git clone https://github.com/hehljo/Holma.git
cd Holma
# Restore the backup
tar -xzf /mnt/external-drive/holma-config-*.tar.gz
# Start the containers - all settings are back!
docker compose up -dExport/Import via the Web UI:
Since v1.1 you can also export/import all settings directly from the web interface:
- Settings β Configuration Export/Import
- Export β downloads a JSON file with all sources & settings
- Import β select a JSON file and restore your configuration
π Full source list β |
|
/mnt/backup/<source-id>/
<source-id>_20260923_020000.tar.gz <- small sources: one archive per run
<source-id>_20260924_020000.tar.gz (GitHub: one repo archive per repository inside)
_mirrors/ <- Git working copies, only new commits are fetched
/mnt/backup/<nas-source>/
<nas-source>_20260923_020000/ <- large sources: snapshot folder per run,
<nas-source>_20260924_020000/ unchanged files are hard links
_current/ <- incremental sync target
Retention keeps the newest N versions per source (Settings β Storage, default 3). Working folders (_mirrors, _current) are never rotated.
β Livebezeichnet einen echten Zielsystemtest.β Autobezeichnet grΓΌne Unit-/Fehlerpfad-/Image-Tests; der reale Zielsystemtest ist dort noch offen.π²ist noch nicht belastbar geprΓΌft.Historische Live-Ergebnisse wurden noch nicht gegen den aktuellen Remediation-Build wiederholt. Aktuelle Details stehen in
docs/SECURITY_AUDIT_LOCAL.md.Lokaler Gesamtgate (23.09.2026):
scripts/run-gates.shβ 8 Backend-Gates, Marken-/Versions-Gate, Frontend-Lint und Produktionsbuild grΓΌn. LΓ€uft in CI vor jedem Image-Build.
| Source | Backup | Restore | Notes |
|---|---|---|---|
| GitHub (mirror clone, auto-discovery) | β Live (Altstand) | β | im vollstΓ€ndigen Backend-Gate geprΓΌft |
| Supabase (DB + Storage + Auth Config) | β Live (Altstand) | β Auto | Restore-Fehlerpfade aktuell geprΓΌft |
| NAS (SMB) | β Auto | β | Dienst live erreichbar, Anmeldung erzwungen; Testfreigabe-E2E offen |
| rsync over SSH | β Auto | β | Command-/Secret-Gate; Live offen |
| GitLab | β Auto | β | gemeinsames Git-Mirror-/Archiv-Gate |
| Bitbucket | β Auto | β | gemeinsames Git-Mirror-/Archiv-Gate |
| Gitea / Forgejo / Codeberg | β Auto | β | sauberer Remote/Auth-Vertrag geprΓΌft |
| MySQL / MariaDB | β Auto | β | Credential-Datei und Image-Client geprΓΌft |
| PostgreSQL | β Auto | β Auto (Supabase) | CLI, Fehlerpfade und Restore-Sicherheit geprΓΌft |
| Redis | β Auto | β | Remote-RDB und Secret-Γbergabe geprΓΌft |
| SQLite | β Auto | β | Live-Backup-API plus IntegritΓ€tscheck geprΓΌft |
| CouchDB | β Auto | β | read-only HTTP-Handler geprΓΌft; Live offen |
| Google Drive (rclone) | β Auto | β | nicht-lΓΆschendes rclone copy + Image-CLI; Live offen |
| Dropbox (rclone) | β Auto | β | nicht-lΓΆschendes rclone copy + Image-CLI; Live offen |
| OneDrive (rclone) | β Auto | β | nicht-lΓΆschendes rclone copy + Image-CLI; Live offen |
| S3 / Backblaze B2 (rclone) | β Auto | β | Secret-/Command-/Image-Gate; Live offen |
| Portainer stack export | β Auto | β | Status-API live auf Port 9000; authentifizierter Export offen |
| Docker Volumes / Images | β Auto | β | Fehler-/Restart-/Image-CLI-Gates; Live offen |
| Home Assistant | π² | β | read-only API-Export |
| Local filesystem | β Auto | β | rsync-Fehlerpfad geprΓΌft; Live offen |
| Host-/Transport-Gate | Status | Notes |
|---|---|---|
Tailscale β diskstation |
β Live | direkte Verbindung, 17 ms |
DSM HTTPS (5001) |
β Live | HTTP 200 |
Portainer Status (9000) |
β Live | API erreichbar; 9443 auf dieser NAS geschlossen |
| DiskStation Docker-Engine | π² | SSH-Port geschlossen; read-only API-Zugang noch nΓΆtig |
If you've tested a source, please share your setup β it helps others a lot.
- Safe Cloud Copies: rclone sources now use non-deleting
copy; source-side removals no longer delete files from the backup destination. - Reliable Container Startup: database initialization is serialized across Gunicorn workers and the backup worker; first-run owner setup is allowed only while no account exists.
- Automation Tokens: USB/systemd jobs can use password-bound tokens that expire after at most 365 days and are revoked by password changes.
- Adaptive UI: The web UI now has consistent touch targets, visible focus states, responsive page shells, mobile-friendly drawers, bottom-sheet modals, and safer wrapping for backup/source lists.
- One Archive per Run: Every backup run becomes exactly one timestamped version per source, and the newest 3 are kept by default (configurable). Small sources (GitHub, GitLab, Gitea, databases, Supabase, Docker, self-hosted apps) become one
.tar.gzper run with one archive per repository inside. Large file sources (NFS, rsync, rclone, FTP/SFTP, WebDAV, local folders) become snapshot folders where unchanged files are hard links, so three versions of a 100 GB share cost 100 GB plus the changes. NAS via SMB and Proxmox dumps are already one full archive per run and are stored as-is in a timestamped folder (no second compression) β with SMB, each version is a full copy. Failed runs never replace a good version. - Source Handler Compatibility: Non-GitHub/Supabase handlers now accept UI-created list fields, direct credentials, and path fallbacks more robustly across local, database, Docker, FTP/SFTP, WebDAV, rclone, rsync, self-hosted, and Proxmox sources.
- i18n Cleanup: The language selector is always visible and common Settings/Storage/Config dialogs now use localized English/German strings.
- Dark Mode: The web UI now follows the system theme on first load, keeps manual theme changes, and includes dark-safe colors for forms, cards, modals, badges, logs, and notifications.
- Supabase Full Backup: Fixed full-mode backups with Storage/Auth config by resolving the service role key from the selected credential profile.
- Restore Safety: Restore paths are now restricted to the configured backup directory and archive extraction is protected against path traversal.
- GitHub Backups: Mirror clones no longer store access tokens in remote URLs; failed Git commands are reported as failures instead of successful backups.
- Configuration Export: Secret-like values are redacted recursively before exporting configuration files.
- Source Forms: UI-created sources now normalize paths, lists, repositories, Docker volumes/images, and NAS/SMB shares for the backend handlers.
- Notifications: Notification endpoints require authentication and the UI uses the real configured channels instead of placeholder data.
Holma exposes 35 source types supported by the standard image and web form. Configure them in config/sources.json or through the web UI:
π NAS (SMB)
{
"id": "nas-project1",
"name": "NAS - Project 1",
"type": "nas",
"enabled": true,
"priority": 1,
"config": {
"host": "192.168.1.100",
"share": "projects",
"path": "project1",
"username": "backup_user",
"credentials": {
"password_env": "NAS_PASSWORD"
},
"options": {
"timeout": 3600
}
},
"schedule": {
"enabled": true,
"trigger": "cron,usb_mount",
"frequency": "daily",
"time": "03:00",
"max_duration": 3600
}
}Read-only connection test:
smbclient //192.168.1.100/projects -U backup_user -c lsπ GitHub Repositories (auto-discovery)
{
"id": "github-repos",
"name": "GitHub Repositories",
"type": "github",
"enabled": true,
"priority": 2,
"discovery_mode": "all",
"exclude": ["user/some-unwanted-fork"],
"repositories": [],
"credentials": {
"token_env": "GITHUB_TOKEN"
},
"options": {
"include_wikis": true,
"include_lfs": true
}
}discovery_mode: "all" automatically backs up all repos (private + public + orgs). New repos are picked up on the next backup. Use "manual" to pick repos individually via the Web UI.
Generate a token: GitHub β Settings β Developer settings β Personal access tokens β scopes: repo, gist
π’ Supabase (DB + Storage)
Configuration is done through the Web UI:
- Settings β Credentials β Supabase β create a new profile with the connection string (Session Pooler URI from the Supabase dashboard) + DB password + optional service role key
- Sources β Add Source β Supabase β pick a profile + choose backup mode (
db_onlyorfull) - Start backup
backup_mode: full saves DB (roles + schema + data) + storage buckets + RLS/auth config. db_only for PostgreSQL dumps only.
Storage backup: full mode with "Storage Buckets einschlieΓen" downloads all bucket objects into storage/<bucket>/... and writes bucket/object metadata to storage_metadata/. This preserves image/file content plus common upload metadata such as content type and cache control for restore.
Restore: History β backup with the restore button β choose target profile β start restore. Manual connection string entry is also supported.
Storage restore: enable "Storage-Objekte wiederherstellen" and provide a target Supabase profile or service role key. Holma recreates missing buckets, uploads objects with upsert, and marks the restore as partial if individual Storage uploads fail. Older backups with legacy _bucket_meta.json bucket metadata remain supported.
βοΈ Cloud Storage (rclone)
{
"id": "google-drive",
"name": "Google Drive Backup",
"type": "rclone",
"remote": "gdrive",
"path": "/My_Backup",
"enabled": true,
"priority": 3,
"options": {
"transfers": 4,
"checkers": 8
}
}Configure rclone:
docker exec -it holma-backend rclone configThe standard form exposes Google Drive, Dropbox, OneDrive, S3, Backblaze B2, iCloud, Box, MEGA and pCloud plus a generic rclone source.
π³ Docker Volumes
{
"id": "docker-volumes",
"name": "Docker Volumes",
"type": "docker-volume",
"enabled": true,
"priority": 4,
"volumes": ["volume1", "volume2"],
"options": {
"compress": true,
"stop_containers": false
}
}πΎ Local Directories
{
"id": "local-docs",
"name": "Local Documents",
"type": "local",
"enabled": true,
"priority": 5,
"sources": [
"/home/pi/documents",
"/home/pi/photos"
],
"options": {
"recursive": true,
"delete": false,
"compress": false
}
}π View all 35 standard source types β
On a systemd-based Linux host, the included scripts can trigger a backup when a USB drive is connected. This is not used on Synology DSM.
Set up udev + systemd
sudo python3 scripts/create-api-token.pyThe helper accepts only HTTPS or a local HTTP API, asks for the admin password
without echoing it, and stores the token with mode 0600. Automation tokens are
valid for at most 365 days and are revoked immediately when the account password
changes.
If the backend uses a different local port:
sudo python3 scripts/create-api-token.py \
--api-url http://127.0.0.1:5050/api/v1sudo ./scripts/install-systemd.shsudo systemctl start holma-backup@sda1
journalctl -u 'holma-backup@*' -n 50Replace sda1 with a dedicated test partition. The trigger mounts the selected
partition at /mnt/backup; review that target before the first real run.
All credentials are managed via the Web UI:
- Settings β Credentials
- Enter token / password (GitHub, NAS, Supabase, SMTP, Telegram etc.)
- Save Credentials
Credentials are stored AES-encrypted in the database (Fernet/PBKDF2). No plaintext passwords in files or environment variables.
Access the dashboard at:
http://raspberrypi.local:3000
http://YOUR_PI_IP:3000
Features:
- π Real-time backup status dashboard
- π Detailed backup history with logs
- βοΈ Source management (add/edit/delete)
- π Credentials configuration
- π Storage usage statistics
- π Notification settings
- π Language switcher (EN/DE)
Start a backup via API
# Get an API token
TOKEN=$(curl -s -X POST http://localhost:5000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "your_username", "password": "your_password"}' \
| jq -r '.access_token')
# Start a backup
curl -X POST http://localhost:5000/api/v1/backup/start \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sources": ["nas-project1", "github-repos"],
"parallel": 2
}'Check backup status
curl -X GET http://localhost:5000/api/v1/backup/BACKUP_ID \
-H "Authorization: Bearer $TOKEN"# Service status and logs
docker compose ps
docker compose logs --tail=100 backend
# Verify tools included in the backend image
docker exec holma-backend rclone version
docker exec holma-backend smbclient --versionHolma ships with full multi-language support:
- π¬π§ English β fully translated
- π©πͺ Deutsch β vollstΓ€ndig ΓΌbersetzt
- Frontend: language switcher in the sidebar header
- Backend: auto-detection via the
Accept-Languageheader - Storage: preference saved in browser localStorage
See the detailed guide: i18n Documentation β
Quick steps:
- Frontend: create
frontend/src/locales/{lang}/translation.json - Backend: run
pybabel init -d app/translations -l {lang} - Translate the
.pofiles - Compile:
pybabel compile -d app/translations
Technology stack:
- Frontend:
react-i18next+i18next-browser-languagedetector - Backend:
Flask-Babel
All application endpoints except login, first-run setup/status and health checks require a Bearer token authentication. Login tokens are valid for 24 hours.
POST /api/v1/auth/login
Content-Type: application/json
{
"username": "your_username",
"password": "your_password"
}
Response:
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 86400,
"user": {}
}For unattended scripts, an admin can create a password-bound automation token:
POST /api/v1/auth/api-token
Authorization: Bearer LOGIN_TOKEN
Content-Type: application/json
{
"current_password": "your_password",
"expires_days": 365
}Changing that account's password revokes both login and automation tokens.
π¦ Backup management
POST /api/v1/backup/start
Authorization: Bearer TOKEN
{
"sources": ["source-id-1", "source-id-2"],
"parallel": 2
}
Response 202:
{
"backup_id": "backup-uuid-1234",
"status": "queued",
"started_at": "2025-11-13T19:30:00Z",
"sources": 2,
"parallel": 2
}GET /api/v1/backup/{backup_id}
Authorization: Bearer TOKEN
Response 200:
{
"backup_id": "backup-uuid-1234",
"status": "running",
"progress": 65,
"started_at": "2025-11-13T19:30:00Z",
"sources": [...]
}GET /api/v1/backup/history?limit=20&offset=0
Authorization: Bearer TOKEN
Response 200:
{
"total": 156,
"backups": [...]
}π§ Source management
GET /api/v1/sources
Authorization: Bearer TOKEN
Response 200:
{
"sources": [
{
"id": "nas-project1",
"name": "NAS - Project 1",
"type": "smb",
"enabled": true
}
]
}POST /api/v1/sources
Authorization: Bearer TOKEN
Content-Type: application/json
{
"name": "GitHub Org",
"type": "github",
"repositories": ["org/repo1"],
"credentials": {
"token_env": "GITHUB_TOKEN"
},
"enabled": true
}
Response 201:
{
"message": "Source created successfully",
"source": {
"id": "github-12345678",
"name": "GitHub Org",
"type": "github",
"enabled": true
}
}PUT /api/v1/sources/{source_id}
Authorization: Bearer TOKEN
Content-Type: application/json
{
"enabled": false
}DELETE /api/v1/sources/{source_id}
Authorization: Bearer TOKENπ« Docker containers won't start
If Portainer reports that holma-backend is unhealthy, first check that the stack has a non-empty SECRET_KEY environment variable. The backend refuses to start without it, and Portainer does not automatically load .env from the Git repository. Keep the same key after the first successful deployment.
# Check the logs
docker compose logs backend
docker compose logs frontend
# Rebuild the containers
docker compose down
docker compose up -d --build --force-recreate
# Check system resources
free -h
df -hβ οΈ Backup doesn't start automatically
# Reload udev rules
sudo udevadm control --reload-rules
sudo udevadm trigger
# Check systemd logs
journalctl -u holma-backup@sd* -n 50 -f
# Debug USB devices
lsblk
sudo udevadm info --name=/dev/sda1 --attribute-walkπ NAS connection fails
# Test SMB connection
smbclient -L //192.168.1.100 -U backup_user
# Test from inside Docker
docker exec holma-backend smbclient -L //192.168.1.100 -U backup_user
# Then use Sources β connection test in the Web UIπ GitHub token invalid
# Verify the token online
curl -H "Authorization: token YOUR_TOKEN" https://api.github.com/user
# Update the token under Settings β Credentials, then test the source againπ½ Disk space full
# Check available space
df -h /mnt/backup
# Find the largest files
du -sh /mnt/backup/* | sort -rh | head -20
# Review configured retention under Settings β Storage
# Preview the optional host cleanup script without deleting anything
sudo DRY_RUN=true ./scripts/backup-cleanup.shImportant
Follow security best practices to protect your backup data!
Set up secure SSH access
# Generate an ED25519 key pair (locally)
ssh-keygen -t ed25519 -o -a 100 -f ~/.ssh/holma
# Copy the public key to the Raspberry Pi
ssh-copy-id -i ~/.ssh/holma.pub pi@raspberrypi.local
# Configure the SSH server
sudo nano /etc/ssh/sshd_configRecommended sshd_config settings:
Port 2222 # Non-standard port
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
MaxAuthTries 3
X11Forwarding no
AllowUsers pi
sudo systemctl restart ssh# Enable UFW
sudo ufw enable
# Allow SSH (adjust port if changed)
sudo ufw allow 2222/tcp
# Allow API (local network only)
sudo ufw allow from 192.168.1.0/24 to any port 5000
# Allow Web UI (local network only)
sudo ufw allow from 192.168.1.0/24 to any port 3000
# Check status
sudo ufw status# Interactive, password-bound, maximum lifetime 365 days
sudo python3 scripts/create-api-token.pyThe helper refuses to overwrite an existing token file. Changing the account
password revokes the token. Source and notification credentials are encrypted in
the database; keep the same SECRET_KEY when moving or restoring an installation.
Holma/
βββ backend/
β βββ app/
β β βββ __init__.py
β β βββ api/ # REST API endpoints
β β β βββ backup.py
β β β βββ sources.py
β β β βββ auth.py
β β βββ backup/ # Backup engine
β β β βββ executor.py
β β β βββ sources/ # Backup handler implementations
β β β β βββ smb.py
β β β β βββ github.py
β β β β βββ rclone.py
β β β β βββ ...
β β β βββ jobs.py # Persistent backup queue
β β β βββ worker.py # Dedicated backup worker
β β βββ models/
β β β βββ backup.py
β β βββ translations/ # i18n translations
β β βββ config.py
β βββ tests/ # unittest regression suite
β βββ requirements.txt
β βββ Dockerfile
βββ frontend/
β βββ src/
β β βββ components/ # React components
β β βββ pages/
β β βββ services/ # API client
β β βββ locales/ # i18n translations
β βββ package.json
β βββ Dockerfile
βββ config/
β βββ sources.json # Backup source definitions
β βββ rclone.conf # rclone remote configurations
β βββ sources-example.json
βββ scripts/
β βββ create-api-token.py
β βββ trigger-backup.sh
β βββ backup-cleanup.sh
βββ docs/
β βββ BACKUP_SOURCES.md # Complete source documentation
β βββ i18n.md # Internationalization guide
β βββ SECURITY_AUDIT_LOCAL.md
βββ README.md
# From the project root, run the backend in its supported container
export SECRET_KEY="development-secret-at-least-32-characters"
docker compose up -d --build backend
# Frontend development server proxies /api to localhost:5000
cd frontend
npm ci
npm run dev# Backend tests with warnings treated as errors
docker run --rm --entrypoint python holma/backend:latest \
-W error -m unittest discover -s tests -p 'test_*.py'
# Frontend checks
cd frontend
npm ci
npm run lint
npm run build
# Deployment syntax
SECRET_KEY="validation-secret-at-least-32-characters" docker compose config --quiet# Build all services
docker compose build
# Build a specific service
docker compose build backend
# The GitHub workflow publishes linux/amd64 and linux/arm64 imagesContributions are welcome! See the Contributing Guide for details.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a pull request
- Follow PEP 8 for Python code
- Follow the Airbnb Style Guide for JavaScript
- Write tests for new features
- Update documentation when API behavior changes
- Add i18n translations for any new UI strings
Done
- Notifications (email, Telegram, ntfy, webhooks via Apprise)
- Supabase backup + restore with profile-based credentials
- Encrypted credential storage (Fernet/PBKDF2)
- EN/DE multi-language UI
- Configuration export/import
- Multi-arch Docker images (amd64, arm64)
In progress / planned
- Dark/light theme toggle
- Advanced filtering in backup history
- Backup scheduling (per-source time schedules)
- Two-factor authentication (2FA)
- Audit logging
- Deduplication / incremental backups
- Restore UI for additional source types (currently Supabase only)
- Prometheus metrics export
- OpenAPI 3.0 spec
- Additional languages (FR, ES, IT)
This project is licensed under the MIT License β see the LICENSE file for details.
Need help? Have questions?
Report Bug β’ Request Feature β’ Ask a Question
If Holma helps you manage your backups, consider supporting development!
Your support keeps this project alive and growing! π
- π Full documentation
- π i18n guide
- π Security policy
Made with β€οΈ by the Holma community
β Star this repo if Holma helps you! β
π Home β’ π Docs β’ π Issues β’ π¬ Discussions