Portainer
Managing Docker from the CLI is fine until you are three SSH sessions deep trying to remember which host runs which stack. Portainer puts a web UI on top of the Docker socket: containers, images, volumes, networks, logs and a shell, in a browser, across multiple hosts.
This guide sets up Portainer CE, connects a second Docker host as an agent, and puts the UI behind Traefik so you stop typing port numbers.
Portainer is a management UI for container platforms — Docker, Docker Swarm and Kubernetes. The Community Edition is free and covers everything a homelab needs.
What it actually gives you:
- Container lifecycle — start, stop, recreate, inspect, without remembering flags
- Live logs and an in-browser terminal, so you can
execinto a container from your phone - Stacks — deploy and edit Compose files from the UI, including straight from a Git repo
- Multi-endpoint management — one UI for every Docker host you own
- Image, volume and network management with a working "what is safe to prune" view
- Users, teams and RBAC if more than one person touches the lab
Portainer has root on your host
Portainer works by mounting /var/run/docker.sock. Anything that can talk to the Docker socket can start a privileged container and own the host. That is not a Portainer flaw, it is what the socket is — but it means you should never expose the Portainer UI to the internet, and you should give it a real password.
Prerequisites
- A Linux host with Docker and the Compose plugin — see the Docker guide
- Port 9443 free
- Optional: Traefik already running for the reverse proxy step
1. Create the data volume
Portainer keeps its database, users and endpoint config in /data. That has to be a named volume or you will lose your setup on the first container recreate.
2. Write the Compose file
Make a directory for the stack. I keep everything under /opt/docker/<app> so backups are one predictable path:
services:
portainer:
container_name: portainer
image: portainer/portainer-ce:lts
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
ports:
- 9443:9443
- 8000:8000
volumes:
portainer_data:
name: portainer_data
Breaking down the parts that matter:
portainer/portainer-ce:lts— the LTS tag, notlatest. Portainer ships breaking UI changes on the current branch; LTS gets security fixes without moving your cheese./var/run/docker.sock:/var/run/docker.sock— the whole point. This is how Portainer sees your containers.portainer_data:/data— users, settings and endpoint definitions. Back this up.9443— the HTTPS UI. Portainer generates its own self-signed cert on first boot.8000— the Edge agent tunnel. If you are not using Edge agents, delete this line.restart: always— Portainer should come back before the things it manages.
3. Start it
4. Create the admin user
Open https://<host-ip>:9443 and accept the self-signed certificate warning.
You get an admin setup screen. Do this within a few minutes of starting the container — Portainer times out the initial setup window as a security measure, and if you miss it you have to restart the container to get the form back. Password minimum is 12 characters.
On the next screen, choose Get Started to manage the local Docker environment. The Environments → local view should now list every container on the host.
5. Add a second Docker host
The Portainer Agent exposes a remote host's Docker socket to your existing UI. On the second machine:
services:
agent:
container_name: portainer_agent
image: portainer/agent:lts
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
ports:
- 9001:9001
Then in the Portainer UI: Environments → Add environment → Docker Standalone → Agent, and give it <second-host-ip>:9001.
Port 9001 is unauthenticated by default
The agent trusts anything that can reach it. Keep 9001 on your LAN or a WireGuard subnet — never forward it at the router, and firewall it to the Portainer host's IP:
6. Put it behind Traefik
Reaching for :9443 gets old. Add Portainer to your Traefik network and give it a hostname.
Note the scheme=https line — Portainer speaks HTTPS on 9443 with a self-signed cert, so Traefik has to be told to talk HTTPS to the backend and not to validate that cert. This is the step most guides get wrong.
services:
portainer:
container_name: portainer
image: portainer/portainer-ce:lts
restart: always
security_opt:
- no-new-privileges:true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
networks:
- traefik-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.example.com`)"
- "traefik.http.routers.portainer.entrypoints=https"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.services.portainer.loadbalancer.server.port=9443"
- "traefik.http.services.portainer.loadbalancer.server.scheme=https"
- "traefik.http.services.portainer.loadbalancer.serverstransport=insecure@file"
volumes:
portainer_data:
name: portainer_data
networks:
traefik-net:
external: true
That last label needs a matching entry in your Traefik dynamic config file:
The ports: block is gone — traffic now arrives through Traefik only. Recreate:
7. Deploy your first stack from the UI
Stacks → Add stack gives you three ways in: paste a Compose file into the web editor, upload one, or point at a Git repository.
The Git option is the interesting one. Give it a repo URL and a path to docker-compose.yml, enable GitOps updates with a 5-minute polling interval, and Portainer will redeploy the stack whenever you push. That turns your Gitea instance into the source of truth for the lab, and the UI into a viewer rather than a place where undocumented changes happen.
Stacks created outside Portainer show as 'limited'
Containers started by docker compose up on the CLI appear under Containers, but Portainer will not let you edit them as a stack because it has no copy of the Compose file. Either move the file into a Portainer stack, or accept that CLI stacks stay CLI stacks. Mixing the two on the same app is how you end up with two competing definitions.
Updating
Portainer will not update itself — the "update" it offers in the UI applies to your containers, not to Portainer.
Your data lives in the portainer_data volume, so this is safe. If you run Diun, it will tell you when a new LTS image lands.
Backup
Two options, and you want both:
From the UI: Settings → Backup Portainer produces an encrypted archive of the whole config. There is a scheduled S3 option in CE too.
From the host:
docker run --rm \
-v portainer_data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/portainer-$(date +%F).tar.gz -C /data .
Troubleshooting
"Failed to load resource" or a blank dashboard. Almost always the Docker socket. Check the container can read it: docker exec portainer ls -l /var/run/docker.sock.
Locked out of the setup screen. Restart the container: docker restart portainer, then complete the form promptly.
Forgotten admin password. Stop Portainer, then run the password reset helper against the same volume:
docker stop portainer
docker run --rm -v portainer_data:/data portainer/helper-reset-password
docker start portainer
It prints a new random password to the terminal.
502 Bad Gateway through Traefik. You missed the scheme=https label, or the serversTransports entry. Traefik is speaking plain HTTP to an HTTPS port.
Agent endpoint shows as down. From the Portainer host: curl -k https://<agent-ip>:9001/ping. If that fails it is firewall or routing, not Portainer.
Where this sits in my lab
Portainer runs on the Docker VM on my first Proxmox node, with agents on the Unraid box and on proxmox2. That gives one URL for all three hosts — the *arr stack, the networking services and the utilities — without three sets of SSH keys and three terminal tabs.
I still write Compose files in an editor and keep them in Gitea. Portainer is where I look at logs, restart something at 1am from my phone, and check what is eating disk. Treating it as a viewer rather than an editor keeps the Git repo honest.