Profile
Back to NewsBack
GitHub Trending 30 min
Reader Mode
arkeep-io/arkeep: Manage backups across all your servers from a single dashboard.

arkeep-io/arkeep: Manage backups across all your servers from a single dashboard.

11 hours ago

Arkeep

Your infra's Ark — back up everything, keep it yours.

Arkeep is an open-source infrastructure management platform with a server/agent architecture. Deploy the server once, install lightweight agents on every machine you want to manage, and handle backups, inventory, updates and recovery from a single web interface — built on top of Restic and Rclone.

!Arkeep dashboard


Table of Contents

- Docker Compose - Standalone Binary - Agent via systemd - Server - Agent - Health endpoints - Prometheus metrics - Configuring notifications - Webhook payload - Event types - Signature verification - Integrations - Healthchecks.io - Prerequisites - Getting Started - Project Structure - Available Tasks - General procedure - Docker Compose - Standalone Binary - Helm - Breaking changes - Backing up Arkeep itself

Why Arkeep?

Running infrastructure across multiple machines — containers, VMs, bare metal — means juggling separate tools for backups, inventory, and updates. There is no central view, no unified alerting, and no easy way to answer the question that matters most before touching anything in production: do I have a verified backup I can recover from?

Arkeep is built around that question. The backup layer is the foundation everything else sits on — inventory tells you what you have, update orchestration makes sure you can recover if something goes wrong, and every operation leaves an audit trail.

  • Centralized management — one dashboard for all your servers, containers, VMs and bare metal machines
  • Docker-aware — automatically discovers containers and volumes, adapts when you add or remove services without restarts
  • Backup-first operations — before anything changes, Arkeep knows whether a verified backup exists and blocks or warns accordingly
  • OIDC ready — integrates with Zitadel, Keycloak, Authentik, or any standard identity provider
  • Multi-destination — apply the 3-2-1 rule with multiple backup destinations per policy
  • End-to-end encryption — all backups are encrypted client-side; credentials are never stored in plain text
  • Real-time — live logs and status updates while backups run, accessible from any device
  • No vendor lock-in — built on Restic and Rclone; your data is always accessible even without Arkeep

Architecture

┌─────────────────────────────────────────┐
│              Arkeep Server              │
│  ┌──────────┐  ┌──────────────────────┐ │
│  │ REST API │  │     gRPC Server      │ │
│  │  :8080   │  │       :9090          │ │
│  └──────────┘  └──────────────────────┘ │
│  ┌────────────────────────────────────┐ │
│  │ Scheduler│Auth│DB│Notif│WebSocket  │ │
│  └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
         ▲                  ▲
         │ REST/WS          │ gRPC (persistent, Pull)
         │                  │
    ┌────┴────┐      ┌──────┴──────┐
    │   GUI   │      │   Agent(s)  │
    │  (PWA)  │      │  (one per   │
    └─────────┘      │   machine)  │
                     └─────────────┘

Server exposes a REST API for the GUI (port 8080) and a gRPC server for agents (port 9090). It handles scheduling, notifications, and stores all state in SQLite (default) or PostgreSQL.

Agent runs on each machine to be backed up. It initiates a persistent outbound gRPC connection to the server — it never listens on any port. This makes deployment behind NAT and corporate firewalls effortless.

gRPC transport limits: the server and agent both enforce a maximum message size of 16 MB per RPC call. The agent also sends keepalive pings every 30 seconds (10-second timeout) to detect stale connections early. These values are fixed in the binary; no configuration is required.

GUI is a Vue 3 PWA served directly by the server as embedded static files. No separate web server required.


Features

| Feature | Status | |---|---| | Server/agent architecture | ✓ | | Web GUI (PWA, mobile-first) | ✓ | | Local auth + OIDC | ✓ | | Multi-destination (3-2-1) | ✓ | | Docker volume discovery | ✓ | | Pre/post hooks (pg_dump, etc.) | ✓ | | Integrity verification | ✓ | | Retention policies | ✓ | | Email + webhook notifications | ✓ | | Healthchecks.io pings | ✓ | | Restore & restore test | ✓ | | Browse & download snapshot files | ✓ | | Helm chart | ✓ | | Host + container inventory | 🗓 planned | | Docker update detection & orchestration | 🗓 planned | | OS package updates | 🗓 planned | | Proxmox VM / LXC integration | 🗓 planned | | CVE correlation | 🗓 planned | | AI advisor & reports | 🗓 planned | | Bandwidth throttling | 🗓 planned | | BYOK encryption key management | 🗓 planned |


Two-factor authentication (TOTP)

Local accounts (not OIDC, which is managed by the identity provider) can enable TOTP-based two-factor authentication from their profile page: scan the QR code with any authenticator app (Google Authenticator, Aegis, 1Password, …) and confirm with a 6-digit code. Ten single-use recovery codes are issued at the same time, for signing in if the authenticator app is unavailable. From then on, login asks for the code after a correct password. An administrator can reset a user's two-factor authentication from the Users page if they lose access to both the app and their recovery codes.

Browse and download snapshot files

Click a snapshot's ID on the Snapshots page to browse its files, one directory at a time. The listing is read by an online agent that can reach the snapshot's repository: the snapshot's own agent by default, or any other one — useful when the machine that was backed up is gone and another agent sees the same S3, SFTP or rclone repository. Administrators can download a single file, or a directory as a ZIP archive, straight from the browser, and restore a selection of files through the usual restore flow. Downloads are streamed from the agent through the server, with no temporary files on either side.

Sync snapshots with the repository

Arkeep reconciles its snapshot list with the repository after every backup and retention run. Snapshots can also change outside Arkeep, for instance when an append-only rest-server runs restic forget --prune from its own cron job. To pick those changes up without waiting for the next backup, an administrator can click Sync Snapshots on a destination's page, or Sync on the Snapshots page (one destination when filtered, every enabled destination otherwise). Snapshots no longer in the repository are removed from the list, and snapshots Arkeep has never seen are added as imported. The repository is read, never locked, by an online agent: the destination's retention agent if connected, otherwise the agent of any policy writing there. To run the sync periodically, set an interval under Settings → Snapshot Sync; it is disabled by default.


Supported Destinations

| Type | Notes | |---|---| | Local filesystem | Direct path on the agent's host | | S3-compatible | AWS S3, MinIO, Backblaze B2, Cloudflare R2, and more | | SFTP | Any SSH server — password or SSH private key (PEM) auth | | Restic REST Server | Self-hosted rest-server | | Rclone | 40+ backends including Google Drive, OneDrive, Azure Blob, and more |

SFTP connections are handled by the agent's bundled client, so no SSH client or ~/.ssh setup is required on the agent host. The credentials you enter (password or private key) are used directly. Note that the server's host key is not currently verified.


Deployment

Docker Compose

The simplest way to get started. All images are published to both GitHub Packages and Docker Hub.

Server only (GUI included):

curl -O https://raw.githubusercontent.com/arkeep-io/arkeep/main/deploy/docker/docker-compose.yml
curl -O https://raw.githubusercontent.com/arkeep-io/arkeep/main/deploy/docker/.env.example
cp .env.example .env

Edit .env — at minimum set ARKEEP_SECRET_KEY and ARKEEP_AGENT_SECRET

docker compose up -d

The GUI is available at http://localhost:8080.

gRPC TLS with Docker:

The server generates a private CA and server certificate on first startup (auto-PKI). Agents auto-enroll via HTTP on first run and use mTLS from that point on — no manual cert configuration required.

For the server you can alternatively use an external certificate:

  • Reverse proxy (recommended): put Caddy or Nginx in front and let it handle TLS termination for both ports. No cert config inside the containers.
  • Direct TLS: mount a certificate and set ARKEEP_GRPC_TLS_CERT/ARKEEP_GRPC_TLS_KEY on the server container (see the commented lines in docker-compose.yml and .env.example).
The all-in-one compose file sets ARKEEP_GRPC_INSECURE=true on both server and agent automatically, since they share the same private Docker network and TLS adds no security benefit there.

Agent only (on the machines you want to back up):

curl -O https://raw.githubusercontent.com/arkeep-io/arkeep/main/deploy/docker/docker-compose.agent.yml

Set ARKEEP_SERVER_ADDR and ARKEEP_AGENT_SECRET in your environment or .env

docker compose -f docker-compose.agent.yml up -d

All-in-one (server + agent on the same host):

curl -O https://raw.githubusercontent.com/arkeep-io/arkeep/main/deploy/docker/docker-compose.all.yml
docker compose -f docker-compose.all.yml up -d

For local filesystem destinations and restore behaviour with Docker, see Local destinations in Docker and Restore in Docker below.


Standalone Binary

Pre-built binaries for Linux, macOS, and Windows are available on the Releases page.

Server:

# Download and extract the server binary for your platform
curl -L https://github.com/arkeep-io/arkeep/releases/latest/download/arkeep-server_linux_amd64.tar.gz | tar xz

Generate secrets

export ARKEEP_SECRET_KEY=$(openssl rand -hex 32) export ARKEEP_AGENT_SECRET=$(openssl rand -hex 32)

./arkeep-server \ --db-dsn /var/lib/arkeep/arkeep.db \ --data-dir /var/lib/arkeep/data \ --http-addr :8080 \ --grpc-addr :9090

TLS (auto-PKI): by default the server generates a private CA and server certificate
under --data-dir/grpc/ on first startup. Agents auto-enroll via
POST /api/v1/agents/enroll on first run and use mTLS from then on —
no manual cert management required.
> To use an external certificate instead (e.g. Let's Encrypt via Caddy), pass
--grpc-tls-cert and --grpc-tls-key. Auto-PKI is then skipped entirely.

Agent:

curl -L https://github.com/arkeep-io/arkeep/releases/latest/download/arkeep-agent_linux_amd64.tar.gz | tar xz

./arkeep-agent \ --server-addr your-server:9090 \ --agent-secret your-agent-secret \ --state-dir /var/lib/arkeep-agent

Auto-enrollment: on first run the agent calls the server's HTTP API
(derived from --server-addr with port 8080 by default) to obtain its
client certificate. The CA cert and client cert are stored in --state-dir
and reused on every subsequent startup — no re-enrollment unless you delete them.
> Reverse proxy (Traefik, Nginx, Caddy): if the server's HTTP API is only
reachable via HTTPS (port 8080 is not exposed directly), set --server-http-addr
to the public HTTPS URL — e.g. --server-http-addr https://arkeep.example.com.
The agent uses this address only for the one-time enrollment request; afterwards
the mTLS certificates in --state-dir are reused on every restart.
> Use --grpc-tls-ca only when connecting to a server that uses an external cert
(not auto-PKI) signed by a non-system CA.
Embedded Linux targets: in addition to linux_amd64/linux_arm64, the
agent is also published for linux_arm (armv7, e.g. Raspberry Pi and similar
single-board computers), linux_mips, and linux_mipsle — useful for
backing up embedded systems such as DVB-S2 satellite receivers. These
architectures compile and are covered by CI build checks, but are not
exercised on real hardware by our test suite; please report any
hardware-specific issues you run into.

Server and agent on the same machine (no reverse proxy, no TLS):

If you are running both binaries on the same host and do not want TLS on the loopback interface, add --grpc-insecure to both. Communication stays on loopback and is never exposed to the network.

./arkeep-server \
  --db-dsn /var/lib/arkeep/arkeep.db \
  --data-dir /var/lib/arkeep/data \
  --grpc-insecure

./arkeep-agent \ --server-addr localhost:9090 \ --agent-secret your-agent-secret \ --state-dir /var/lib/arkeep-agent \ --grpc-insecure

For any setup where the gRPC port is reachable from other machines, always use TLS (the default).


Agent via systemd

A systemd unit file is provided at deploy/systemd/arkeep-agent.service.

# Copy the binary
sudo cp arkeep-agent /usr/local/bin/arkeep-agent
sudo chmod +x /usr/local/bin/arkeep-agent

Copy and edit the unit file

sudo cp deploy/systemd/arkeep-agent.service /etc/systemd/system/ sudo systemctl daemon-reload

Create an environment file with your credentials

sudo mkdir -p /etc/arkeep sudo tee /etc/arkeep/agent.env > /dev/null <<EOF ARKEEP_SERVER_ADDR=your-server:9090 ARKEEP_AGENT_SECRET=your-agent-secret

For self-signed server certs only — leave empty for Let's Encrypt/trusted CAs:

ARKEEP_GRPC_TLS_CA=/etc/arkeep/ca.crt

EOF sudo chmod 600 /etc/arkeep/agent.env

sudo systemctl enable --now arkeep-agent sudo journalctl -u arkeep-agent -f


Configuration

All options can be set via CLI flags or environment variables. CLI flags take precedence over environment variables when both are provided.

Server Configuration

| Flag | Env | Default | Description | |---|---|---|---| | --http-addr | ARKEEP_HTTP_ADDR | :8080 | HTTP API and GUI listen address | | --grpc-addr | ARKEEP_GRPC_ADDR | :9090 | gRPC listen address for agents | | --grpc-tls-cert | ARKEEP_GRPC_TLS_CERT | — | Path to PEM certificate for gRPC TLS (requires --grpc-tls-key) | | --grpc-tls-key | ARKEEP_GRPC_TLS_KEY | — | Path to PEM private key for gRPC TLS (requires --grpc-tls-cert) | | --db-driver | ARKEEP_DB_DRIVER | sqlite | Database driver (sqlite or postgres) | | --db-dsn | ARKEEP_DB_DSN | ./arkeep.db | SQLite file path or PostgreSQL DSN | | --secret-key | ARKEEP_SECRET_KEY | — | Required. Master key for AES-256-GCM credential encryption | | --agent-secret | ARKEEP_AGENT_SECRET | — | Required. Shared secret agents present to enroll and authenticate over gRPC | | --data-dir | ARKEEP_DATA_DIR | ./data | Directory for RSA JWT keys and server state | | --base-url | ARKEEP_BASE_URL | — | External URL of the server (e.g. https://arkeep.example.com). Used to build links in outbound email (password reset). Required for self-service password reset: when unset, reset emails are disabled and users must ask an administrator. Links are never derived from request headers, to prevent password reset poisoning via a forged Host / X-Forwarded-Host. | | --log-level | ARKEEP_LOG_LEVEL | info | Log level (debug, info, warn, error) | | --secure-cookies | ARKEEP_SECURE_COOKIES | false | Set Secure flag on auth cookies (enable in production over HTTPS) | | --trusted-proxies | ARKEEP_TRUSTED_PROXIES | (loopback and private networks) | Comma-separated IPs/CIDRs of reverse proxies whose X-Forwarded-For is trusted for the client address, used by login rate limits and the audit log. none trusts no proxy. Narrow it to your proxy's address if clients can reach the server directly from a private network. | | --metrics-token | ARKEEP_METRICS_TOKEN | (empty) | When set, /metrics requires Authorization: Bearer (Prometheus authorization / bearer_token scrape option). When empty, /metrics is public. | | --telemetry | ARKEEP_TELEMETRY | true | Send anonymous usage stats (opt-out) | | --grpc-insecure | ARKEEP_GRPC_INSECURE | false | Disable TLS for gRPC transport — development and same-machine deployments only |

Generating secrets:

# Secret key (AES-256, must be kept stable — changing it invalidates all stored credentials)
openssl rand -hex 32

Agent secret (any random string)

openssl rand -hex 24

PostgreSQL DSN example:

postgres://arkeep:password@localhost:5432/arkeep?sslmode=require

Agent Configuration

| Flag | Env | Default | Description | |---|---|---|---| | --server-addr | ARKEEP_SERVER_ADDR | localhost:9090 | Server gRPC address (host:port) | | --agent-secret | ARKEEP_AGENT_SECRET | — | Shared secret (must match server) | | --state-dir | ARKEEP_STATE_DIR | ~/.arkeep | Directory for agent state and extracted binaries | | --docker-socket | ARKEEP_DOCKER_SOCKET | (platform default) | Docker socket path | | --log-level | ARKEEP_LOG_LEVEL | info | Log level | | --server-http-addr | ARKEEP_SERVER_HTTP_ADDR | (derived from --server-addr) | Base URL of the server HTTP API used for enrollment. Required when the server is behind a TLS-terminating reverse proxy (e.g. https://arkeep.example.com). Default: --server-addr host with port 8080 over plain HTTP. The agent generates its private key and sends only a certificate request, but over plain HTTP the agent secret travels in clear and the CA certificate cannot be authenticated, so prefer an https:// URL when the server is reachable over HTTPS. | | --grpc-tls-ca | ARKEEP_GRPC_TLS_CA | — | Path to CA certificate for gRPC TLS (only needed when the server uses an external, non-system-trusted cert) | | --grpc-insecure | ARKEEP_GRPC_INSECURE | false | Disable TLS for gRPC transport — development and same-machine deployments only | | --docker-host-root | ARKEEP_DOCKER_HOST_ROOT | /hostfs (auto-detected inside Docker) | Container path where the host filesystem is mounted. Auto-defaults to /hostfs inside Docker — no configuration required. Set only when using a custom mount point. See Local destinations in Docker. | | --hostname | ARKEEP_AGENT_HOSTNAME | (host's hostname) | Hostname reported to the server (and used as the agent's default name) and recorded on restic snapshots. Inside Docker it is read from the host's /etc/hostname via the host filesystem mount, so the container ID is never shown. Set it only to override that, or when the host filesystem is not mounted. |


Local destinations in Docker

When the agent runs in Docker, it can only write to paths that are mounted into the container. Mount the entire host filesystem once at /hostfs — the agent detects it is inside Docker and enables path translation automatically. No environment variable required.

Linux — add to the agent service volumes:

volumes:
  - /:/hostfs:ro   # :ro for backup-only; change to :rw to also restore to local paths

Windows (Docker Desktop) — one entry per drive letter:

volumes:
  - C:/:/hostfs/c:ro
  - D:/:/hostfs/d:ro   # add more drives as needed

With this in place you can type any native host path directly in the Arkeep UI as a backup destination (e.g. C:\Users\Filippo\Downloads or /home/user/backups) and the agent will translate it automatically. No per-directory bind-mounts required.

Advanced: if you mount the host filesystem at a path other than /hostfs, set ARKEEP_DOCKER_HOST_ROOT to your custom mount point.
For binary, systemd, and Helm deployments leave it unset — paths are used as-is.

Restore in Docker

Restore to a custom path works out of the box — enter any host path in the UI (e.g. C:\Users\Filippo\Downloads\restore) and the agent writes the files there via the hostfs mount.

In-place restore (original location) behaviour depends on the /var/lib/docker/volumes mount mode:

| Mount mode | Local filesystem paths | Docker volume paths | |---|---|---| | :ro (default) | Restored normally | Skipped — agent logs a warning. Change to :rw to enable. | | :rw | Restored normally | Restored if the container is stopped; skipped with a warning if running. |

To restore Docker volumes in-place:

  1. Change :ro to :rw for the /var/lib/docker/volumes mount in docker-compose.yml
  2. Stop the containers whose volumes you want to restore
  3. Run the in-place restore from the UI
  4. Restart the containers
Volumes of containers that are still running when the restore starts are automatically skipped with a log entry that names the container. The rest of the restore (local paths + stopped-container volumes) completes normally.


Observability

Health endpoints

The server exposes two health endpoints, both unauthenticated on the HTTP port (default :8080):

| Endpoint | Purpose | Response | |---|---|---| | GET /health/live | Liveness — is the process alive? | 200 ok (plain text, always) | | GET /health/ready | Readiness — can the server serve traffic? | 200 / 503 + JSON |

The /health/ready response checks the database and scheduler:

{
  "status": "healthy",
  "checks": {
    "database":  { "status": "ok", "latency_ms": 2 },
    "scheduler": { "status": "ok" }
  }
}

Returns 503 with "status": "unhealthy" if any check fails (e.g. database unreachable). Docker Compose and Kubernetes probes use /health/ready so containers are automatically restarted or removed from rotation when the database is down.

Prometheus metrics

The server exposes a Prometheus-compatible metrics endpoint at GET /metrics (same port as the HTTP API, default :8080).

| Metric | Type | Labels | Description | |---|---|---|---| | arkeep_jobs_total | Counter | status, job_type | Jobs that reached a terminal state (succeeded, failed, cancelled) | | arkeep_job_duration_seconds | Histogram | job_type | Job wall-clock duration in seconds | | arkeep_agents_connected | Gauge | — | Agents currently holding an active gRPC connection | | arkeep_http_requests_total | Counter | method, route, status_code | HTTP requests handled by the server | | arkeep_http_request_duration_seconds | Histogram | method, route | HTTP request latency in seconds | | go_, process_ | various | — | Go runtime and process metrics (goroutines, GC, file descriptors) |

Prometheus scrape config:

scrape_configs:
  - job_name: arkeep
    static_configs:
      - targets: ['arkeep-server:8080']
    scrape_interval: 15s

Security: /metrics is unauthenticated (like /health/live and /health/ready). In production, restrict access at the reverse-proxy level so only your Prometheus scraper can reach it:

location /metrics {
    allow 10.0.0.0/8;   # your internal network / Prometheus scraper
    deny all;
}

See SECURITY.md for details.


Notifications

Configuring notifications

Navigate to Settings → Notifications in the Arkeep web UI to configure:

  • SMTP — email notifications sent to admin addresses (or a custom recipient list)
  • Webhook — HTTP POST to any URL on every backup event
Both channels support automatic retry with exponential backoff (up to 3 attempts: immediately, +5 min, +30 min). Failed and exhausted deliveries are visible under GET /api/v1/admin/notifications/queue.

Webhook payload

Every event sends a POST request with a JSON body and Content-Type: application/json. The text field is named for Slack/Discord compatibility — it contains the same human-readable message as title but includes full context.

job_success example:

{
  "type": "job_success",
  "title": "Backup completed: my-server",
  "text": "Policy \"my-server\" completed successfully at 2026-01-15T10:30:00Z.",
  "payload": {
    "job_id": "550e8400-e29b-41d4-a716-446655440000",
    "policy_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "policy_name": "my-server"
  },
  "timestamp": "2026-01-15T10:30:00Z"
}

job_failure example:

{
  "type": "job_failure",
  "title": "Backup failed: my-server",
  "text": "Policy \"my-server\" failed at 2026-01-15T10:30:00Z: exit status 1",
  "payload": {
    "job_id": "550e8400-e29b-41d4-a716-446655440000",
    "policy_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "policy_name": "my-server",
    "error": "exit status 1"
  },
  "timestamp": "2026-01-15T10:30:00Z"
}

agent_offline example:

{
  "type": "agent_offline",
  "title": "Agent offline: prod-node-1",
  "text": "Agent \"prod-node-1\" stopped responding at 2026-01-15T10:30:00Z.",
  "payload": {
    "agent_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "agent_name": "prod-node-1"
  },
  "timestamp": "2026-01-15T10:30:00Z"
}

Event types

| type | Trigger | Extra payload fields | |---|---|---| | job_success | Backup completed successfully | job_id, policy_id, policy_name | | job_failure | Backup failed with an error | job_id, policy_id, policy_name, error | | agent_offline | Agent stopped sending heartbeats | agent_id, agent_name |

Signature verification

When a Webhook secret is set in the Arkeep UI, every request includes an X-Arkeep-Signature header carrying an HMAC-SHA256 signature of the raw request body:

X-Arkeep-Signature: sha256=<lowercase hex>

This follows the same convention as GitHub and Stripe webhooks. Always verify the signature before processing the payload.

Go:

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strings"
)

func verifyArkeepSignature(body []byte, secret, header string) bool { expected := strings.TrimPrefix(header, "sha256=") mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) actual := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(actual)) }

Python:

import hmac, hashlib

def verify_arkeep_signature(body: bytes, secret: str, header: str) -> bool: expected = header.removeprefix("sha256=") actual = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, actual)

Node.js:

const crypto = require('crypto')

function verifyArkeepSignature(body, secret, header) { const expected = header.replace(/^sha256=/, '') const actual = crypto.createHmac('sha256', secret).update(body).digest('hex') return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(actual)) }

Integrations

Slack

  1. Create an incoming webhook in your Slack workspace.
  2. Paste the URL into Settings → Notifications → Webhook URL.
  3. Done — Arkeep's text field maps directly to Slack's message body. No additional configuration required.

Discord

  1. In your Discord server, open Channel Settings → Integrations → Webhooks and create a new webhook.
  2. Copy the webhook URL and paste it into Arkeep's Webhook URL setting.
  3. Discord accepts Slack-compatible payloads at the /slack URL suffix — append it if you want richer formatting:
https://discord.com/api/webhooks/<id>/<token>/slack
Without the suffix, Discord renders the text field as the message content, which works for plain-text alerts.

n8n

  1. Add a Webhook trigger node (Method: POST, Response mode: Immediately).
  2. Paste the generated URL into Arkeep's Webhook URL setting.
  3. (Optional) Add a Code node after the trigger to verify X-Arkeep-Signature (see Signature verification).
  4. Add a Switch node that routes on {{ $json.type }} — job_failure, job_success, agent_offline.
  5. Connect downstream nodes (Slack, PagerDuty, Telegram, email, etc.) to each branch.
All event fields are available as {{ $json.payload.policy_name }}, {{ $json.payload.error }}, and so on.

Zapier

  1. Create a new Zap and add a Webhooks by Zapier → Catch Hook trigger.
  2. Paste the generated hook URL into Arkeep's Webhook URL setting.
  3. Send a test backup event from the Arkeep UI to populate Zapier's sample data.
  4. Use type, title, text, payload__policy_name, payload__error, and timestamp fields in your Zap actions.
  5. Add a Filter step to route on type (e.g. only trigger downstream actions for job_failure).

Healthchecks.io

Notifications fire when a job ends, so a backup that never starts (server down, scheduler stuck, policy disabled by mistake) stays silent. A Healthchecks.io check covers that case: it alerts when the expected ping does not arrive on time.

  1. Create a check in Healthchecks (hosted or self-hosted) with a schedule matching the policy's.
  2. Paste its ping URL into the policy's Healthchecks.io field (admins only) and click Send test ping: the test lands in the check's event log without changing its status.
Each backup job of the policy then pings:

| Event | Ping | |-------|------| | Job starts on the agent | /start | | Job succeeds | | | Job fails or is cancelled | /fail |

Every ping carries the job ID as rid, so Healthchecks measures each run's duration correctly, and a short plain-text summary (duration, per-destination snapshot and size, error message) as body. A job interrupted by an agent disconnection sends nothing until it is resumed; if automatic resume gives up, the job pings /fail. Restore jobs never ping. Any ping URL works — https://hc-ping.com/, ping-key + slug URLs (including ?create=1) and self-hosted instances.


Development

Prerequisites

| Tool | Version | Install | |---|---|---| | Go | 1.27+ | go.dev | | Node.js | 22+ | nodejs.org | | pnpm | 9+ | corepack enable | | Docker | any | docker.com | | Task | latest | go install github.com/go-task/task/v3/cmd/task@latest | | protoc | latest | apt install protobuf-compiler / brew install protobuf | | protoc-gen-go | latest | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest | | protoc-gen-go-grpc | latest | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest |

Getting Started

git clone https://github.com/arkeep-io/arkeep
cd arkeep

Download restic and rclone binaries (embedded into the agent at build time)

task deps:download

Generate gRPC code from .proto definitions

task proto

Start the server (http://localhost:8080, gRPC :9090)

task run:server

In a separate terminal — start the GUI dev server with HMR (http://localhost:5173)

task run:gui

In a separate terminal — start an agent pointing to the local server

task run:agent

The GUI dev server proxies API requests to the server, so you can work on frontend and backend simultaneously with hot reload on both sides.

Note: In development, the GUI runs as a separate Vite dev server on port 5173.
In production (binary or Docker), the GUI is compiled and embedded directly inside
the server binary — no separate process or web server is needed.

First login:

Open http://localhost:8080 in your browser. On first access you will be redirected to the setup page where you can create the initial admin account. For safety the setup page only accepts the account within 15 minutes of the server starting; if that window has passed, restart the server and try again.

Project Structure

arkeep/
├── agent/                      # Agent binary
│   ├── cmd/agent/              # Entry point
│   └── internal/
│       ├── connection/         # gRPC client, job stream, state persistence
│       ├── executor/           # Job queue and execution orchestration
│       ├── restic/             # Restic/rclone wrapper (binary extraction, backup, forget, check)
│       ├── docker/             # Docker volume discovery
│       ├── hooks/              # Pre/post backup hook runner
│       └── metrics/            # Host metrics (CPU, RAM, disk) via gopsutil
├── server/                     # Server binary
│   ├── cmd/server/             # Entry point
│   └── internal/
│       ├── api/                # Chi router, HTTP handlers, middleware
│       ├── auth/               # JWT (RS256), local auth, OIDC
│       ├── db/                 # GORM setup, migrations, EncryptedString type
│       ├── repositories/       # Data access layer (explicit queries, no GORM Preload)
│       ├── grpc/               # gRPC server — receives agent streams, dispatches jobs
│       ├── agentmanager/       # In-memory registry of connected agents
│       ├── scheduler/          # gocron-based backup scheduler
│       ├── notification/       # Email (SMTP) and webhook notification senders
│       ├── metrics/            # Prometheus metric collectors and HTTP middleware
│       └── websocket/          # WebSocket hub for real-time GUI updates
├── shared/                     # Code shared between server and agent
│   ├── proto/                  # Protobuf definitions and generated Go code
│   └── types/                  # Shared type definitions
├── gui/                        # Vue 3 PWA frontend
│   └── src/
│       ├── components/         # Reusable UI components (shadcn-vue based)
│       ├── pages/              # Route-level page components
│       ├── stores/             # Pinia state stores
│       ├── services/           # API client, WebSocket client
│       ├── composables/        # Vue composables
│       ├── router/             # Vue Router configuration
│       └── types/              # TypeScript interfaces
├── deploy/
│   ├── docker/                 # Docker Compose files
│   ├── systemd/                # systemd unit file for the agent
│   └── helm/                   # Helm chart
├── go.work                     # Go workspace (agent + server + shared)
└── Taskfile.yml                # Task runner

Available Tasks

task build          # Build all binaries (server + agent, GUI included)
task build:server   # Build server binary (builds GUI first, then embeds it)
task build:agent    # Build agent binary (downloads restic + rclone first)
task build:gui      # Build the Vue GUI only (output to gui/dist/)
task test           # Run all tests (Go + GUI)
task lint           # Run linters (golangci-lint + vue-tsc)
task proto          # Regenerate gRPC code from .proto definitions
task tidy           # Tidy all Go modules
task clean          # Remove build artifacts

task run:server # Run the server in development mode (GUI via task run:gui) task run:agent # Run the agent in development mode task run:gui # Run the GUI dev server with HMR (proxies API to :8080)

task deps:download # Download restic and rclone binaries for the current platform


FAQ

Why Restic under the hood?

Restic is battle-tested, content-addressable, and has excellent deduplication. It handles encryption, chunking, and repository integrity natively. Arkeep adds the management layer on top — scheduling, multi-machine coordination, a GUI, notifications — without reinventing the storage engine.

Can I access my backups without Arkeep?

Yes. Since the underlying engine is Restic, you can always use the restic CLI directly against any repository that Arkeep has created. Your data is never locked in.

Does the agent need root privileges?

No. The agent runs as an unprivileged user. The only exception is Docker volume backup: to access volume mountpoints at /var/lib/docker/volumes/ on Linux, the agent needs to be in the docker group (or run as root). The Docker Compose deployment handles this automatically via the socket mount.

Are pre/post backup hooks safe?

Hooks are shell commands executed by the agent process on the backup target machine. This means hooks run with the same privileges as the agent — typically an unprivileged user, but root if you configured the agent that way, or with Docker socket access if you enabled Docker volume backup.

Treat hook configuration with the same level of trust as SSH access to the machine. For this reason, only admin users can set or modify hook commands. Regular users can view policies but cannot change hook fields.

Supported hook patterns:

# Database dump before backup
pg_dump mydb > /var/backups/mydb.sql

Stop a container before backup, restart it after

(requires the agent's Docker socket mount to be :rw — see docker-compose.agent.yml)

docker stop my-container docker start my-container

Custom script

/opt/scripts/pre-backup.sh

The following patterns are rejected by the server to prevent credential exfiltration:

  • Command substitution: $(...) and backticks
  • Path traversal: ..
  • Internal environment variable references: $RESTIC_, $RCLONE_, $ARKEEP_*
Why does the agent connect to the server, not the other way around?

Pull architecture means agents work behind NAT, firewalls, and dynamic IPs without any port-forwarding or VPN. The server never needs to reach out to agents — agents maintain a persistent gRPC stream and receive jobs through it.

SQLite or PostgreSQL?

SQLite is the default and works well for most deployments. Switch to PostgreSQL if you need high concurrency (many agents running jobs simultaneously) or if you want to run multiple server replicas behind a load balancer.

Is there a Kubernetes deployment?

Yes. A Helm chart is available in deploy/helm/. Set grpc.tls.existingSecret to the name of a TLS Secret (type kubernetes.io/tls) to enable TLS on the gRPC port — cert-manager with Let's Encrypt is the recommended approach. For simpler setups, Docker Compose on a single node is also supported.


Upgrading

Always back up the database before upgrading — see docs/operations/backup-recovery.md for the full procedure and disaster-recovery runbook.

General procedure

  1. Back up the database before every upgrade (see Backing up Arkeep itself).
  2. Pull the new image / download the new binary.
  3. Stop the server.
  4. Start the new server — schema migrations run automatically on startup.
  5. Verify GET /health/ready returns "status": "healthy" before sending traffic.
The agent is independently versioned. Upgrade agents after the server is confirmed healthy. Agents running an older version continue to work during a rolling upgrade — the gRPC protocol is backwards-compatible within a major version.

Docker Compose

docker compose pull
docker compose up -d
docker compose logs -f arkeep-server   # watch for "migrations applied" + "server started"

Standalone binary

# Replace the binary, then restart the service
sudo systemctl stop arkeep-server
sudo cp arkeep-server-new /usr/local/bin/arkeep-server
sudo systemctl start arkeep-server
sudo journalctl -u arkeep-server -f

Helm

helm repo update
helm upgrade arkeep arkeep/arkeep --reuse-values
kubectl rollout status deployment/arkeep-server

After upgrading, verify GET /health/ready returns "status": "healthy" before sending traffic. Schema migrations run automatically on startup.

The agent is independently versioned. Agents running an older version continue to work during a rolling upgrade — the gRPC protocol is backwards-compatible within a major version.

Breaking changes

| From → To | Breaking change | Action required | |---|---|---| | any → 1.0.0 | None — all migrations are additive and run automatically | None |

Backing up Arkeep itself

Arkeep backs up your machines — but who backs up Arkeep? Back up the database
before every upgrade and on a regular schedule.

SQLite:

# One-off snapshot (safe to run while the server is running — SQLite WAL mode)
sqlite3 /var/lib/arkeep/arkeep.db ".backup '/var/lib/arkeep/arkeep.db.bak'"

Or with a timestamp

sqlite3 /var/lib/arkeep/arkeep.db \ ".backup '/var/lib/arkeep/arkeep-$(date +%Y%m%d-%H%M%S).db'"

PostgreSQL:

pg_dump -Fc arkeep > arkeep-$(date +%Y%m%d-%H%M%S).dump

What to back up: the database file (SQLite) or database dump (PostgreSQL), plus the --data-dir directory (contains the CA private key and gRPC certificates). If you lose --data-dir you will need to re-enroll all agents.

The underlying Restic repositories (your actual backup data) are stored on the destinations you configured — they are not affected by an Arkeep server failure.


Roadmap

v1.0 — Production-ready core

  • [x] Restore & restore test
  • [x] Helm chart
  • [ ] Comprehensive test coverage (server + agent + GUI)
  • [ ] Full documentation site

v1.x — Inventory

Central visibility across your entire infrastructure — containers, VMs, bare metal — from a single dashboard.
  • [ ] Host inventory: OS, kernel version, uptime, reboot-pending status
  • [ ] Docker inventory: running containers, images, tags, digests, exposed ports, volume coverage
  • [ ] VM inventory: Proxmox VM and LXC status, resource usage
  • [ ] Backup coverage map: which hosts and volumes have an active, recently verified policy

v2.0 — Update orchestration

Safe, recoverable updates for containers and hosts — with backup verification built into every step.
  • [ ] Docker update detection: current vs latest digest, outdated tag warnings (latest, unversioned)
  • [ ] OS package update detection: pending updates per host, reboot-required flags
  • [ ] Pre-update backup gate: updates blocked or warned if no recent verified backup exists
  • [ ] One-click Docker updates with automatic rollback on failed health check
  • [ ] Proxmox VM and LXC updates
  • [ ] Audit log for every update operation

v2.x — Intelligence

  • [ ] CVE correlation: known vulnerabilities mapped to your running containers and packages
  • [ ] AI advisor: weekly infrastructure reports, CVE triage, update risk summaries, runbook generation
  • [ ] Bandwidth throttling
  • [ ] BYOK encryption key management

Telemetry

Arkeep sends anonymous usage statistics once per day to help prioritize development. **No personal data, backup contents, credentials, or hostnames are ever transmitted.**

What is sent: a stable random instance ID, Arkeep version, OS, number of connected agents, and number of active policies.

Aggregate stats are public at: https://telemetry.arkeep.io/stats

To opt out: set ARKEEP_TELEMETRY=false or pass --telemetry=false.


Contributing

Contributions are welcome. Please read CONTRIBUTING.md first.

Support Arkeep

Hey 👋 I’m Filippo, the independent maintainer of Arkeep.

Arkeep is built around a simple idea: before touching production, you should know you have a backup you can actually recover from.

If Arkeep saves you time, helps your workflow, or gives you more confidence managing your infrastructure, you can support the work behind it through GitHub Sponsors.

Sponsoring is a bit like buying me a coffee for open source: it helps me spend more time on bug fixes, documentation, releases, restore testing, and the next pieces of the roadmap, from inventory to safer update orchestration.

Become a sponsor

Sponsors

Thank you to everyone who has sponsored Arkeep, past and present. Your support keeps the project independent and moving forward.

123soleil

License

Arkeep is licensed under the Apache License 2.0.

Copyright 2026 Filippo Crotti / Arkeep Contributors

Chat with me