Introduction: Why Arcane?

Managing Docker from the command line is fine — until it isn't. You start with a few containers. Then compose stacks pile up. Image updates get missed. You lose track of what's running. Opening three terminals to check logs across different services gets old fast.

That's where Arcane comes in.

Arcane is an open-source (BSD-3-Clause) web interface for managing Docker containers, images, networks, volumes, and compose projects. Think of it as a modern, lightweight alternative to Portainer — built with a Go backend, SvelteKit frontend, and SQLite database. It connects to your Docker daemon via the socket (or a socket proxy) and gives you a clean dashboard to monitor and manage everything.

GitHub: getarcaneapp/arcane — 7,400+ stars, active development with new releases shipping on a near-weekly cadence. It's a young project, but it's moving fast.

What Arcane Does

  • Container management — start, stop, restart, pause, kill, inspect, edit, redeploy
  • Compose projects — auto-discovers existing stacks, edit compose files in-browser, real-time logs
  • Image management — pull, build, scan (Trivy), track pending updates
  • Network & volume management — inspect, create, delete, see what's in use
  • Docker Swarm — basic Swarm stack and service management
  • Remote hosts — agent-based multi-host management
  • Templates — reusable compose templates for quick deployment
  • RBAC + OIDC — six predefined roles (plus custom roles) and single sign-on via providers like Keycloak, Authentik, or Okta
  • Mobile-friendly — responsive dark-themed UI

Arcane vs The Alternatives

Feature Arcane Portainer Dockge Komodo
Tech stack Go + SvelteKit Go + Angular Node.js + EJS Go + React
Docker Compose ✅ Full ✅ Full ✅ Full ✅ Full
Docker Swarm ✅ Basic
Remote hosts ✅ Agent-based ✅ Agent-based
Image scanning ✅ (Trivy)
GitOps sync ✅ (Business)
RBAC + OIDC ✅ (Business)
License BSD-3-Clause Freemium MIT BSD-3-Clause
Resource usage Light Heavier Light Medium

When to pick Arcane: You want full Docker management (not just compose), a modern UI, and don't need Kubernetes orchestration. It's the "golden mean" between Dockge (compose-only, largely stagnant) and Portainer (enterprise-heavy, freemium model).


Architecture

Arcane follows a straightforward client-server model:

┌─────────────┐     REST API     ┌──────────────┐     Docker API     ┌──────────────┐
│  SvelteKit   │ ◄─────────────► │   Go Backend │ ◄────────────────► │ Docker Daemon│
│  Frontend    │                  │  (SQLite DB) │  via socket proxy  │              │
└─────────────┘                  └──────────────┘                    └──────────────┘
       ▲                                ▲
       │                                │
    Browser                     /app/data volume
                               (config, projects, DB)
  • Go backend — serves REST API, manages Docker interactions, stores config in SQLite
  • SvelteKit frontend — dark-themed responsive dashboard
  • Docker Socket Proxy — secure middle layer (more on this below)
  • Volume — persistent storage for app data, project configs, and database

Prerequisites

  • Docker 20.10+
  • Docker Compose v2+
  • A user with UID/GID 1000 (check with id)
  • Ports available: 3552 (Arcane), 2375 (socket proxy, internal only)

Running Podman instead of Docker? Arcane targets the Docker API specifically, but Podman's Docker-compatible socket (podman.sock) works with the same compose file below — point the volume mount at your Podman socket path instead of /run/user/1000/docker.sock, and enable podman.socket for your user. I run Podman on my own VPS, and this setup works there with that one change. The socket proxy container itself still needs to be Docker-compatible-API aware, which tecnativa/docker-socket-proxy is.


The Setup: Production-Hardened Docker Compose

Here's the full docker-compose.yaml I use. Every line has a reason.

services:
  arcane:
    image: ghcr.io/getarcaneapp/arcane:latest
    container_name: arcane
    ports:
      - '127.0.0.1:3552:3552'
    volumes:
      - arcane_data:/app/data
    env_file:
      - .env
    depends_on:
      socket-proxy:
        condition: service_healthy
    healthcheck:
      test: ['CMD', '/app/arcane', 'health']
      interval: 30s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

  socket-proxy:
    image: tecnativa/docker-socket-proxy:latest
    container_name: arcane-socket-proxy
    volumes:
      - /run/user/1000/docker.sock:/var/run/docker.sock:ro
    environment:
      - CONTAINERS=1
      - POST=1
      - EXEC=1
      - EVENTS=1
      - NETWORKS=1
      - VOLUMES=1
      - IMAGES=1
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:2375/_ping']
      interval: 10s
      timeout: 5s
      retries: 3
    read_only: true
    tmpfs:
      - /tmp
      - /run
    security_opt:
      - no-new-privileges:true
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 128M
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

volumes:
  arcane_data:
    external: true

networks:
  default:
    external: true
    name: proxy_net

Arcane Service — Line by Line

image: ghcr.io/getarcaneapp/arcane:latest

This is the published image the project ships releases under. Older guides (and some internal PR-build artifacts) reference ghcr.io/getarcaneapp/manager — that naming shows up in the project's CI for pull-request builds, but the tagged releases you actually want to run live at ghcr.io/getarcaneapp/arcane. If a compose file you find elsewhere still says manager, swap it for arcane.

ports: '127.0.0.1:3552:3552'

Bound to localhost only. Not exposed to the internet. Access via SSH tunnel, VPN (Tailscale/ZeroTier), or reverse proxy. This is intentional — Arcane manages your entire Docker host, so it shouldn't be publicly accessible without TLS and authentication.

volumes: arcane_data:/app/data

Named volume for persistent data. Arcane stores its SQLite database, project configs, and user accounts here. Using a named volume (not a bind mount) keeps data managed by Docker and survives container recreation.

env_file: .env

Separates runtime config from orchestration config. Secrets, URLs, and UID/GID go in .env. Things like depends_on, ports, and restart stay in docker-compose.yaml — because they're read by Docker Compose, not the application.

depends_on: socket-proxy (condition: service_healthy)

Arcane won't start until the socket proxy is healthy. Prevents startup race conditions where Arcane tries to connect to Docker before the proxy is ready.

read_only: true

Immutable root filesystem. The container can't modify its own filesystem at runtime. Only /tmp (via tmpfs) and /app/data (via volume) are writable. If an attacker somehow gets code execution inside the container, they can't plant persistent files.

tmpfs: /tmp

Temporary files go to memory-backed tmpfs. Nothing persists on the read-only filesystem.

security_opt: no-new-privileges:true

Prevents privilege escalation. Even if a process inside the container tries to gain capabilities (via setuid, setgid, etc.), it's blocked. This is defense-in-depth — the container already runs as non-root, but this closes escalation paths.

deploy.resources

CPU capped at 1 core, memory at 512MB. Reservations ensure minimum resources (0.25 CPU, 128MB). This prevents Arcane from consuming excessive resources on a busy host. For a management UI, these limits are generous.

logging

JSON file driver with rotation: max 10MB per file, 3 files kept. Prevents log files from growing unbounded and filling your disk. Total log storage capped at 30MB.

healthcheck

Built-in /app/arcane health command. Docker checks every 30 seconds. If it fails 3 times, container is marked unhealthy. Useful for monitoring and automated restart policies.

Socket Proxy Service — Why It Exists

The Docker socket (/var/run/docker.sock) is the most powerful interface on your host. Anyone with access to it can:

  • Create privileged containers
  • Mount the host filesystem
  • Execute arbitrary commands as root
  • Take over the entire machine

Mounting the socket directly into Arcane means if Arcane is compromised, the attacker owns your host. The socket proxy (tecnativa/docker-socket-proxy) acts as a firewall for the Docker API.

How it works:

  1. Socket proxy mounts the Docker socket as read-only
  2. It exposes only a filtered TCP API on port 2375
  3. Arcane connects to tcp://socket-proxy:2375 instead of the socket directly
  4. Only enabled API endpoints are accessible

Enabled endpoints:

Endpoint What it allows
CONTAINERS List, inspect, create, start, stop containers
POST Create containers, networks, volumes
EXEC Execute commands inside containers
EVENTS Stream Docker events (real-time updates)
NETWORKS Manage Docker networks
VOLUMES Manage Docker volumes
IMAGES Pull, list, inspect images

Disabled by default (and why):

  • AUTH — authentication management
  • SECRETS — Docker secrets access
  • BUILD — build images (security risk)
  • COMMIT — commit containers to images
  • CONFIGS — Docker configs
  • DISTRIBUTION — registry operations
  • INFO — host system info
  • NODES — Swarm node info
  • PLUGINS — Docker plugins
  • SERVICES — Swarm services
  • SESSION — session management
  • SWARM — Swarm cluster management
  • SYSTEM — system-wide operations
  • TASKS — Swarm tasks

This is the principle of least privilege. Arcane gets exactly what it needs, nothing more.

/run/user/1000/docker.sock

This is the rootless Docker socket path. If you're running Docker in rootless mode (as I do), the socket lives at /run/user/<uid>/docker.sock instead of /var/run/docker.sock. For standard Docker installs, change this to /var/run/docker.sock.


Environment Configuration

APP_URL=http://127.0.0.1:3552
DOCKER_HOST=tcp://socket-proxy:2375
PUID=1000
PGID=1000
ENCRYPTION_KEY=your-64-char-hex-encryption-key
JWT_SECRET=your-64-char-hex-jwt-secret
Variable Description Notes
APP_URL Public URL where Arcane is accessible Used for CORS, redirects, and WebSocket connections. Change if using reverse proxy or VPN IP.
DOCKER_HOST Docker API endpoint Points to the socket proxy, not the socket directly.
PUID / PGID User/Group ID for file permissions Match your host user (id to check). Files created by Arcane will be owned by this UID/GID.
ENCRYPTION_KEY 64-char hex key for encrypting sensitive data Generate with openssl rand -hex 32. Keep secret.
JWT_SECRET 64-char hex secret for JWT token signing Generate with openssl rand -hex 32. Keep secret.

Generating secrets:

openssl rand -hex 32  # Run twice — once for ENCRYPTION_KEY, once for JWT_SECRET

The Deploy Script

The deploy.sh script handles initial setup. Run it once, then use docker compose up -d for subsequent starts.

#!/usr/bin/env bash
set -e

VOLUME_NAME="arcane_data"
NETWORK_NAME="proxy_net"
ENV_FILE=".env"

# Check Docker is running
if ! command -v docker &>/dev/null; then
  echo "Docker not found."
  exit 1
fi

if ! docker info &>/dev/null; then
  echo "Docker daemon not running."
  exit 1
fi

# Create volume if missing
if ! docker volume ls | grep -q "$VOLUME_NAME"; then
  docker volume create "$VOLUME_NAME"
fi

# Create network if missing
if ! docker network ls | grep -q "$NETWORK_NAME"; then
  docker network create "$NETWORK_NAME"
fi

# Generate .env if missing
if [ ! -f "$ENV_FILE" ]; then
  read -p "Enter APP_URL [http://localhost:3552]: " APP_URL_INPUT
  APP_URL="${APP_URL_INPUT:-http://localhost:3552}"
  cat > "$ENV_FILE" <<EOF
APP_URL=$APP_URL
DOCKER_HOST=tcp://socket-proxy:2375
PUID=1000
PGID=1000
ENCRYPTION_KEY=$(openssl rand -hex 32)
JWT_SECRET=$(openssl rand -hex 32)
EOF
fi

# Configure nftables for container networking
if sudo nft list chain inet filter forward &>/dev/null; then
  sudo nft add rule inet filter forward iifname "br-*" accept 2>/dev/null || true
  sudo nft add rule inet filter forward oifname "br-*" accept 2>/dev/null || true
  sudo nft add rule inet filter forward iifname "docker0" accept 2>/dev/null || true
  sudo nft add rule inet filter forward oifname "docker0" accept 2>/dev/null || true
fi

docker compose up -d

What It Does

  1. Checks prerequisites — Docker installed and running
  2. Creates volumearcane_data for persistent storage
  3. Creates networkproxy_net for inter-container communication
  4. Generates .env — with secure random keys (prompts for APP_URL)
  5. Configures nftables — allows traffic between Docker bridge networks and containers
  6. Starts servicesdocker compose up -d

The nftables Problem

On modern Linux distributions (Debian 12+, NixOS, Fedora), nftables replaces iptables as the firewall backend. Docker creates bridge networks (br-*) for container communication, but nftables may block forward traffic by default.

Without these rules, containers on the same Docker network can't talk to each other. The symptoms:

  • Arcane can't reach socket-proxy
  • "Unable to connect to local Docker daemon at tcp://socket-proxy:2375"
  • Containers show as unreachable

The fix is adding accept rules for Docker bridge interfaces:

sudo nft add rule inet filter forward iifname "br-*" accept
sudo nft add rule inet filter forward oifname "br-*" accept
sudo nft add rule inet filter forward iifname "docker0" accept
sudo nft add rule inet filter forward oifname "docker0" accept

First Login

After deployment:

  1. Open http://127.0.0.1:3552 (or your configured APP_URL)
  2. Login with default credentials:
    • Username: arcane
    • Password: arcane-admin
  3. Change the password immediately — you'll be prompted on first login

What to Explore

  • Dashboard — overview of all containers, resource usage, running/stopped status
  • Containers — full list with CPU/memory, status, update indicators, action buttons
  • Images — pull new images, check for updates, scan for vulnerabilities
  • Compose Projects — auto-discovered from your configured projects directory
  • Networks & Volumes — inspect, create, see what's in use vs orphaned
  • Updates — centralized view of available image updates, batch update support

Security Features Summary

Feature Implementation Why
Socket proxy tecnativa/docker-socket-proxy with filtered API Least privilege — only Docker endpoints Arcane needs
Read-only filesystem read_only: true + tmpfs Prevents file-based persistence if compromised
No privilege escalation no-new-privileges:true Blocks setuid/setgid capability gains
Resource limits CPU 1.0 / 512MB RAM Prevents resource exhaustion
Localhost binding 127.0.0.1:3552 Not exposed to public internet
Log rotation 10MB × 3 files Prevents disk fill from log growth
Rootless Docker socket /run/user/1000/docker.sock Reduced attack surface vs root Docker
Named volume arcane_data: external: true Managed lifecycle, survives container recreation
External network proxy_net Shared network for reverse proxy integration
Health checks Both services Auto-detection of failures, orchestrator awareness

Troubleshooting

Socket-proxy fails to start (read-only filesystem)

Symptom: can't create /tmp/haproxy.cfg: Read-only file system

Cause: Socket-proxy needs writable /tmp and /run. Already handled by tmpfs in the compose file. If you removed tmpfs, add it back:

tmpfs:
  - /tmp
  - /run

Arcane can't see other containers

Symptom: "Unable to connect to local Docker daemon at tcp://socket-proxy:2375"

Check 1: Containers on same network?

docker network inspect proxy_net --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'

Check 2: Can they communicate?

docker exec arcane ping -c 2 socket-proxy

Check 3: nftables blocking traffic?

sudo nft list chain inet filter forward

If policy is drop with no Docker rules, apply the nftables fix from the deploy script section.

Socket-proxy unhealthy but running

Symptom: Container restarts in a loop

Check logs:

docker logs arcane-socket-proxy

Common cause: haproxy.pid can't be created. Ensure /run is in tmpfs.

Registry 403 errors in logs

Symptom: distribution inspect failed: 403 Forbidden

Explanation: Registry API operations aren't proxied through the socket proxy (by design). Arcane falls back to direct registry access. Image scanning still works. These errors are informational, not critical.

Permission denied on volume

Symptom: Arcane can't write to /app/data

Fix: Ensure PUID/PGID in .env match your host user:

id  # Check your UID/GID

Lingering Docker networks

Old compose runs may leave orphan networks:

docker network ls | grep arcane
docker network rm arcane_default  # if not needed

Full reset

Nuclear option — wipe everything and start fresh:

docker compose down -v
docker volume rm arcane_data
rm .env
./deploy.sh

Production Hardening Tips

TLS via Reverse Proxy

Arcane runs plain HTTP. For internet-facing setups, put it behind a reverse proxy with TLS:

Caddy (simplest):

arcane.example.com {
    reverse_proxy localhost:3552
}

Nginx:

server {
    listen 443 ssl;
    server_name arcane.example.com;

    ssl_certificate /etc/letsencrypt/live/arcane.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/arcane.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3552;
        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 real-time logs)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Important: Arcane relies heavily on WebSocket connections for real-time logs, metrics, and container status updates. Your reverse proxy must support WebSocket upgrade.

Firewall Rules

Restrict port 3552 to trusted IPs:

# Allow only from VPN subnet
sudo nft add rule inet filter input ip saddr 100.64.0.0/10 tcp dport 3552 accept
sudo nft add rule inet filter input tcp dport 3552 drop

Pin Image Version

Don't use :latest in production. Pin to a specific version — check the Releases page for the current one:

image: ghcr.io/getarcaneapp/arcane:v2.12.0

This prevents unexpected breakage from upstream changes and makes rollbacks predictable. Arcane ships releases frequently, so revisit this pin periodically rather than assuming it's still current.

Backups

Back up the arcane_data volume regularly:

# Export volume to tar
docker run --rm -v arcane_data:/data -v $(pwd):/backup alpine \
  tar czf /backup/arcane-backup-$(date +%Y%m%d).tar.gz -C /data .

# Restore
docker run --rm -v arcane_data:/data -v $(pwd):/backup alpine \
  tar xzf /backup/arcane-backup-20260917.tar.gz -C /data

VPN Access (Tailscale/ZeroTier)

For secure remote access without public exposure:

  1. Get your VPN interface IP:

    ip addr show tailscale0  # Tailscale
    ip addr show zt0          # ZeroTier
    
  2. Update APP_URL in .env:

    APP_URL=http://100.x.x.x:3552
    
  3. Restart:

    docker compose restart
    

SSH Tunnel

Access Arcane without any network exposure:

ssh -L 3552:127.0.0.1:3552 user@your-server

Then open http://localhost:3552 in your browser.


Summary

Arcane fills a real gap in the Docker management landscape. It's not trying to replace Kubernetes or be a CI/CD platform. It's a clean, modern UI for managing Docker containers, images, networks, and compose projects — with security best practices baked in from the start.

The setup described here — socket proxy, read-only filesystem, resource limits, localhost binding, nftables rules — takes about 5 minutes to deploy and gives you a production-hardened Docker management dashboard.

If Portainer feels like overkill and Dockge is too limited (and largely stagnant), Arcane is worth a serious look.


Links: