Migrating from Portainer to Arcane
A few years ago I wrote a post on running n8n with Portainer, and it’s been my default Docker UI ever since, on that box and pretty much every one I’ve spun up after it. That’s changing now, and the recent Portainer news is why.
What changed
Portainer announced 3.0, a ground-up rebuild with Kubernetes at the centre, and the headline for the rest of us is that there’s no Portainer CE 3.x. The free Community Edition stays on the 2.45 LTS line, that’s the last release in the 2.x series, and everything new goes into the Kubernetes-first product instead.
CE isn’t dead, it keeps its open source licence and it’ll still get security fixes, bug fixes, and backports of the 3.x features where there’s a matching Docker API to hang them on. What you won’t get is feature parity, some of the new stuff leans on Kubernetes bits that Docker doesn’t have, so it can’t come across even if they wanted it to.
If you want the 3.x features for free the only route is the “3 Nodes Free” programme, and that’s the closed source Business Edition capped at three nodes with one licence per organisation. It works, and plenty of people run it happily, but it’s a proprietary product with a licence key on it, not the open CE you’ve been running all this time.
For a homelab there’s no fire to put out here, 2.45 LTS still runs and it’s still being maintained, so you don’t have to touch anything today. The direction is clear enough though, the open source version is the frozen one now, and I’d rather move while it’s still my choice to make than wait until something down the line forces it.
Why Arcane
I looked at a couple of options before landing anywhere. Komodo is worth a look if you want a build-and-deploy tool spread across a few servers, but the one that fit how I actually work is Arcane.
Arcane is a Docker management UI written in Go, shipped as a container, and it’s BSD-3-Clause licensed with none of the features gated behind a paid tier. It talks to the Docker socket the same way the CLI does, so the mental model is the one you’ve already got from Portainer, there’s nothing new to learn there.
The things that sold me:
- It keeps your Compose files as real files on disk in a projects directory, so you can edit them in the UI or on the filesystem and both sides stay in sync. No more compose buried inside a database.
- The vulnerability scanner is built in with Trivy, not locked behind a licence tier.
- Live log streaming and an exec shell in the browser.
- A fleet view, so one Arcane can drive Docker on other machines through a small agent.
- Image update tracking, registries, RBAC, and webhooks.
Installing Arcane
My VPS already has a docker user that owns all the compose projects, and I keep them under /home/docker/projects, so Arcane gets set up to match that rather than the other way round.
First we need two secrets, Arcane wants a 32 character encryption key and a JWT secret:
openssl rand -base64 32
openssl rand -base64 32
Then the compose file:
services:
arcane:
image: ghcr.io/getarcaneapp/manager:latest
container_name: arcane
ports:
- "127.0.0.1:3552:3552"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- arcane-data:/app/data
- /home/docker/projects:/home/docker/projects
environment:
- ENCRYPTION_KEY=your_first_generated_key
- JWT_SECRET=your_second_generated_key
- PROJECTS_DIRECTORY=/home/docker/projects
- PUID=1001
- PGID=998
- TZ=Europe/London
restart: unless-stopped
volumes:
arcane-data:
There are three things in there worth calling out.
The port is bound to 127.0.0.1, so Arcane isn’t sitting open on the internet, it’s only reachable through my reverse proxy which handles the TLS on a proper hostname.
The projects directory is mounted at the same path inside the container as it is on the host. That matters more than it looks, Arcane hands your compose files to the Docker daemon, and the daemon resolves any bind mounts against the host filesystem, so if the paths inside and outside don’t match, relative paths in your compose end up pointing somewhere you didn’t expect.
PUID and PGID match the uid and gid of my docker user, so anything Arcane writes into the projects folder is owned by the same user as everything else in there. Run id docker on your host to get your own numbers, mine are 1001 and 998 but yours will almost certainly be different.
Bring it up:
docker compose up -d
Then open Arcane through your proxy and set up the first admin account. Arcane moves quickly, so it’s worth pinning a version tag rather than leaving it on latest, that way an update doesn’t land on you when you’re not expecting it.
Moving the n8n stack over
The big one for me was n8n, it’s a queue mode setup with Postgres, Redis, a main instance, webhook workers and runners, and it’s the one I really didn’t want to break. Portainer keeps stack definitions in its own database while Arcane wants them as files, so there’s no clever import, you move each stack by hand. This is how I did it.
Back up the database first. Before touching anything I took a pg_dump of the n8n database, if anything went wrong from here I could at least put the data back:
docker exec <your-postgres-container> pg_dump -U <db-user> <db-name> > n8n-backup.sql
Copy the compose and env into a project folder. I made a folder for the stack under /home/docker/projects, pasted the compose from Portainer’s editor into a compose.yaml, and moved the environment variables out of Portainer’s env section into a .env file next to it.
Pin the volumes before you do anything else. This is the step that’ll catch you out. Compose prefixes named volumes with the project name, and in Portainer the project name is the stack name, so my db_new volume actually lives on disk as n8n-cloud-joffcom-net_db_new. If you start the stack in Arcane under a different project name, Compose happily creates a fresh empty db_new, Postgres starts up on a blank database, and n8n runs its first time setup like you’ve never used it. To stop that happening I pinned the existing volumes as external using their full names:
volumes:
n8n-joffcom:
external: true
db_new:
external: true
name: n8n-cloud-joffcom-net_db_new
redis_new:
external: true
name: n8n-cloud-joffcom-net_redis_new
Run docker volume ls to find your real volume names, they’ll be whatever your Portainer stack was called with the volume name stuck on the end.
Remove the stack in Portainer. This stops and removes the containers, but named volumes are left alone, so your data stays put. This is the only bit with any downtime, from here until the stack comes back up in Arcane.
Check the volumes survived. A quick docker volume ls to make sure they’re all still there before going any further. They were, but I wasn’t going to take that on trust.
Start it with Up, not Redeploy. In Arcane, open the project and hit Up. Redeploy pulls fresh images first, and I didn’t want to be migrating the stack and upgrading n8n at the same time, if something broke I wanted to know which of the two did it. Up starts it with the images already on the host, so it comes back exactly as it was.
The stack also uses an external network called mymaco that it shares with a couple of other containers, and because that’s declared as external in the compose there was nothing to do for it, the network already existed and the stack just joined it again when it came up.
n8n came back with all my workflows, credentials and execution history in place, and that was that.
A couple of gotchas
- Pin a version. Like I said above,
latestwill update out from under you. - Get the volume names right before you start the stack, not after. Once Postgres has initialised a fresh empty volume you’re into restoring from your backup.
- Arcane managing its own container is a bit awkward on redeploy. If you run Arcane as a project that it also manages, redeploying Arcane itself can need a manual
docker compose up -dto bring it back. Keeping Arcane’s own compose outside the projects folder avoids the head-scratch.
What’s left
n8n was the hard one and it’s done. I’ve still got a few smaller stacks sat in Portainer, and they’ll go over the same way, back up, copy, pin the volumes, remove, check, Up. Once the last one’s moved Portainer’s container comes off the box, though I’ll be keeping a backup of portainer_data for a while in case I find something I forgot to copy across.