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 enablepodman.socketfor 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, whichtecnativa/docker-socket-proxyis.
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:
- Socket proxy mounts the Docker socket as read-only
- It exposes only a filtered TCP API on port 2375
- Arcane connects to
tcp://socket-proxy:2375instead of the socket directly - 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 managementSECRETS— Docker secrets accessBUILD— build images (security risk)COMMIT— commit containers to imagesCONFIGS— Docker configsDISTRIBUTION— registry operationsINFO— host system infoNODES— Swarm node infoPLUGINS— Docker pluginsSERVICES— Swarm servicesSESSION— session managementSWARM— Swarm cluster managementSYSTEM— system-wide operationsTASKS— 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
- Checks prerequisites — Docker installed and running
- Creates volume —
arcane_datafor persistent storage - Creates network —
proxy_netfor inter-container communication - Generates
.env— with secure random keys (prompts for APP_URL) - Configures nftables — allows traffic between Docker bridge networks and containers
- Starts services —
docker 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:
- Open
http://127.0.0.1:3552(or your configuredAPP_URL) - Login with default credentials:
- Username:
arcane - Password:
arcane-admin
- Username:
- 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:
-
Get your VPN interface IP:
ip addr show tailscale0 # Tailscale ip addr show zt0 # ZeroTier -
Update
APP_URLin.env:APP_URL=http://100.x.x.x:3552 -
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: