Deployment Guide

Production deployment strategies for the ServerBee server and agents.

This guide covers production deployment best practices for ServerBee: Railway, Docker, systemd, reverse proxy configuration, TLS, and backup strategies.

Mobile Push Relay

The Server uses a dedicated HTTPS Relay; Apple signing credentials stay at the publisher, outside self-hosted Servers. See Mobile for explicit subscriptions and Push Relay Operations for isolated configuration, signing, privacy, troubleshooting and genuine-device acceptance. This deployment guide does not provision Apple accounts, install secrets or establish live APNs delivery.

Railway (One-Click Deploy)

Deploy on Railway

The fastest way to get ServerBee running. Click the button above, then configure the environment variables:

SERVERBEE_LOG__LEVEL="info"                    # Log level (trace/debug/info/warn/error)

SERVERBEE_RETENTION__RECORDS_DAYS="7"          # Raw metrics retention in days
SERVERBEE_RETENTION__RECORDS_HOURLY_DAYS="90"  # Hourly aggregates retention in days
SERVERBEE_RETENTION__AUDIT_LOGS_DAYS="180"     # Audit log retention in days
SERVERBEE_SCHEDULER__TIMEZONE="UTC"            # Timezone for daily traffic aggregation (e.g. Asia/Shanghai)

SERVERBEE_OAUTH__BASE_URL=""                   # OAuth callback URL (e.g. https://xxx.up.railway.app)
SERVERBEE_OAUTH__GITHUB__CLIENT_ID=""          # GitHub OAuth Client ID
SERVERBEE_OAUTH__GITHUB__CLIENT_SECRET=""      # GitHub OAuth Client Secret
SERVERBEE_OAUTH__ALLOW_REGISTRATION="false"    # Auto-create accounts on first OAuth login (true=open, false=linked only)

After deploying:

  1. Add a Volume mounted at /data to persist data across deploys
  2. Configure your agents to connect using the Railway-provided URL
  3. On first start, the server auto-creates an admin account with a randomly generated password. Open the Railway deploy logs and look for the highlighted credentials banner to retrieve it. On first login you must change this password and may optionally choose a different username.

Railway automatically assigns a port and provides HTTPS. No need to configure SERVERBEE_SERVER__LISTEN or TLS certificates.

Selecting a Railway Version

The source template pins the repository's current package version, currently 1.0.0-beta.6. This avoids silently deploying the older image behind :latest while ServerBee has no stable 1.x release.

To pin a specific version (stable or pre-release), add the following variable under Variables on the Railway service:

SERVERBEE_IMAGE_TAG=1.0.0-beta.6

Save and trigger a redeploy. Check the release page before changing this pin. Future prereleases also publish a moving beta tag, while latest moves only for suffix-free stable releases; an exact version remains the safest repeatable choice.

The Dockerfile exposes this switch via ARG SERVERBEE_IMAGE_TAG=1.0.0-beta.6. Railway injects service variables as both build args and runtime environment variables; since this variable is not part of the SERVERBEE_* configuration schema, it has no runtime side effects.

Bootstrap Installer

On Linux hosts, the bootstrap installer is the quickest binary/systemd path:

# Install the server
curl -fsSL https://raw.githubusercontent.com/ZingerLittleBee/ServerBee/main/deploy/install.sh | sudo sh -s -- server

# Install an agent
curl -fsSL https://raw.githubusercontent.com/ZingerLittleBee/ServerBee/main/deploy/install.sh | sudo sh -s -- agent \
  --server-url http://your-server-ip:9527 \
  --enrollment-code YOUR_ONE_TIME_CODE

After installation, manage your deployment with the serverbee CLI (automatically installed to /usr/local/bin/serverbee):

sudo serverbee status
sudo serverbee upgrade -y
sudo serverbee upgrade --channel beta -y   # Explicitly stay on prereleases
sudo serverbee restart
sudo serverbee config
sudo serverbee env
sudo serverbee uninstall agent -y

install server / install agent are bootstrap commands. If the binary already exists in /usr/local/bin, the installer adopts it instead of overwriting it. Use upgrade (or replace the binary manually) when you need to roll out a refreshed release binary to an existing host.

The default auto channel selects the newest suffix-free stable release when one exists, otherwise it falls back to the newest prerelease. The installer saves that policy per component and reuses it for upgrades. Pass --channel stable or --channel beta to opt into one track explicitly, or --version vX.Y.Z-beta.N to pin an exact release. The CLI refuses to replace an installed version with an older SemVer.

Docker Compose is the simplest way to deploy ServerBee in production. First create the configuration file (a missing bind source must not become a directory):

mkdir -p config
cat > config/server.toml <<'EOF'
[server]
data_dir = "/data"
EOF

Then create docker-compose.yml beside the config directory:

services:
  serverbee-server:
    image: ghcr.io/zingerlittlebee/serverbee-server:1.0.0-beta.6
    container_name: serverbee-server
    restart: unless-stopped
    ports:
      - "127.0.0.1:9527:9527"
    volumes:
      - serverbee-data:/data
      - ./config/server.toml:/etc/serverbee/server.toml:ro
    environment:
      - SERVERBEE_AUTH__SECURE_COOKIE=true
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:9527/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

volumes:
  serverbee-data:

Binding to 127.0.0.1:9527 instead of 0.0.0.0:9527 ensures the server is only accessible through the reverse proxy, not directly from the internet.

Start the service:

docker compose up -d

View logs:

docker compose logs -f serverbee-server

Build from Source

Developers and users with customization needs can build ServerBee from source. This requires Rust 1.89+ and Bun 1.x (or Node.js 22+).

# Clone the repo
git clone https://github.com/ZingerLittleBee/ServerBee.git
cd ServerBee

# Build the frontend
cd apps/web
bun install
bun run build
cd ../..

# Build the server (embeds the frontend via rust-embed)
cargo build --release -p serverbee-server

# Build the agent
cargo build --release -p serverbee-agent

The binaries will be located in target/release/:

  • serverbee-server — the server, with the frontend assets embedded
  • serverbee-agent — the metrics agent

Start the server:

./target/release/serverbee-server

After deploying the compiled binaries to the target host, use the systemd service configuration below to manage the processes.

Systemd Services

For deployments without Docker, use systemd to manage the server and agent processes.

If you do not need hand-written unit files, prefer the bootstrap installer above. It creates the config files and systemd units for you, then the serverbee CLI handles upgrades, restarts, and config edits.

Note: the unit examples below use custom paths (/usr/local/bin, /var/lib/serverbee, /etc/serverbee) to illustrate a manual deployment. The bootstrap installer uses a different layout — binaries in /opt/serverbee/bin/, config in /opt/serverbee/etc/, data in /opt/serverbee/data/ — managed by the serverbee CLI, with no hand-written units.

Server Service

Create /etc/systemd/system/serverbee-server.service:

[Unit]
Description=ServerBee Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/serverbee-server
Restart=always
RestartSec=5
User=serverbee
Group=serverbee
WorkingDirectory=/var/lib/serverbee

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/serverbee /var/log/serverbee
PrivateTmp=true

# Resource limits
MemoryMax=512M

[Install]
WantedBy=multi-user.target

Create the service user and directories:

# Create system user
sudo useradd -r -s /sbin/nologin -d /var/lib/serverbee serverbee

# Create directories
sudo mkdir -p /var/lib/serverbee /var/log/serverbee /etc/serverbee
sudo chown serverbee:serverbee /var/lib/serverbee /var/log/serverbee

# Place configuration
sudo cp server.toml /etc/serverbee/server.toml

# Place binary
sudo cp serverbee-server /usr/local/bin/
sudo chmod +x /usr/local/bin/serverbee-server

A production server.toml for systemd:

[server]
listen = "127.0.0.1:9527"
data_dir = "/var/lib/serverbee"

[log]
level = "info"
file = "/var/log/serverbee/server.log"

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable serverbee-server
sudo systemctl start serverbee-server
sudo systemctl status serverbee-server

Agent Service

See the Agent Setup guide for a complete systemd service unit file.

Reverse Proxy

Running ServerBee behind a reverse proxy is strongly recommended in production. The proxy provides TLS termination, HTTP/2, and additional security headers.

Pick one public access URL and keep the browser URL, the agent's server_url, and the cookie setting aligned:

ScenarioPublic URLauth.secure_cookieDocker Environment VariableAgent URL
Direct IP over HTTPhttp://203.0.113.10:9527falseSERVERBEE_AUTH__SECURE_COOKIE=falsehttp://203.0.113.10:9527
Domain through HTTPS reverse proxyhttps://monitor.example.comtrueSERVERBEE_AUTH__SECURE_COOKIE=true or omit the variablehttps://monitor.example.com

For domain access, create a DNS A or AAAA record pointing to the server, terminate HTTPS in Nginx, Caddy, or Traefik, and proxy traffic to ServerBee on 127.0.0.1:9527.

If you installed with the quick-start script, it writes auth.secure_cookie = false for direct HTTP access. Before switching that same installation to HTTPS, update /opt/serverbee/etc/server.toml:

[auth]
secure_cookie = true

Then restart the server:

sudo systemctl restart serverbee-server

If ServerBee was installed with the install script, you can also let the CLI configure Caddy HTTPS automatically:

sudo serverbee domain setup --domain monitor.example.com --email admin@example.com -y

This command verifies that the domain resolves to the current server, installs and configures Caddy, changes ServerBee to listen only on 127.0.0.1:9527, and sets auth.secure_cookie = true. If DNS is not ready yet, it stops and prints the A/AAAA record you need to add.

Nginx

upstream serverbee {
    server 127.0.0.1:9527;
}

server {
    listen 80;
    server_name monitor.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name monitor.example.com;

    ssl_certificate     /etc/letsencrypt/live/monitor.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/monitor.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        proxy_pass http://serverbee;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket support (required for agents, browser updates, and terminal)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Long timeouts for persistent WebSocket connections
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;

        # Disable buffering for real-time data
        proxy_buffering off;
    }
}

Caddy

Caddy handles TLS automatically with Let's Encrypt:

monitor.example.com {
    reverse_proxy 127.0.0.1:9527 {
        # WebSocket support is automatic in Caddy
    }
}

That is the entire Caddy configuration needed. Caddy handles HTTPS certificates, HTTP/2, WebSocket upgrades, and security headers by default.

Traefik

Traefik integrates with Docker via labels — no separate config file needed. Traefik automatically detects WebSocket connections, so no extra configuration is required.

services:
  serverbee-server:
    image: ghcr.io/zingerlittlebee/serverbee-server:1.0.0-beta.6
    volumes:
      - serverbee-data:/data
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.serverbee.rule=Host(`monitor.example.com`)"
      - "traefik.http.routers.serverbee.entrypoints=websecure"
      - "traefik.http.routers.serverbee.tls.certresolver=letsencrypt"
      - "traefik.http.services.serverbee.loadbalancer.server.port=9527"
    restart: unless-stopped
    networks:
      - traefik

networks:
  traefik:
    external: true

volumes:
  serverbee-data:

TLS / HTTPS

For production deployments, always use HTTPS. The recommended approaches:

Let's Encrypt with Certbot (Nginx)

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d monitor.example.com

Certbot will automatically configure nginx and set up certificate renewal.

Let's Encrypt with Caddy

Caddy obtains and renews certificates automatically. No additional setup is needed.

Manual Certificates

If you have your own certificates, place them in a secure directory and reference them in your reverse proxy configuration. Ensure the certificate files are readable by the proxy process and that you have a renewal process in place.

Agent Configuration for HTTPS

When your server is behind HTTPS, update the agent's server_url accordingly:

server_url = "https://monitor.example.com"

The agent automatically handles WebSocket connections using the provided URL.

ServerBee does not terminate TLS itself. All HTTPS/WSS encryption is handled by the reverse proxy in front of it (Nginx/Caddy/Traefik). This keeps the server implementation simple and lets you manage certificates in one place.

When using HTTPS, keep auth.secure_cookie = true in your server configuration. Leaving it false may still allow login, but it removes the browser's Secure cookie protection and is not appropriate for production.

Backup and Restore

What to Back Up

SQLite stores monitoring records, users, settings, and uploaded widget packages. Several assets are separate files. A database download alone does not restore the complete installation:

ItemLocationIncluded in database backupDescription
Database{data_dir}/serverbee.dbYesDefault database.path; includes database-backed settings and widget packages
Brand assets{data_dir}/brand/NoUploaded logo and favicon; the database stores their URLs, not the image bytes
Downloaded GeoIP Country database{data_dir}/dbip-country-lite.mmdbNoDB-IP Lite Country database, if downloaded
Downloaded ASN database{data_dir}/dbip-asn-lite.mmdbNoDB-IP Lite ASN database, if downloaded
Custom MMDB filesgeoip.mmdb_path, asn.mmdb_pathNoPreserve these configured files separately if they are outside data_dir
Server configuration/opt/serverbee/etc/NoInstaller configuration and release policy; also preserve external environment files and secrets
Agent configuration and state/opt/serverbee/etc/agent.toml, configured state_dir and security.data_dirNoBack up on each Agent host, including its run token; the Server backup does not copy remote files

serverbee.db-wal may contain changes not yet in the main database. Use SQLite .backup or the backup API for a running server, rather than copying its database and sidecars independently. A stopped full-directory snapshot preserves any remaining serverbee.db-wal and serverbee.db-shm files together.

Backup Strategy

The following examples use the installer defaults: /opt/serverbee/data/serverbee.db and /opt/serverbee/etc/. Substitute your actual data directory, database filename, configuration directory, and any external MMDB paths. For Docker, the example assumes the Compose layout above (/data, config/server.toml, and docker-compose.yml).

Run host filesystem commands as root, install the sqlite3 CLI, and keep backup sets private. Create one timestamped directory for the database and its matching assets:

set -e
umask 077
SERVERBEE_BACKUP_DIR="/backups/serverbee-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$SERVERBEE_BACKUP_DIR"

Option 0: Built-in admin backup API, database only

The admin-only /api/settings/backup endpoint uses SQLite VACUUM INTO and exports only the database. It does not include brand images, MMDB files, configuration, or Agent state. Store an admin-scoped API key in the environment:

curl --fail --request POST \
  --header "X-API-Key: ${SERVERBEE_ADMIN_API_KEY}" \
  --output "$SERVERBEE_BACKUP_DIR/serverbee.db" \
  https://monitor.example.com/api/settings/backup
test "$(sqlite3 "$SERVERBEE_BACKUP_DIR/serverbee.db" 'PRAGMA integrity_check;')" = ok

If downloading from another host, collect the asset and configuration archives on the Server host and copy them into the same backup set.

Option 1: SQLite backup command, database only

sqlite3 /opt/serverbee/data/serverbee.db ".backup '$SERVERBEE_BACKUP_DIR/serverbee.db'"
test "$(sqlite3 "$SERVERBEE_BACKUP_DIR/serverbee.db" 'PRAGMA integrity_check;')" = ok

For either database-only option, complete the backup set with the following archives. assets.tar.gz saves the data directory except the live database and its sidecars, so it includes brand/ and both downloaded MMDB files when present. config.tar.gz saves the installer configuration directory:

tar czf "$SERVERBEE_BACKUP_DIR/assets.tar.gz" \
  --exclude='./serverbee.db*' -C /opt/serverbee/data .
tar czf "$SERVERBEE_BACKUP_DIR/config.tar.gz" -C /opt/serverbee etc

Keep branding, MMDB downloads, and configuration unchanged while collecting this set. Separate online copies are not an atomic snapshot; use a stopped snapshot below when those files are changing. Add any external geoip.mmdb_path, asn.mmdb_path, environment files, and secrets to the backup set with their original paths recorded. Copy verified backup sets off the ServerBee host.

Option 2: Complete host snapshot, requires stopping the server

set -e
sudo systemctl stop serverbee-server
tar czf "$SERVERBEE_BACKUP_DIR/data.tar.gz" -C /opt/serverbee/data .
tar czf "$SERVERBEE_BACKUP_DIR/config.tar.gz" -C /opt/serverbee etc
sudo systemctl start serverbee-server

The data archive includes the database, any remaining WAL/SHM files, brand assets, and downloaded MMDB files. Save external files separately as described above. If an archive command fails, resolve it before restarting and treating the set as complete.

Option 3: Complete Docker volume and configuration snapshot

set -e
umask 077
SERVERBEE_BACKUP_DIR="$PWD/backups/serverbee-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$SERVERBEE_BACKUP_DIR"
docker compose stop serverbee-server

SERVERBEE_DATA_VOLUME="$(
  docker inspect serverbee-server \
    --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{println .Name}}{{end}}{{end}}'
)"
test -n "$SERVERBEE_DATA_VOLUME"

docker run --rm \
  --mount "type=volume,src=${SERVERBEE_DATA_VOLUME},dst=/data,readonly" \
  --mount "type=bind,src=${SERVERBEE_BACKUP_DIR},dst=/backups" \
  alpine tar czf /backups/data.tar.gz -C /data .
tar czf "$SERVERBEE_BACKUP_DIR/config.tar.gz" config docker-compose.yml

docker compose start serverbee-server

This snapshots everything in the actual /data volume, including brand and downloaded MMDB assets, and archives the separately mounted configuration. Also preserve any Compose .env file, external secrets, and MMDB bind mounts. Do not assume the volume is literally named serverbee-data: Compose usually adds a project prefix, and a wrong name can silently create an empty volume.

Restore

Restore the database and the matching assets/configuration before starting the Server. The example below uses a database-only backup completed with assets.tar.gz and config.tar.gz; it assumes the installer layout and a backup already checked with PRAGMA integrity_check:

set -e
umask 077
SERVERBEE_RESTORE_DIR="/backups/serverbee-20260314T020000Z"
test "$(sqlite3 "$SERVERBEE_RESTORE_DIR/serverbee.db" 'PRAGMA integrity_check;')" = ok
test -f "$SERVERBEE_RESTORE_DIR/assets.tar.gz"
test -f "$SERVERBEE_RESTORE_DIR/config.tar.gz"
sudo systemctl stop serverbee-server

# Preserve the current state before replacing it.
SERVERBEE_BEFORE_RESTORE="/backups/before-restore-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$SERVERBEE_BEFORE_RESTORE"
mv /opt/serverbee/data /opt/serverbee/etc "$SERVERBEE_BEFORE_RESTORE/"

# Restore into an empty directory, without the previous database WAL/SHM.
mkdir -p /opt/serverbee/data
cp "$SERVERBEE_RESTORE_DIR/serverbee.db" /opt/serverbee/data/serverbee.db
tar xzf "$SERVERBEE_RESTORE_DIR/assets.tar.gz" -C /opt/serverbee/data
tar xzf "$SERVERBEE_RESTORE_DIR/config.tar.gz" -C /opt/serverbee

# Restore external MMDB/environment files and service ownership before starting.
# For a dedicated service user: chown -R serverbee:serverbee /opt/serverbee/data
sudo systemctl start serverbee-server

For a stopped host snapshot, extract data.tar.gz into the empty data directory instead of copying serverbee.db and extracting assets.tar.gz. Restore the matching config.tar.gz and external files in either case. Review restored configuration before startup; old environment variables or secret-store values can override the restored TOML.

The admin-only /api/settings/restore endpoint accepts a raw SQLite file, keeps the previous database as .pre-restore, and requires a restart. It restores only the database, leaving file assets and configuration untouched. It is not a complete installation restore; use the stopped procedure above or the Docker recovery runbook when those files must also be restored.

Automated Backups

This cron entry schedules database-only backups. Install sqlite3 and create the destination with sudo install -d -m 0700 /backups first:

# /etc/cron.d/serverbee-backup
0 2 * * * root umask 077; sqlite3 /opt/serverbee/data/serverbee.db ".backup '/backups/serverbee-$(date +\%Y\%m\%d).db'" && find /backups -name 'serverbee-*.db' -mtime +30 -delete

It runs at 2:00 AM and deletes database backups older than 30 days. Schedule the matching asset/configuration archives too, especially after branding, MMDB, or configuration changes; verify and copy complete sets off-host.

Health Checks

ServerBee exposes a health check endpoint:

GET /healthz

Use this to verify the server is running and responsive. Configure it in your monitoring system, Docker health check, or load balancer.

Docker Health Check

The Docker Compose example above includes a health check. Three consecutive failures mark the container unhealthy; Docker does not restart a merely unhealthy container. The restart policy only reacts when the process exits, so monitor the health state externally and investigate or restart it deliberately.

External Monitoring

If you use an external uptime monitoring service, point it at:

https://monitor.example.com/healthz

This creates a "monitor the monitor" setup, so you are alerted if ServerBee itself goes down.

Resource Requirements

ServerBee is designed for lightweight VPS instances:

ComponentMinimumRecommended
Server CPU1 vCPU2 vCPU
Server RAM128 MB256 MB
Server Disk100 MB + data1 GB+ (depends on retention)
Agent CPUNegligible< 1% of 1 core
Agent RAM~27 MB measured cgroup steady state30–40 MB headroom

Database size depends on server count, enabled writers, sampling intervals, and retention settings. Use Storage & Sizing for the measured 30-day baseline, feature multipliers, and WAL headroom instead of combining rough estimates from different retention windows.

Official Linux release binaries and Docker images use musl; MALLOC_ARENA_MAX does not affect those builds. Measure the version and workload you deploy rather than sizing from the historical glibc notes. See Resource Usage.

Security Checklist

Before exposing ServerBee to the internet:

Upgrading

ServerBee runs database migrations automatically on startup, so no manual migration step is needed after an upgrade. Back up the complete data directory, configuration, and external assets before upgrading.

Docker:

docker compose pull
docker compose up -d            # restarts and runs migrations automatically
docker compose ps

Binary:

wget https://github.com/ZingerLittleBee/ServerBee/releases/download/v1.0.0-beta.6/serverbee-server-linux-amd64
sudo systemctl stop serverbee-server
sudo mv serverbee-server-linux-amd64 /usr/local/bin/serverbee-server
sudo chmod +x /usr/local/bin/serverbee-server
sudo systemctl start serverbee-server

If you deployed with the install script, sudo serverbee upgrade -y handles the binary download, replacement, and restart for you.

Uninstalling and Purging

sudo serverbee uninstall <server|agent> stops and removes the managed service/container and executable, but intentionally preserves configuration and data for a reinstall. Add --purge -y only after taking a verified backup; it also removes installer-managed config, Server data or Docker volumes/images, and removes the management CLI when no components remain.

Review these leftovers separately:

  • Agent runtime state defaults to /var/lib/serverbee/capability_grants.json and /var/lib/serverbee/security. A custom state_dir or security.data_dir may point elsewhere; remove it only after confirming no remaining Agent uses it.
  • Domain setup may have installed Caddy and added a domain block to /etc/caddy/Caddyfile. The uninstaller does not delete packages or shared proxy configuration. Back up the file, remove only the ServerBee domain block, run sudo caddy validate --config /etc/caddy/Caddyfile, then sudo systemctl reload caddy. Remove Caddy itself only if no other site uses it.
  • A non-purge Docker uninstall preserves its Compose file, config, and named volume. Inspect the exact volume with docker inspect serverbee-server before deleting anything.

After cleanup, verify that no serverbee-* service/container is running and that ports 9527 (and, if removing the proxy, 80/443) have the expected owners.

Database Migration Failures and Recovery

ServerBee runs database migrations during Rust server startup. Migrations finish before the HTTP listener starts; when a migration fails, the process exits, and Docker's restart: unless-stopped or a systemd restart policy may put the service into a restart loop.

The current release does not create an automatic pre-migration snapshot and cannot automatically roll migrations back. A Docker volume persists files, but it is not a versioned restore point. All migrations move forward, and migration down() implementations do not restore the old schema.

Failure Scenarios

CategoryTypical causesRecommended response
Storage is not writableIncorrect /data permissions, read-only mount, exhausted disk space or inodesStop the restart loop, repair storage, then retry the migration
Database lockAnother ServerBee instance, backup process, or SQLite client holds a long-lived lockEnsure there is one database writer, stop the conflicting process, then retry
Database corruptionDamaged SQLite database, WAL, or SHM file; abrupt host power lossPreserve the failed state, run integrity checks, and prefer a verified backup
Schema driftTables, indexes, or migration history were edited manuallyDo not fabricate migration records; restore a compatible backup or ship a forward repair migration
Existing data violates a new constraintDuplicate values, null values, orphaned foreign keys, or unexpected legacy formatsLocate and repair the data in a database copy, then retry the migration
Migration implementation defectIncorrect SQL, backfill logic, or target schemaPreserve the database and deploy a new image containing a corrected migration
Interrupted executionThe container was killed or the host restarted during a migrationSnapshot the failed state first; verify schema and migration history before retrying
Multiple instances competeMultiple containers migrate the same SQLite file concurrentlyScale down to one instance; multiple Server instances must not share writes to one SQLite database
Incompatible application rollbackA new migration succeeded, then only the application image was downgradedRestore the matching pre-upgrade database backup or run an application version compatible with the new schema

Step 1: Stop the Restart Loop

Docker Compose:

docker compose stop serverbee-server

If an external tool still keeps starting the container, temporarily disable its container-level restart policy:

docker update --restart=no serverbee-server
docker stop serverbee-server

systemd:

sudo systemctl stop serverbee-server
sudo systemctl reset-failed serverbee-server

Stopping the service does not delete its Docker volume or database. Do not run docker compose down -v; the -v flag deletes Compose-managed volumes.

Step 2: Read the Migration Error

Docker:

docker logs --tail 200 serverbee-server

systemd:

sudo journalctl -u serverbee-server -b -n 200 --no-pager

A successful migration writes Database migrations complete to the log. If that message is absent, investigate the first database or migration error rather than the later restart messages.

Step 3: Preserve the Failed State

Create a complete copy even when the database may be partially migrated or damaged. It is both the input for repair and protection against later attempts overwriting evidence.

Docker:

SERVERBEE_DATA_VOLUME="$(
  docker inspect serverbee-server \
    --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{println .Name}}{{end}}{{end}}'
)"
test -n "$SERVERBEE_DATA_VOLUME"

mkdir -p backups
docker run --rm \
  --mount "type=volume,src=${SERVERBEE_DATA_VOLUME},dst=/data,readonly" \
  --mount "type=bind,src=$(pwd)/backups,dst=/backups" \
  alpine \
  tar czf /backups/serverbee-failed-migration-$(date -u +%Y%m%dT%H%M%SZ).tar.gz -C /data .

For systemd or binary deployments, copy the entire data directory after stopping the service, not just the main database file:

sudo mkdir -p /backups
sudo cp -a /opt/serverbee/data \
  "/backups/serverbee-failed-migration-$(date -u +%Y%m%dT%H%M%SZ)"

Preserve the matching configuration, external MMDB files, environment files, and secrets alongside the failed data snapshot. They may explain the failure and are required for a complete restore.

Step 4: Inspect the Database State

Run checks against an extracted copy of the failed snapshot, not the only production volume:

mkdir -p recovery-data
SERVERBEE_FAILED_BACKUP="$(ls -1t backups/serverbee-failed-migration-*.tar.gz | head -n 1)"
test -f "$SERVERBEE_FAILED_BACKUP"
tar xzf "$SERVERBEE_FAILED_BACKUP" -C recovery-data

sqlite3 recovery-data/serverbee.db "PRAGMA integrity_check;"
sqlite3 recovery-data/serverbee.db "PRAGMA foreign_key_check;"
sqlite3 recovery-data/serverbee.db \
  "SELECT * FROM seaql_migrations ORDER BY applied_at;"
  • PRAGMA integrity_check should return ok.
  • PRAGMA foreign_key_check should normally return no rows.
  • seaql_migrations shows migrations recorded as successful; the table may not exist if a brand-new database failed before migration bookkeeping was created.

Do not delete rows from seaql_migrations or manually mark a failed migration as successful. When migration history and the actual schema disagree, a later release can fail in a more difficult-to-recover state.

Step 5: Choose a Recovery Path

Environmental failure

If the cause was disk space, permissions, a read-only mount, or a temporary database lock, repair the root cause and restart. SeaORM will retry migrations not recorded as successful:

docker update --restart=unless-stopped serverbee-server
docker compose up -d serverbee-server
docker logs -f serverbee-server

Incompatible existing data or schema

Identify conflicting data in a database copy. Prefer a forward repair migration that handles the legacy state. If manual data repair is unavoidable, validate the complete migration and application startup against the copy before applying the same reviewed operations to production.

Migration code defect

Do not repeatedly restart the same failing image. Preserve the failed state and deploy a new image containing a corrected migration. Switching only to the old image is not a database rollback; it is safe only when you have established that the failed migration made no schema changes. Otherwise restore the pre-upgrade backup.

Database corruption

Prefer the latest backup that passes integrity checks. SQLite .recover is a last-resort data salvage tool when no usable backup exists; its output may be incomplete and must be validated in isolation.

Restore a Docker Backup into a New Volume

Restore into a new volume instead of overwriting the failed volume. This preserves the failed state for further investigation:

SERVERBEE_RECOVERY_VOLUME="serverbee-data-recovery-$(date -u +%Y%m%dT%H%M%SZ)"
docker volume create "$SERVERBEE_RECOVERY_VOLUME"

docker run --rm \
  --mount "type=volume,src=${SERVERBEE_RECOVERY_VOLUME},dst=/data" \
  --mount "type=bind,src=$(pwd)/backups/serverbee-20260314T020000Z,dst=/backups,readonly" \
  alpine \
  tar xzf /backups/data.tar.gz -C /data

echo "$SERVERBEE_RECOVERY_VOLUME"

# Inspect the matching Compose/configuration archive before restoring it.
mkdir -p recovered-config
tar xzf backups/serverbee-20260314T020000Z/config.tar.gz -C recovered-config

Review recovered-config/config/server.toml and recovered-config/docker-compose.yml, restore them to the deployment project, and reapply the matching external MMDB mounts, environment files, and secrets. The data archive restores brand/ and downloaded MMDB files; it cannot restore files outside /data.

Then point the existing serverbee-data logical Compose volume at the newly created volume:

volumes:
  serverbee-data:
    external: true
    name: serverbee-data-recovery-20260722T103000Z

Confirm that name exactly matches the preceding command's output before starting the service. Restoring an older database may run migrations again, so first decide whether to run the original application version or a newer version containing the migration fix.

Verify Recovery

docker compose up -d serverbee-server
docker compose ps
docker logs --tail 200 serverbee-server
curl --fail http://127.0.0.1:9527/healthz

Consider recovery complete only when all of the following are true:

  • The logs contain Database migrations complete.
  • The container remains running and its health check passes.
  • PRAGMA integrity_check returns ok.
  • You can sign in and read existing servers, alerts, and historical records.
  • Previously configured logos, favicons, and GeoIP/ASN databases are available.
  • Database lock, foreign key, and missing table or column errors do not continue.

Prevention

  • Create and verify a complete data/configuration backup before every upgrade; the current upgrade command does not create one automatically.
  • Copy backups off the Docker host so one disk failure cannot damage both the production volume and its local backups.
  • Keep SQLite deployments at one Server instance; do not share one writable database file among multiple containers.
  • Check available disk space before upgrading, and use an explicit image version tag so the migration source is identifiable.
  • Keep the pre-upgrade image and its matching database, asset, and configuration backups. Database rollback depends on the backup, not just the old image.
  • Wait for the migration success log and health check after an upgrade; a successful docker compose up -d return is not sufficient evidence.