DEV Community

Cover image for Self-Hosting Portabase: A Comprehensive Guide to Effortless Database Backup and Restore
Soluce Technologies
Soluce Technologies

Posted on • Edited on

Self-Hosting Portabase: A Comprehensive Guide to Effortless Database Backup and Restore

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) and portabase/agent (agent). The old solucetechnologies/* 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 -mtime cron.
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If you skipped --start:

portabase start my-dashboard
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Set up workspaces and projects. Organize databases, storage backends, and notification channels by organization and project.
  2. Define a backup policy. Cron-based schedule (or manual trigger) plus a retention strategy: by count, by time, or GFS rotation.
  3. Pick storage. Local for a quick start; S3-compatible, Google Drive, or Google Cloud Storage for durability. Target several at once for redundancy.
  4. Wire notifications. Email, Slack, Discord, Telegram, Ntfy, Gotify, MS Teams, Pushover, or generic webhooks, with per-database alert rules for success/failure.
  5. 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.

Star the repos, deploy it, and report back: which database are you backing up first?

Top comments (0)