Skip to Content

Production Guide: Deploy Beszel with Docker Compose + Caddy on Ubuntu

Lightweight multi-server infrastructure monitoring with automatic HTTPS and hub-and-agent architecture

Operations teams managing more than two servers quickly discover that shell-hopping to check disk pressure, CPU saturation, and container health does not scale. Beszel solves that problem with a lightweight hub-and-agent architecture that aggregates real-time metrics from every host in your fleet behind a single web interface. Unlike Grafana + Prometheus stacks that require you to wire together exporters, storage backends, and dashboards from scratch, Beszel ships as a single hub container and a single-binary agent — zero configuration language, zero scrape interval tuning, and a dashboard that is readable in five seconds rather than five hours. It surfaces CPU usage, memory, disk I/O, network throughput, container-level stats for running Docker workloads, and per-mount disk usage, with configurable alerting and a clean dark-themed UI backed by SQLite.

This guide deploys a production-ready Beszel hub using Docker Compose on Ubuntu 22.04 LTS with Caddy as the TLS-terminating reverse proxy. By the end you will have a publicly accessible Beszel dashboard secured with automatic HTTPS, a persistent SQLite volume that survives restarts and upgrades, and a systemd-supervised stack you can add new agents to in under two minutes per host.

Architecture and flow overview

Beszel uses a hub-and-agent model. The hub is a containerized Go application that stores all metrics in an embedded SQLite database and serves the web interface. Agents are lightweight binaries (or containers) installed on each server you want to monitor. Each agent opens an outbound SSH tunnel back to the hub — the hub never needs inbound access to agent hosts, which means agents can run behind NAT or firewalls without exposing any ports on the monitored machine.

Traffic flow for the dashboard: a browser connects to Caddy on port 443, Caddy terminates TLS and reverse-proxies to the Beszel hub container on a localhost port (8090). Caddy handles certificate issuance and renewal automatically via Let's Encrypt ACME HTTP-01 challenge. The hub stores its SQLite database and SSH host key in a named Docker volume that persists across restarts. Agents on remote servers use the hub's public SSH port (typically 45876) to push metrics — this port is exposed directly on the host and must be reachable from agent servers.

Because agents initiate the connection outbound, there is no credential exposure on the monitored servers beyond the agent's own SSH keypair. The hub authenticates each agent by the public key the agent generates on first run, which you register in the hub's web UI when adding the system.

Prerequisites

  • Ubuntu 22.04 LTS server with root or sudo access for the hub machine
  • Minimum 512 MB RAM and 1 vCPU (Beszel hub is extremely lightweight; runs comfortably alongside other workloads)
  • Docker Engine 24+ and Docker Compose Plugin installed on the hub server
  • Caddy 2.7+ installed as a systemd service on the hub server
  • A domain name (e.g., beszel.yourdomain.com) with an A record pointing to the hub server's public IP, fully propagated before starting Caddy
  • UFW or equivalent firewall allowing inbound ports 22 (SSH), 80 (Caddy ACME), 443 (HTTPS dashboard), and 45876 (Beszel agent port) only
  • Target servers you want to monitor must have SSH outbound access to the hub's port 45876

Step-by-step deployment

1. Install Docker Engine and Compose Plugin

sudo apt update && sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker

2. Install Caddy from the official repository

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy
sudo systemctl enable --now caddy

3. Create the project directory structure

sudo mkdir -p /opt/beszel
cd /opt/beszel

4. Write the Docker Compose file

cat > /opt/beszel/docker-compose.yml << 'EOF'
version: "3.8"

services:
  beszel:
    image: henrygd/beszel:latest
    container_name: beszel
    restart: unless-stopped
    ports:
      - "127.0.0.1:8090:8090"
      - "0.0.0.0:45876:45876"
    volumes:
      - beszel_data:/beszel_data
    environment:
      - TZ=UTC
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8090/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s

volumes:
  beszel_data:
    driver: local
EOF

Key points about this configuration: port 8090 is bound only to localhost so the dashboard is never exposed directly — all external traffic goes through Caddy. Port 45876 is bound to all interfaces so agents on remote servers can connect. The named volume beszel_data stores the SQLite database, the SSH host key pair, and any configuration changes you make through the UI.

5. Configure the Caddyfile

sudo tee /etc/caddy/Caddyfile << 'EOF'
beszel.yourdomain.com {
    reverse_proxy 127.0.0.1:8090
    encode gzip
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
    log {
        output file /var/log/caddy/beszel_access.log
        format json
    }
}
EOF

sudo mkdir -p /var/log/caddy
sudo systemctl reload caddy

Replace beszel.yourdomain.com with your actual domain. Caddy will obtain and auto-renew a Let's Encrypt certificate on first startup.

6. Harden the firewall with UFW

sudo ufw allow 22/tcp comment "SSH"
sudo ufw allow 80/tcp comment "Caddy ACME"
sudo ufw allow 443/tcp comment "HTTPS dashboard"
sudo ufw allow 45876/tcp comment "Beszel agent port"
sudo ufw --force enable
sudo ufw status verbose

7. Start the Beszel hub stack

cd /opt/beszel
docker compose up -d
docker compose ps
docker compose logs -f --tail=30

8. Deploy the Beszel agent on a monitored server

Run the following on each server you want to add to Beszel. The agent binary is a single file with no dependencies and runs as a systemd service:

# On the target server to be monitored
curl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/install-agent.sh \
  | bash -s -- \
  -port 45876 \
  -key "PASTE_PUBLIC_KEY_FROM_HUB_UI_HERE" \
  -hub beszel.yourdomain.com:45876

You obtain the public key from the Beszel hub web UI: navigate to Systems → Add System and copy the displayed SSH public key before running the agent install script. After the agent starts it will connect back to the hub within a few seconds and you can click Add System to confirm the connection.

Configuration and secrets handling

Beszel's primary secrets are the admin account password you set on first login and the SSH keypair the hub generates automatically at startup. The keypair is stored in the beszel_data Docker volume at /beszel_data/id_ed25519 and /beszel_data/id_ed25519.pub. Do not delete or rotate the hub's private key without re-registering all agents, since agents authenticate using the hub's public key they received at registration time.

For the admin password, use a strong randomly generated value and store it immediately in a secrets manager such as Vaultwarden, HashiCorp Vault, or your team's password manager. Beszel does not support LDAP or OAuth out of the box at the time of writing, so the local admin account is the sole access mechanism for the web UI. If you are running Authentik or Keycloak in your stack already, consider placing Beszel behind Authentik's proxy provider to add SSO enforcement and MFA at the reverse-proxy layer.

For alert notifications, Beszel supports webhooks, Slack, Discord, Telegram, Gotify, and SMTP. Configure these under Settings → Notifications in the web UI. Store webhook URLs and SMTP credentials outside the container — paste them into the UI rather than injecting them as environment variables, since Beszel stores notification config in its SQLite database rather than reading from environment. Take a daily backup of the beszel_data volume to a remote S3-compatible store (Kopia works well here) to protect notification config, agent registrations, and historical metric data.

Verification

After deployment, run through this checklist to confirm the stack is fully operational before you rely on it in production:

# Confirm hub container is running and healthy
docker compose -f /opt/beszel/docker-compose.yml ps

# Confirm hub API responds on localhost
curl -s http://localhost:8090/api/health

# Confirm Caddy is serving HTTPS and cert is valid
curl -sI https://beszel.yourdomain.com | head -6

# Confirm agent port is accepting connections (run from an agent server)
nc -vz beszel.yourdomain.com 45876

# Check hub logs for agent connection events
docker compose -f /opt/beszel/docker-compose.yml logs beszel | grep -i "connected\|agent\|error" | tail -20

After adding a system in the UI, wait approximately 30 seconds for the first metric cycle to complete. The system card should show green status and begin populating CPU, memory, and disk graphs. If the card stays orange (pending), the agent has not yet connected — check firewall rules on both the hub server (port 45876 open) and the agent server (outbound to hub port 45876 allowed).

Common issues and fixes

Agent connects but card stays orange (pending) after 60 seconds. Open the Beszel hub logs with docker compose logs beszel. Look for ssh: handshake failed or key mismatch. This almost always means the agent was started with the wrong public key. Re-run the agent install script with the correct key displayed in the hub UI for that specific system.

Cannot reach dashboard — Caddy returns 502 Bad Gateway. The hub container is likely not running or is still starting. Run docker compose -f /opt/beszel/docker-compose.yml ps and check the health status. If the container shows starting, wait 20 seconds and retry. If it shows unhealthy, inspect logs with docker compose logs beszel --tail=50 for port conflicts or permission errors on the data volume.

Let's Encrypt certificate fails to issue. Ensure port 80 is open in UFW and that your DNS A record has propagated (dig +short beszel.yourdomain.com should return your server's IP). Caddy requires port 80 to be reachable for the HTTP-01 ACME challenge even when the final site serves only HTTPS.

Disk usage graphs show zero on agent systems. The agent reports disk stats for the top-level mount point by default. If your data lives on a non-root mount (e.g., /data on a separate block device), add it explicitly: re-run the agent install script with an additional -fs /data flag to include that filesystem in the report.

Hub container restarts after server reboot. If restart: unless-stopped in the Compose file is not triggering auto-start, verify that the Docker daemon itself is enabled to start on boot: sudo systemctl is-enabled docker. If it returns disabled, run sudo systemctl enable docker and reboot to confirm the fix.

FAQ

How many agents can a single Beszel hub handle?

In practice, a hub running on a 2 vCPU / 2 GB RAM server handles 50 to 100 agents with no performance degradation. The SQLite database and Go runtime are both extremely efficient at this scale. Beyond 200 agents you may notice slightly longer page load times for the systems list, but metric collection itself remains unaffected. If you are monitoring hundreds of servers, consider placing the hub on a slightly larger instance (4 vCPU / 4 GB RAM) and enabling regular SQLite WAL-mode checkpoints via the hub's settings page.

Is the historical metric data stored permanently?

Beszel stores metrics at multiple resolutions: high-resolution data (per-record intervals) is retained for a configurable period (default 7 days for 1-minute resolution), and rolled-up hourly and daily summaries are retained indefinitely by default. You can adjust retention policies under Settings → System in the hub UI. The entire SQLite database lives in the beszel_data Docker volume — back it up with docker run --rm -v beszel_data:/data -v /tmp:/backup alpine tar czf /backup/beszel_backup_$(date +%Y%m%d).tar.gz /data.

Can I monitor Docker containers running on agent servers?

Yes. The Beszel agent automatically detects Docker containers on the host when it can read the Docker socket. To enable this, run the agent with -docker flag or, if using the Docker agent image instead of the binary, mount the Docker socket: -v /var/run/docker.sock:/var/run/docker.sock:ro. Container CPU, memory, and network stats then appear in a separate Containers tab on each system card in the hub UI.

Does Beszel support multi-user access with role-based permissions?

Beszel supports multiple user accounts. You can create read-only viewer accounts and assign specific systems to specific users from Settings → Users. This is useful for giving individual teams visibility into their own servers without exposing your entire fleet. Admin accounts have full access including system addition, deletion, and global settings. There is no LDAP sync or SCIM provisioning at the time of writing, so accounts must be created manually or via the Beszel API.

How do I upgrade the Beszel hub to a newer version?

Since the hub image is tagged latest, pull the new image and restart: cd /opt/beszel && docker compose pull && docker compose up -d. Beszel runs automatic SQLite schema migrations on startup, so no manual migration steps are required for most point releases. Always read the GitHub release notes for breaking changes before upgrading major versions, and take a volume backup first as a safety net: docker compose stop && docker run --rm -v beszel_data:/data -v $(pwd):/backup alpine tar czf /backup/pre_upgrade_$(date +%Y%m%d).tar.gz /data && docker compose up -d.

Can I place Beszel behind Authentik or Keycloak for SSO enforcement?

Yes. Because Caddy is your reverse proxy, you can add Authentik's Forward Auth middleware between Caddy and the Beszel hub. This means every request to beszel.yourdomain.com is authenticated by Authentik before it reaches the Beszel container — you get MFA, OIDC SSO, and session management without Beszel needing to implement it natively. Add the Authentik forwardauth Caddyfile snippet documented in the Authentik reverse-proxy guide to your Beszel vhost block, point the trusted proxies at your Authentik instance, and all dashboard access will require a valid Authentik session. Beszel's internal user accounts still work for API calls not routed through the browser.

What is the difference between the binary agent and the Docker agent?

The binary agent (beszel-agent installed via the install script) runs directly on the host as a systemd service and has access to all host-level metrics including filesystems, network interfaces, and hardware sensors. The Docker agent image (henrygd/beszel-agent) runs as a container and can read host metrics only if you mount /proc, /sys, and the Docker socket into the container. For production use the binary agent is recommended because it gives complete host visibility and is simpler to update — just restart the systemd service after replacing the binary. The Docker agent is useful when you cannot install binaries on the host or are running a Kubernetes node where all workloads must be containerized.

Internal links

Talk to us

If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.

Contact Us

Production Guide: Deploy Appwrite with Docker Compose + Caddy on Ubuntu
Self-host an open-source backend-as-a-service platform with authentication, databases, file storage, and serverless functions behind automatic HTTPS.