VolumeVault is a self-hosted Laravel application for managing Docker volume and host path backups with safe restores through offen/docker-volume-backup.
It provides a clear web UI for scheduled backups, grouped backups with a single aggregated notification, stack-level volume coverage, restore runs, encrypted destinations, notifications, proactive alerts, run history, onboarding, and API-driven automation.
|
|
Generate an application key first:
docker run --rm ghcr.io/darkdragon14/volumevault:latest php artisan key:generate --showCreate a docker-compose.yml file and paste the generated key in APP_KEY:
services:
volumevault:
image: ghcr.io/darkdragon14/volumevault:latest
ports:
- "8080:8080"
volumes:
- volumevault_data:/app/storage
- /var/run/docker.sock:/var/run/docker.sock
environment:
APP_KEY: base64:paste-generated-key-here
restart: unless-stopped
volumes:
volumevault_data:Start VolumeVault:
docker compose up -dOpen http://localhost:8080 and create the first administrator account from the onboarding screen.
The container listens on port 8080; change the host side of the mapping, for example 9090:8080, if you want to expose VolumeVault on another port.
The single container runs nginx, PHP-FPM, database migrations, queue worker, and scheduler.
The web UI requires Safari 16.4+, Chrome 111+, or Firefox 128+ (or a compatible modern browser). Tailwind CSS 4 uses modern CSS features that are not supported by older browsers.
If a MySQL upgrade failed in 2026_09_22_072732_add_destination_operations_to_agent_operations with error 1059 (index name too long), update to the fixed code and rerun php artisan migrate --force in the application environment. The migration resumes after partially committed columns or foreign keys without deleting existing rows. Do not use migrate:fresh for recovery; it deletes data. Existing SQLite installations require no schema changes and retain rollback compatibility.
To connect through a Docker TCP endpoint, such as a socket proxy in front of the same Docker engine, use the standalone TCP file: VOLUMEVAULT_DOCKER_HOST=tcp://docker-proxy.example.internal:2375 docker compose -f docker-compose.tcp.yml up -d. The separate interpolation variable configures the container without redirecting the host Docker CLI, and the TCP file does not mount /var/run/docker.sock. The base file is fixed to the local Unix socket so an ambient DOCKER_HOST cannot accidentally combine proxy mode with unrestricted socket access. The endpoint applies to the entire VolumeVault instance and must be reachable from both VolumeVault and the temporary Offen backup containers. If the proxy hostname only resolves on a user-defined Docker network, set VOLUMEVAULT_DOCKER_NETWORK to that network's engine-visible name so Offen joins it. This does not add support for managing a Docker engine on another host. Docker TCP access is root-equivalent; keep it on a trusted private network and see the installation and security documentation before enabling it.
Defaults are built into the application for a production SQLite setup. Add environment variables only when you need to override them, for example APP_URL, APP_TIMEZONE, or SMTP settings.
You can also use env_file: .env for overrides, but do not reuse a development .env in production without review. Values such as APP_ENV=local or APP_DEBUG=true override the safe production defaults.
Host path backup jobs and local backup destinations are restricted by VOLUMEVAULT_HOST_PATH_ALLOWLIST, a comma-separated list of allowed Docker host path prefixes such as /srv,/mnt/data. This is fail-closed: when the variable is empty, host path sources and local destinations are refused entirely. Configure the prefixes you intend to back up to/from; paths outside them are rejected both when saved and again at run time. VolumeVault canonicalizes every path visible in its own filesystem, regardless of Docker transport, so resolvable symlink escapes are refused. Paths unavailable to VolumeVault receive lexical-only validation and must be protected from untrusted symlink replacement.
The requests VolumeVault makes to a backup destination on your behalf are guarded against SSRF, and this is deny-by-default: a destination host that resolves to a private, loopback or link-local address (including the cloud metadata endpoint 169.254.169.254) is refused. In practice this only matters when your destination sits on a private IP — a cloud destination reached by a public URL (AWS S3, Backblaze, Dropbox, …) is never affected and needs no configuration.
The trigger is the range of the resolved IP, not whether you typed an IP or a hostname. A public IP — whether written directly (http://203.0.113.10:9000) or reached through a hostname — is never blocked; that is by design, because the point of SSRF protection is to stop the server being used to reach internal resources an attacker could not otherwise touch (a public address is something they could already request themselves). Conversely a hostname is blocked when it resolves to a private address (e.g. an internal DNS name nas.home.lan → 192.168.1.x).
What this changes for a destination on a private IP, before you allow its range:
- ✅ Scheduled backups still run — the upload is done by the backup container, which is not guarded, so the archive still lands on your NAS.
⚠️ The archive size is not detected (the run is still marked successful).- ❌ Restore is blocked (listing + download) — this is the one that matters: a backup you cannot restore is useless.
- ❌ The "Test destination" button and the storage-quota alert are blocked.
The fix is a single line: list the CIDR range(s) your destinations live on in VOLUMEVAULT_SSRF_ALLOWED_IPS (comma-separated), after which everything works normally while 169.254.169.254 and friends stay blocked:
# e.g. a NAS on your home LAN
VOLUMEVAULT_SSRF_ALLOWED_IPS=192.168.1.0/24The host is resolved just before connecting, so a determined attacker controlling DNS could still rebind it afterwards — accepted here because every guarded action is admin-gated. Notification channels (Gotify, Ntfy, SMTP, …) are not guarded: they are blind, admin-configured, and commonly self-hosted on the LAN.
retention_count keeps the newest N archives belonging to a backup job on its destination. After a successful regular backup, VolumeVault lists the actual archives and deletes only the oldest excess files. Failed backups and pre-restore safety backups do not trigger count retention.
New archives carry a stable per-job prefix and a unique run ID, including when a custom filename template is used. Safety backups use a separate prefix. Both count and day retention are handled by VolumeVault and apply only to regular archives in the job namespace; if both limits are set, an archive exceeding either limit is eligible for deletion, except the newly uploaded archive. Archives from other jobs, unrelated files and older archives without this prefix are preserved. Existing files are not renamed; clean up legacy archives manually if needed.
Destination credentials must permit both listing and deletion, and private destinations must be allowed by VOLUMEVAULT_SSRF_ALLOWED_IPS. Update remote agents before using count retention. Check the backup run logs for cleanup failures: a successful upload does not guarantee that destination cleanup succeeded.
When you serve VolumeVault over HTTPS (directly or behind a TLS-terminating reverse proxy), set SESSION_SECURE_COOKIE=true so the session cookie is only sent over HTTPS. Leave it off for plain-HTTP or LAN-only access — a Secure cookie is never sent over plain HTTP, so enabling it without TLS breaks login. Behind a reverse proxy, this works once TRUSTED_PROXIES is set and the proxy forwards X-Forwarded-Proto: https (see the docs for the full reverse-proxy setup).
Keep your APP_KEY safe: it is required to decrypt destinations, notifications, two-factor secrets, and installation saves.
The default VOLUMEVAULT_MODE=hybrid keeps the existing local Docker executor. Set VOLUMEVAULT_MODE=orchestrator to run the central application without a Docker socket or remote Docker endpoint; docker-compose.orchestrator.yml provides a standalone deployment for this mode. Local jobs and history remain stored, but local execution is disabled. Drain local work through maintenance before changing modes.
Agents use the dedicated volumevault-agent PHP CLI image. The Docker hosts page shows local/central/agent roles, software and protocol compatibility, maintenance state, and a manual update guide. Keep each agent's existing /app/storage volume and configuration when replacing its container; a normal update does not require enrollment again. See docs/_tabs/installation.md for activation, image targets, compatibility and update procedures.
Container builds require PHP 8.5.12 or newer in both the application and agent base images. The Publish container images GitHub Actions workflow can be run manually on main to refresh the published images after upstream security updates; it always pulls current base images and refuses builds below this PHP security baseline. For local builds, use docker compose build --pull. Updating a base image does not update an existing VolumeVault image or running container: rebuild and publish both images, then pull the updated images and recreate the application and every agent container, preserving their storage volumes and APP_KEY. Installations pinned to a release tag or digest must explicitly select a newly published corrected image; rebuilding main only refreshes its branch/SHA tags and latest.
Compatible agents execute standalone backups and restores. Shared network destinations support restoring an archive from host A into a new volume on host B. Distributed groups, remote Docker-label reconciliation and relay of archives stored only on another host remain under development.
The compact, single-line responsive header includes a themed environment dropdown beside the logo. Choose a Docker host or All environments to save the selection in your current authenticated server-side web session. The header submits a native, CSRF-protected HTTP POST followed by a 303 redirect and a full Dashboard reload. Normal navigation continues to use Inertia; browser Back/Forward and BFCache restoration refresh from the server rather than replaying stale SPA state. An explicit docker_host_id URL parameter temporarily filters only the current request: it is read-only and never saves the global preference. Only the header POST saves that preference. The API remains stateless: pass docker_host_id explicitly when filtering API requests. Backup and restore forms still require their own explicit source/target choices; the navigation selection never implicitly changes execution settings.
The Backups menu groups backup jobs and backup groups, while an alert bell with a counter sits beside the compact account menu.
Laravel session blocking serializes requests sharing a web session to prevent older session data from overwriting the selected environment. Keep a server-side session driver (database, the default, or file/redis) and an atomic-lock-capable cache store (database, the default, or file/redis). Cookie-only sessions are not supported for this workflow. The lock lease is 300 seconds, released when the request finishes; the normal lock wait timeout is 10 seconds. This does not serialize API requests or queued backup/restore work.
If an editable form has unsaved changes, switching environments requires confirmation before submitting. Cancel keeps the draft and sends no POST; proceeding discards the draft and loads the Dashboard. Drafts are not automatically transferred to the new environment.
The full documentation is published with GitHub Pages and built from the docs directory.
- Documentation URL: https://volumevault.darkdragon14.dev/
- Documentation source:
docs/index.md
VolumeVault relies on offen/docker-volume-backup for the actual Docker volume backup engine and destination support.
Huge thanks to Offen and the maintainers of offen/docker-volume-backup for their work.
All contributions are welcome.
- Contributor acknowledgements:
CONTRIBUTORS.md


