Two ways to run a Docker UI: point it at one box, or let one manager drive every box you own. Arcane does both, and it is the second case that decides whether a container dashboard is worth installing at all. Managing a single host is a convenience. Managing five from one login is the reason you stop reaching for SSH.
That is the job Arcane is built for. This guide covers how to install Arcane on Ubuntu 26.04 and 24.04, deploy Compose projects from the browser, attach a second Docker host with the agent, scan your images for CVEs, and set up role based access control before anyone else logs in. It is a single Go binary shipped as a container, licensed BSD-3-Clause, and it talks to the Docker socket the same way the CLI does. Every command, version, and screenshot below came from a run on Ubuntu 26.04 and Ubuntu 24.04 with Arcane 2.6.0 in August 2026.
What Arcane actually does
Arcane wraps the Docker Engine API in a dashboard: containers, images, volumes, networks, and Compose projects, plus the things the CLI makes tedious. Live log streaming, an exec shell in the browser, Trivy vulnerability scanning, image update tracking, and a fleet view where one manager drives Docker daemons on other machines through a small agent.
If you have used Portainer on Ubuntu, the mental model is familiar. The differences that matter in practice: Arcane keeps your Compose files as real files on disk under a projects directory (so you can edit them in the UI or on the filesystem and both sides agree), the vulnerability scanner is built in rather than a paid tier, and the whole thing is BSD licensed with no feature gating. Komodo sits in the same category if you want a comparison point that leans harder into build pipelines.
The tradeoff is the one every Docker UI makes. Arcane needs the Docker socket, and the Docker socket is root on the host. There is a section further down on giving it less than that.
Lab setup and prerequisites
Two hosts, so the remote environment section is real rather than theoretical:
| Role | OS | Spec | What runs on it |
|---|---|---|---|
| Manager | Ubuntu 26.04 LTS | 2 vCPU, 4 GB RAM, 40 GB disk | Arcane manager, Nginx, a Compose project |
| Node 2 | Ubuntu 24.04 LTS | 2 vCPU, 2 GB RAM, 25 GB disk | Arcane agent, two containers |
Those specs are a floor, not a recommendation. Arcane itself is light, and the manager container sat around 200 MB resident with nine containers under management. What drives your sizing is everything else on the box: the workloads Arcane is watching, and the Trivy scans, which are CPU and disk hungry in bursts because each scan unpacks image layers. For a manager that also runs production containers, start at 4 vCPU and 8 GB and watch the scan window. A manager that only manages other hosts is comfortable on 2 vCPU and 2 GB.
You also need a non-root user in the docker group on each host, and outbound access to ghcr.io, which is where all three images live: the manager, the agent, and the tools image the scanner runs from.
Set the values that repeat throughout this guide once, so you can paste the rest without editing every line:
export ARCANE_DOMAIN="arcane.example.com"
export ARCANE_EMAIL="[email protected]"
Substitute your own domain and address. The lab used 10.0.1.50 for the manager and 10.0.1.51 for the second host, so swap those in where they appear. These are shell exports, so re-run them if you reconnect; an empty ${ARCANE_DOMAIN} later writes a broken server_name into the vhost.
Install Docker Engine
Arcane ships as a container and drives Compose through the Docker CLI plugin, so both have to be present before anything else. If the box ever had the distro packages, remove them first (docker.io, docker-compose, containerd, runc), because they conflict with docker-ce. Then add the official key:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
Add the repository. The VERSION_CODENAME lookup keeps this identical on resolute and noble:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
Then install the engine, the CLI, and the Compose plugin:
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker
Confirm both pieces answer before moving on:
docker --version && docker compose version
Both hosts in this lab reported the same versions, which is what you want when a manager and an agent have to agree on the API:
Docker version 29.7.1, build e9452d6
Docker Compose version v5.3.1
If any of that failed, the long form is in the dedicated guides for Docker CE on Ubuntu 26.04 and Docker Compose on Ubuntu 26.04. Run this step on the second host too; the agent needs the same engine underneath it.
Deploy Arcane with Docker Compose
Arcane wants two secrets at startup. ENCRYPTION_KEY protects credentials it stores (registry logins, agent tokens) and must be at least 32 characters as a raw passphrase, which is why the 64-character hex string below is safe. JWT_SECRET signs session tokens. Both ship with insecure defaults, so generate real ones into an env file:
mkdir -p ~/arcane && cd ~/arcane
printf 'ENCRYPTION_KEY=%s\nJWT_SECRET=%s\n' "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > .env
chmod 600 .env
Now write the Compose file:
sudo vim ~/arcane/compose.yaml
Paste the following. The cgroup: host line lets Arcane detect its own container ID reliably, and PUID and PGID decide who owns the files it writes into the data volume:
services:
arcane:
image: ghcr.io/getarcaneapp/manager:latest
container_name: arcane
ports:
- '3552:3552'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- arcane-data:/app/data
environment:
- APP_URL=http://10.0.1.50:3552
- PUID=1000
- PGID=1000
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- JWT_SECRET=${JWT_SECRET}
cgroup: host
restart: unless-stopped
volumes:
arcane-data:
Set APP_URL to the address you will actually reach Arcane on. It is not cosmetic: Arcane hands that URL to agents as their callback address, which breaks Edge-mode agents later if it points somewhere they cannot resolve. Bring it up:
docker compose up -d
First boot runs 68 database migrations against an embedded SQLite file, creates the projects directory, and detects the Docker API version. Check that it settled:
docker compose ps
The container should be up with port 3552 published on the host:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
arcane ghcr.io/getarcaneapp/manager:latest "./arcane" arcane 20 minutes ago Up 20 minutes 0.0.0.0:3552->3552/tcp
There is a health endpoint and a version endpoint that are handy for monitoring hooks later:
curl -s http://localhost:3552/api/health
A healthy instance answers with a one-line status document:
{"status":"UP"}
Here is the same state from the shell, including the Compose project Arcane will manage for us shortly:

With the service answering on 3552, the rest of the work happens in the browser.
First login and the password that is not in the docs
Open http://10.0.1.50:3552 and you get the sign-in screen.

This trips people up. The documentation says the default user is admin, and that is wrong on current builds. The account Arcane creates on first boot is arcane, and it prints the credentials in the startup log:
docker logs arcane 2>&1 | grep -A3 "Default admin user"
The four lines you are looking for spell out both halves and the fact that you are about to be forced to change one of them:
INF 👑 Default admin user created!
INF 🔑 Username: arcane
INF 🔑 Password: arcane-admin
INF ⚠️ User will be prompted to change password on first login
Sign in with those and Arcane immediately blocks the dashboard behind a password change dialog. You cannot dismiss it, which is the correct behaviour for a service holding the Docker socket.

Minimum length is eight characters. Use something longer, because this account is a global admin until you narrow it down with roles further on.
Find your way around the dashboard
The landing page is an environment board rather than a container list, which tells you what Arcane is optimised for. Each environment card carries its own container, image, and action-item counts plus live CPU, memory, and disk gauges.

That screenshot is from the end of this guide, with the second host already attached. On a fresh install you get one card labelled Local Docker with the Manager badge. The sidebar splits into Management (dashboard, projects, environments, customization), Resources (containers, images, updates, networks, volumes), Swarm, and Administration (event log, settings). The environment selector at the top left is the piece worth remembering, because every Resources page renders in the context of whichever environment is selected there.
One page that does not have an obvious CLI equivalent is Networks, then Topology, which draws the bridge networks and the containers attached to each one. It is the fastest way to spot a container that ended up on the default bridge when it should have been on your application network:

Everything else lives one level down, starting with the containers themselves.
Manage containers: logs, stats, and a shell
The Containers page is the one you will live in. Filtering, bulk start and stop, and per-row actions are all there.

Click into a container and you get the detail view: state, mounts, networks, environment, published ports, and live resource graphs that update over a websocket rather than on a refresh timer.

The Logs tab streams the same output as docker logs -f, with search and severity filtering on top. Newer builds also expose the raw Docker CLI output for operations, so when a pull or a deploy misbehaves you see exactly what the daemon said instead of a sanitised summary.

There is also an exec shell per container. It is genuinely useful for a quick psql or a config check, and it is genuinely dangerous, which is the argument for the read-only role covered below.
Deploy a Compose project from the browser
This is where Arcane separates itself from a plain container list. Projects are Compose stacks stored as real files under /app/data/projects inside the data volume, one directory per project.
Go to Projects, click Create Project, then click the title to rename it. The name becomes the directory name and the Compose project name, so keep it lowercase. The editor is a proper YAML editor with folding and validation:

The stack used for this walkthrough is deliberately ordinary, a web tier and a database with a named volume:
services:
notes-web:
image: nginx:1.27-alpine
container_name: notes-web
ports:
- '8090:80'
depends_on:
- notes-db
restart: unless-stopped
notes-db:
image: postgres:17-alpine
container_name: notes-db
environment:
POSTGRES_PASSWORD: change-me-in-production
POSTGRES_DB: notes
volumes:
- notes-data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
notes-data:
Create Project writes the file but does not start anything. Open the project and use the Up button to bring it online; Redeploy, Pull, and Destroy sit next to it, and an Archive action keeps the definition while removing the containers.

Because these are ordinary Compose files, docker compose ls on the host sees them alongside anything you started by hand. Arcane is not maintaining a parallel universe, which matters the day you need to recover without the UI. If Compose itself is the part you want to shore up, the complete Docker Compose guide goes deeper than this article can.
Add a second Docker host with the Arcane agent
One manager, many daemons. The agent is a second container that exposes the remote host’s Docker socket to the manager over an authenticated channel, and it runs in one of two modes.
Direct means the manager opens a connection to the agent on TCP 3553, so the agent needs a reachable port. Edge reverses it: the agent dials out to the manager and holds the tunnel open, which is what you want for a host behind NAT or a restrictive firewall. Transport is auto (gRPC with a websocket fallback), or grpc, websocket, or poll if you would rather pin it. The value only applies to Edge mode; the snippet generator emits it for Direct agents too, where it does nothing.
Create the environment in Arcane first, because that is what mints the token. Environments, then Add Environment:

Pick Direct, give it a name, and set the agent address to the remote host and port 3553. Generate Agent Configuration returns an API key plus ready-made docker run and Compose snippets:

Copy the key now. Arcane will not show it again, and the token is what binds this agent to this environment, so there is no separate pairing dance.
One line in that snippet deserves a second look. Arcane fills MANAGER_API_URL from whatever you set as APP_URL on the manager, so a manager configured with a localhost address hands every agent a callback pointing at itself. Which mode you chose decides whether that matters. In Direct mode the agent is a passive HTTP server and the manager dials it, so the value is unused and the address that has to be right is the agent address you typed into the dialog. In Edge mode it is the address the agent dials out to, and a wrong one means the agent starts cleanly and never appears. Correct it before you deploy either way. On the second host:
mkdir -p ~/arcane-agent
sudo vim ~/arcane-agent/compose.yaml
Paste the snippet with the manager address corrected and your own token in place:
services:
arcane-agent:
image: ghcr.io/getarcaneapp/agent:latest
container_name: arcane-agent
restart: unless-stopped
environment:
- AGENT_MODE=true
- EDGE_TRANSPORT=poll
- AGENT_TOKEN=arc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- MANAGER_API_URL=http://10.0.1.50:3552
ports:
- '3553:3553'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- arcane-agent-data:/app/data
volumes:
arcane-agent-data:
For Edge mode, swap AGENT_MODE=true for EDGE_AGENT=true and drop the ports block entirely, since nothing needs to reach in. Start it:
cd ~/arcane-agent && docker compose up -d
The manager runs an environment health job every two minutes, and it also pushes registry and Git repository config down to each agent as it connects. You can watch that happen from the manager side:
docker logs arcane 2>&1 | grep -i environment
A successful attach logs the sync in both directions and finishes the health check without an error:
INF Starting registry sync to environment environmentName=docker-node-2
INF Successfully synced registries to environment environmentName=docker-node-2
INF Successfully synced git repositories to environment environmentName=docker-node-2
INF Job finished name=environment-health:dcc5211b-f08b-439b-b0fa-b1587356f9b3
Back in the UI, the environment list shows both hosts online and reports the version each is running, which is how you catch a manager and agent that have drifted apart after an upgrade:

Switch the environment selector to the remote host and every Resources page repoints at that daemon. These containers are running on the Ubuntu 24.04 box, driven entirely from the manager:

Start, stop, logs, and exec all behave identically against a remote daemon, with the round trip as the only difference you will notice.
Scan images for CVEs
Arcane runs Trivy from a helper container and aggregates findings per environment under Images, then Vulnerabilities. Scan all images kicks off a background job that works through every image the daemon has, one at a time.
Real numbers from this lab, because published scan results are usually either marketing or absent. The run covered the six application images below; the counter in the screenshot reads 6 of 7 because Arcane also tracks the helper image it runs the scanner from:
| Image | Vulnerabilities | Scan time |
|---|---|---|
| nginx:1.27-alpine | 107 | 17.0s |
| postgres:17-alpine | 39 | 30.7s |
| ghcr.io/getarcaneapp/manager:latest | 2 | 35.7s |
| alpine:3.20 | 0 | 14.8s |
| redis:7-alpine | 0 | 18.4s |
| nginx:alpine | 0 | 38.9s |
The aggregate came to 148 findings: 3 Critical, 49 High, 67 Medium, 28 Low, and 1 Unknown. Note the pair at the top and bottom of that table. nginx:alpine was clean while nginx:1.27-alpine carried 107 of them. Pinning a minor version and forgetting about it is exactly how that happens.

Each row gives the CVE, the affected package, installed version, and the version that fixes it, which is the column that actually drives work. All three criticals in this run came from two CVEs: CVE-2026-31789 in OpenSSL, counted once against libcrypto3 and once against libssl3 on nginx:1.27-alpine (3.3.3-r0, fixed in 3.3.7-r0), and CVE-2025-68121 in the Go standard library baked into postgres:17-alpine. Findings you have consciously accepted can be pushed to an Ignored list so the next scan does not re-raise them.
Budget for the scan window. Roughly 15 to 40 seconds per image on 2 vCPU, and the scanner is capped at one core. Scheduled scanning is off by default; once you enable it the default cron is daily at midnight. On a host with fifty images that is a real chunk of I/O. If you want a dedicated scanning stack rather than a per-host one, Greenbone in Docker covers the heavier end.
Track image updates
The Updates page compares the digest of every running image against its registry tag and tells you what has moved. An image update watcher runs on a schedule and a batch check completes in well under a second per image, since it only pulls manifests.
Row-level, bulk, and Update All actions are available, so you can pull and recreate straight from the page. This replaces a Watchtower deployment for most people, with the advantage that nothing restarts unless you say so. Containers labelled com.getarcaneapp.arcane.updater=false are excluded from automatic updates, which is how you keep a pinned database out of a bulk update.
Users, roles, and OIDC
The admin account created at first login can do everything, including exec into any container on any attached host. That is not an account anyone should be sharing.
Settings, then Roles, shows the built-in roles and exactly which permissions each one carries:

Create users under Settings, then Users, and give people the narrowest role that lets them do their job. A viewer role that can read logs but cannot exec covers most of the “can you check if it’s up” requests that would otherwise become an admin account.
For anything with more than a couple of people, wire it to your identity provider instead. Arcane speaks OIDC through environment variables, and group claims map onto its roles:
- OIDC_ENABLED=true
- OIDC_ISSUER_URL=https://auth.example.com/application/o/arcane/
- OIDC_CLIENT_ID=arcane
- OIDC_CLIENT_SECRET=your-client-secret
- OIDC_SCOPES=openid email profile
- OIDC_GROUPS_CLAIM=groups
- OIDC_PROVIDER_NAME=Authentik
- OIDC_AUTO_REDIRECT_TO_PROVIDER=true
OIDC_ROLE_MAPPINGS takes a JSON array and is what turns an IdP group into an Arcane role, so revoking access in the directory revokes it here. Set OIDC_AUTO_REDIRECT_TO_PROVIDER only once you have confirmed the flow works, because it takes the local login form out of the path.
Harden Arcane before it faces anything
Mounting /var/run/docker.sock into a container hands that container root on the host. Anyone who compromises Arcane can start a privileged container and walk out with the filesystem. The fix is a socket proxy that only forwards the API calls Arcane needs.
Replace your Compose file with a two-service version:
sudo vim ~/arcane/compose.yaml
The proxy holds the socket read-only and exposes a filtered HTTP API on an internal network. Arcane then talks to it over DOCKER_HOST and never sees the socket at all:
services:
docker-socket-proxy:
image: tecnativa/docker-socket-proxy:latest
container_name: arcane-docker-proxy
environment:
- EVENTS=1
- PING=1
- VERSION=1
- AUTH=0
- SECRETS=0
- POST=1
- BUILD=0
- CONTAINERS=1
- EXEC=1
- IMAGES=1
- INFO=1
- NETWORKS=1
- SWARM=0
- SYSTEM=0
- VOLUMES=1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- arcane-internal
restart: unless-stopped
security_opt:
- no-new-privileges:true
arcane:
image: ghcr.io/getarcaneapp/manager:latest
container_name: arcane
ports:
- '3552:3552'
volumes:
- arcane-data:/app/data
environment:
- APP_URL=http://10.0.1.50:3552
- PUID=1000
- PGID=1000
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- JWT_SECRET=${JWT_SECRET}
- DOCKER_HOST=tcp://docker-socket-proxy:2375
networks:
- arcane-internal
depends_on:
- docker-socket-proxy
cgroup: host
restart: unless-stopped
networks:
arcane-internal:
driver: bridge
volumes:
arcane-data:
Apply it with docker compose up -d, then know what you are giving up. With BUILD=0 the image build features stop working and with SWARM=0 so does anything under the Swarm section. Two more are worth naming because the proxy revokes them unless you opt in, so they bite even though they are absent from the block above. SYSTEM stays off, which blocks the disk-usage call behind the volume and dashboard size figures, and DISTRIBUTION stays off, which blocks the registry inspection Arcane uses to look up remote image metadata. EXEC=1 is still a large privilege; set it to 0 if nobody needs a browser shell. Turn the flags off one at a time and re-test rather than pasting a stricter set and wondering which page broke.
Two more things while you are in the file. Arcane checks an analytics heartbeat hourly and sends it to checkin.getarcane.app at most once a day by default, reporting version and an instance ID. Add ANALYTICS_DISABLED=true to stop it. And if you have finished configuring the instance, UI_CONFIGURATION_DISABLED=true freezes the settings UI so a compromised session cannot reconfigure the service.
Put Arcane behind Nginx with HTTPS
Arcane serves plain HTTP on 3552. Never leave that exposed; session tokens and the exec shell both cross that connection. Put Nginx in front and terminate TLS there.
Point an A record for your chosen hostname at the server’s public IP first, using whatever DNS provider you already have, and make sure port 80 is reachable so the ACME challenge can complete. Then install the pieces:
sudo apt-get install -y nginx certbot python3-certbot-nginx
Create the site definition:
sudo vim /etc/nginx/sites-available/arcane.conf
The websocket block is not optional. Without proxy_http_version 1.1 and the two upgrade headers, Arcane loads but the live log streams, resource graphs, and the exec terminal all sit there dead:
server {
listen 80;
server_name arcane.example.com;
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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
}
Point the vhost at your own hostname, enable it, drop the default site, and check the syntax. Certbot matches on server_name, so skipping the substitution makes the next step fail with a missing server block:
sudo sed -i "s/arcane.example.com/${ARCANE_DOMAIN}/" /etc/nginx/sites-available/arcane.conf
sudo ln -sf /etc/nginx/sites-available/arcane.conf /etc/nginx/sites-enabled/arcane.conf
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
A clean parse looks like this, and anything else means a typo in the block above:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload, then let certbot rewrite the vhost for TLS and add the redirect:
sudo systemctl reload nginx
sudo certbot --nginx -d "${ARCANE_DOMAIN}" --non-interactive --agree-tos --redirect -m "${ARCANE_EMAIL}"
Certbot edits the vhost in place, adds the certificate paths, and installs a 301 from port 80. That is the whole flow for a server with a public IP.
When port 80 is not reachable
Servers on a private LAN, behind NAT, or in a security group you cannot open need the DNS-01 challenge instead, which proves ownership with a TXT record and needs no inbound connection. Install the plugin for your provider and substitute it below:
| Provider | Certbot plugin package |
|---|---|
| Cloudflare | python3-certbot-dns-cloudflare |
| AWS Route 53 | python3-certbot-dns-route53 |
| DigitalOcean | python3-certbot-dns-digitalocean |
| Google Cloud DNS | python3-certbot-dns-google |
| Linode | python3-certbot-dns-linode |
| OVH | python3-certbot-dns-ovh |
| RFC2136 (BIND) | python3-certbot-dns-rfc2136 |
Using Cloudflare as the worked example, store a scoped API token and issue against it:
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d "${ARCANE_DOMAIN}" --non-interactive --agree-tos -m "${ARCANE_EMAIL}"
The credentials file holds one line, dns_cloudflare_api_token = your-token-here, and must be chmod 600. Note that certonly obtains the certificate without touching Nginx, so this path needs one more command to wire it into the vhost and add the redirect:
sudo certbot install --nginx --cert-name "${ARCANE_DOMAIN}"
Whichever challenge you used, confirm renewal works before you forget about it:
sudo certbot renew --dry-run
Finally, tell Arcane it lives behind a proxy now. Update APP_URL to the HTTPS address and set TRUSTED_PROXIES so it reads the real client IP from X-Forwarded-For instead of rate-limiting every login against the proxy’s address:
- APP_URL=https://arcane.example.com
- TRUSTED_PROXIES=172.16.0.0/12
Recreate the container after that change, and note that any agent generated from here on inherits the new APP_URL as its callback address.
Troubleshooting: what actually broke
Four things went wrong during this build. All four are the kind that waste an evening.
Error: “Invalid username or password” with the documented credentials
The installation docs list admin for both fields. Current builds create arcane with the password arcane-admin, so neither half of the documented pair works. Read the real value out of the startup log with docker logs arcane 2>&1 | grep Username rather than guessing, and if the log has already rotated away, the account exists in the SQLite database in the arcane-data volume.
The agent starts cleanly but the environment never comes online
Which side to blame depends on the mode, and this is where people lose an hour. In Direct mode the agent just listens, so a healthy agent log proves nothing; the manager is the side that has to reach it. Check that port 3553 is open from the manager and that the agent address you entered when creating the environment is correct, then run curl -s http://10.0.1.51:3553/api/health from the manager. In Edge mode the agent dials out, so the value to check is MANAGER_API_URL, which Arcane generates from the manager’s APP_URL and will happily set to a localhost address the remote box can never reach. Fix whichever applies, recreate the agent, and the health job picks it up within two minutes.
Error: “rate limited by analytics heartbeat endpoint (429 Too Many Requests)”
Harmless, and it appears with a full Go stack trace in the log, which makes it look far worse than it is. Arcane phones home with its version and instance ID; the upstream endpoint rate-limits and the retry logic dumps a trace. Set ANALYTICS_DISABLED=true and both the calls and the noise stop.
Live logs and resource graphs are blank behind a reverse proxy
Missing websocket configuration, every time. The page renders because the initial HTTP request succeeds, then anything streaming stays empty. Add proxy_http_version 1.1 plus the Upgrade and Connection headers shown above. Traefik handles the upgrade automatically and needs no extra middleware.
One last thing worth doing on day one: take a copy of the arcane-data volume somewhere off the host. It holds the SQLite database, your Compose project files, and the encrypted registry credentials, and losing it means rebuilding every project definition by hand. A nightly docker run --rm -v arcane_arcane-data:/data -v /backup:/backup alpine tar czf /backup/arcane-$(date +%F).tar.gz -C /data . is enough, and the same image restores it with tar xzf /backup/arcane-<date>.tar.gz -C /data against a stopped container. Pair it with something like Beszel for lightweight Docker monitoring so you find out the manager is down before someone else does.