An open-source, agent-based backup and restore platform for self-hosted and decentralized infrastructure. Deploy it yourself for encrypted, automated backups across PostgreSQL, MySQL, MariaDB, MongoDB, SQLite, Redis, Valkey, Firebird, and MSSQL.
Demo video: https://youtu.be/1tNDvo0AaEE
In database operations, recoverability is non-negotiable. Unplanned data loss translates directly into downtime, missed RPOs, and, depending on your sector, compliance exposure. Outsourcing backups to a managed third party often means handing over credentials, accepting opaque retention, and trading away network isolation.
Portabase takes the opposite stance: a free, open-source (Apache-2.0), self-hosted control plane that orchestrates lightweight agents deployed next to your databases. The agents execute encrypted dumps and ship them to the storage backends you control, without ever exposing the databases to inbound traffic. The stack is built on Next.js for the dashboard, a Rust/Tokio agent for execution, Better Auth, Drizzle ORM, and Docker for packaging.
This guide walks through a from-scratch self-hosted deployment: dashboard, agent, database declaration, and first backup. Prerequisites are minimal: Docker and a tolerance for YAML.
What changed since the early builds
If you followed an earlier version of this guide, note three breaking changes:
-
The agent was rewritten in Rust. It is no longer a Python/Celery worker. The rewrite (repo:
portabase/agent-rust) eliminates an entire class of runtime errors, shrinks the resource footprint, and is the basis of the "secure-by-design" claim. Backups are now encrypted with AES-GCM for confidentiality and integrity. -
The images moved namespaces. Use
portabase/portabase(dashboard) andportabase/agent(agent). The oldsolucetechnologies/*images are deprecated. - There is now a CLI installer that scaffolds compose files, generates secrets, and manages the container lifecycle. It is the recommended path.
Why self-host Portabase
- Isolation by default. Databases stay inside their private networks. The control plane never dials into your environments; agents poll outbound to the server. No inbound ports, smaller attack surface, zero-trust-friendly.
- No lock-in, no fees. Apache-2.0, fully auditable codebase.
- Multi-backend storage. Local disk, any S3-compatible target (AWS S3, MinIO, RustFS), Google Drive, and Google Cloud Storage. Azure Blob Storage is on the roadmap. You can fan a single backup out to multiple destinations simultaneously for redundancy.
-
Real retention strategies. Retention by count, retention by time, and Grandfather-Father-Son (GFS) rotation, not just a
find -mtimecron. - Team-ready. Workspaces, projects, and role-based access control (member / admin / owner) at both system and organization scope.
Supported databases
All of the following are stable and tested in production:
| Engine | Tested versions | Restore |
|---|---|---|
| PostgreSQL | 12-18 | Yes |
| MySQL | 5.7, 8, 9 | Yes |
| MariaDB | 10, 11 | Yes |
| MongoDB | 4-8 | Yes |
| SQLite | 3.x | Yes |
| Firebird | 3.0, 4.0, 5.0 | Yes |
| MSSQL Server | n/a | Yes |
| Redis | 2.8+ | No (backup only) |
| Valkey | 7.2+ | No (backup only) |
Prerequisites
- Docker & Docker Compose (engine 20+).
- A host running Linux/macOS/Windows with ≥ 2 GB RAM and ≥ 10 GB free.
- Basic YAML literacy.
- Database credentials with dump/read permissions for whatever you intend to back up.
- Behind Traefik or Nginx? See the reverse-proxy guide: https://portabase.io/docs/dashboard/installation/reverse-proxy
Architecture in one paragraph
The central server is the control plane: web UI, agent registry, backup configuration, restore triggers, storage and notification integrations. The agent sits next to your databases and does the actual work. The server never contacts the agent; the agent periodically polls the server (default every 5 s, POLLING). That single design decision is what removes the need for inbound firewall rules and contains the blast radius if the control plane is ever compromised.
Step 1: Deploy the dashboard
You have four install paths: CLI (recommended), docker run (quick test), Docker Compose (production/GitOps), and a Kubernetes Helm chart. Both the CLI and the manual route are below.
Option A: CLI (recommended)
Install the CLI:
curl -sL https://portabase.io/install | bash
portabase --version
Scaffold and start the dashboard in one shot. The CLI downloads the templates, generates PROJECT_SECRET, wires up the internal PostgreSQL database, and launches the stack:
portabase dashboard my-dashboard --port 8887 --start
If you skipped --start:
portabase start my-dashboard
Then open http://localhost:8887.
Option B: Docker Compose (manual)
Create a working directory and a docker-compose.yml:
name: portabase-dashboard
services:
app:
container_name: portabase-app-prod
image: portabase/portabase:latest
restart: unless-stopped
env_file:
- .env
environment:
TZ: "Europe/Paris" # Adjust to your timezone
ports:
- "8887:80"
volumes:
- portabase-data:/data
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/api/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
db:
container_name: portabase-pg
image: postgres:17-alpine
restart: unless-stopped
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
portabase-data:
This spins up the dashboard on port 8887 and a PostgreSQL instance for Portabase's own metadata. To be explicit: this Postgres holds Portabase's internal state, not your backups.
Environment configuration (.env)
The current .env is deliberately minimal. Storage backends, notification channels, and authentication (OAuth2 / OIDC) are configured in the dashboard UI, not via environment variables, so the sprawling SMTP/OAuth/S3 blocks from older guides are gone.
# --- App configuration ---
PROJECT_URL=http://localhost:8887
# Used to encrypt communication with agents. Generate a strong one:
# openssl rand -hex 32
PROJECT_SECRET=change_me_please_generate_a_secure_hex_token
# --- Internal metadata database ---
POSTGRES_USER=portabase
POSTGRES_PASSWORD=changeme # change this
POSTGRES_HOST=db
POSTGRES_PORT=5432
POSTGRES_DB=portabase
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public
PROJECT_SECRET is non-optional: it secures the agent ↔ server channel. Don't ship a placeholder to production.
Launch and tail the logs:
docker compose up -d
docker compose logs -f
The health check gates the app on the database, so startup is ordered. Once it's up, hit http://localhost:8887 and register. The first account gets admin in the default workspace.
Kubernetes users can install straight from the OCI registry:
helm install portabase oci://ghcr.io/portabase/charts/portabase \ -n portabase --create-namespace \ --set project.secret=$(openssl rand -hex 32)
Step 2: Deploy an agent
The agent is the Rust connector that runs near your databases. Install one on each host that owns databases you want backed up.
Option A: CLI
On the database host, grab an Edge Key from the dashboard (Agents → add a new agent), then:
portabase agent my-agent
The wizard asks for the Edge Key and offers to add databases immediately, either by spinning up a fresh local PostgreSQL/MariaDB container, or by pointing at an existing/managed instance (host, port, credentials).
Option B: Docker Compose
First create the external network the agent expects:
docker network create portabase_network
Then scaffold the files. The config file must exist before first start, even if empty:
mkdir portabase-agent && cd portabase-agent
echo '{"databases": []}' > databases.json
docker-compose.yml:
name: portabase-agent
services:
app:
container_name: portabase-agent
image: portabase/agent:latest
restart: always
volumes:
- ./databases.json:/config/config.json # mount your DB config
extra_hosts:
- "localhost:host-gateway" # lets the agent reach the host's localhost
environment:
TZ: "Europe/Paris"
LOG: info
POLLING: 5
APP_ENV: production
DATA_PATH: "/data"
# DATABASES_CONFIG_FILE: "config.toml" # uncomment to use TOML instead
EDGE_KEY: "${EDGE_KEY}" # from the dashboard
networks:
- portabase
networks:
portabase:
name: portabase_network
external: true
Two details that trip people up: the config file mounts to /config/config.json (not the old /app/src/data/... path), and extra_hosts is what lets the containerized agent reach databases bound to the host's localhost.
Agent environment reference:
| Variable | Description | Default |
|---|---|---|
EDGE_KEY |
Unique agent key from the dashboard | Required |
TZ |
Timezone (e.g. UTC, Europe/Paris) |
UTC |
POLLING |
Poll frequency in seconds | 5 |
APP_ENV |
production / staging / development
|
production |
DATA_PATH |
Internal data path in the container | /data |
LOG |
Log level (info, debug, …) |
info |
Declare your databases (databases.json)
Mount a config file describing each connection. JSON is the default; TOML is supported and more readable. Each entry needs a unique UUID v4 in generated_id. This is how the dashboard keeps a database's backup history stable even if you rename it. Generate one with uuidgen (Linux/macOS) or any UUID v4 tool. Do not invent a random string.
{
"databases": [
{
"name": "PROD - api",
"database": "prod_api",
"type": "postgresql",
"host": "localhost",
"port": 5432,
"username": "admin_prod",
"password": "super_secure_password",
"generated_id": "550e8400-e29b-41d4-a716-446655440000"
},
{
"name": "DEV - site",
"database": "mariadb",
"type": "mysql",
"host": "192.168.1.50",
"port": 3306,
"username": "root",
"password": "dev_password",
"generated_id": "123e4567-e89b-12d3-a456-426614174000"
}
]
}
TOML equivalent:
[[databases]]
name = "PROD - api"
database = "prod_api"
type = "postgresql"
host = "localhost"
port = 5432
username = "admin_prod"
password = "super_secure_password"
generated_id = "550e8400-e29b-41d4-a716-446655440000"
[[databases]]
name = "DEV - site"
database = "mariadb"
type = "mysql"
host = "192.168.1.50"
port = 3306
username = "root"
password = "dev_password"
generated_id = "123e4567-e89b-12d3-a456-426614174000"
Field notes: type accepts postgresql, mysql, mariadb (use mysql for MariaDB), sqlite, etc. After any manual edit, restart the agent so it reloads: docker compose restart app.
You can also let the CLI manage this file safely: portabase db add <agent-path> validates syntax and generates the UUID for you; portabase db list shows what's configured.
Bring it online
docker compose up -d # or: portabase start my-agent
In the dashboard, the agent should register and flip to online within a few poll cycles. If it doesn't, check portabase logs my-agent for the "Ping server" line.
Step 3: Configure backups and run the first one
With server and agent connected:
- Set up workspaces and projects. Organize databases, storage backends, and notification channels by organization and project.
- Define a backup policy. Cron-based schedule (or manual trigger) plus a retention strategy: by count, by time, or GFS rotation.
- Pick storage. Local for a quick start; S3-compatible, Google Drive, or Google Cloud Storage for durability. Target several at once for redundancy.
- Wire notifications. Email, Slack, Discord, Telegram, Ntfy, Gotify, MS Teams, Pushover, or generic webhooks, with per-database alert rules for success/failure.
- Save and trigger. Portabase validates the config and kicks off the first run. Monitor jobs, download dumps, or restore with one click.
Where the project is
Since stepping up communication in late 2025 across Reddit, Medium, Dev.to, and Hacker News, Portabase has crossed 1,100+ GitHub stars with a growing stream of user-reported issues. Real adoption, not vanity metrics. It's already referenced on the MariaDB EcoHub and is in production internally and at several companies.
On the near-term roadmap: extended PITR support via a universal connector, additional storage backends (Google Cloud Storage shipped; Azure Blob, FTP/SFTP planned), a RESTful API and an MCP server for standardized automation, and a hosted cloud edition.
Wrapping up
Self-hosting Portabase turns backups from a fragile pile of cron scripts into a managed, observable pipeline you fully own, typically stood up in under 30 minutes. Encrypted, agent-isolated, multi-engine, and Apache-2.0.
- Docs: https://portabase.io/docs
- Dashboard: https://github.com/portabase/portabase
- Rust agent: https://github.com/portabase/agent
- CLI: https://github.com/portabase/cli
- Discord: https://discord.gg/Wgv7xZ8fWJ
Star the repos, deploy it, and report back: which database are you backing up first?
Top comments (0)