Use case · Container management

Portainer CE self hosted on a dedicated server, admin port on loopback

Portainer CE is a free web UI for Docker that runs as one container on the host it manages, and the safe way to self-host it on a dedicated server is to bind its HTTPS port 9443 to 127.0.0.1, create the administrator through your own SSH tunnel inside the five minutes the docs allow, and only then decide whether to expose it. It drives Docker through the socket, so its admin login carries the power of root on the machine.

See the machines

What Portainer CE is, and what its makers say it is for

Portainer Server is a container that drives the Docker engine through its Unix socket and shows containers, images, volumes, networks and Compose stacks in a browser. The Community Edition (CE) lives on GitHub as portainer/portainer under the zlib licence. Its README is blunt about scope: CE is meant for homelabs, learning, personal projects and other non-production deployments, it has no official support channel, and production or business use is pointed at Business Edition (BE), which the README says adds RBAC, GitOps, edge management, audit and SSO among other things. A Portainer FAQ adds that some features are kept in BE to fund CE's development.

Read that before putting a company's estate behind CE. On a dedicated server it fits a lab, a staging host, a personal app box, or a small team willing to support itself.

The Docker socket is root

The official command mounts /var/run/docker.sock into the container, and the install guide assumes Docker runs as root. Our reading, not a sentence from the docs: whatever can drive that socket can start a container that mounts the host's /, so a Portainer administrator account, or a stolen session for one, is root on the server. Portainer's security page draws the same line from its side: within the access you grant, it does not restrict what Docker itself lets an authorised user do, and how you expose the instance is your responsibility.

What CE gives you to narrow that:

  • Users and teams, each user either an administrator or not. The finer roles (Operator, Helpdesk, Read-only and the rest) belong to RBAC, which the docs mark as Business Edition only.
  • Per-resource access control: administrators only, all users, or restricted to named users and teams. Containers started outside Portainer appear as external and are administrator-only by default; labels such as io.portainer.accesscontrol.teams=dev open them to a team.
  • Docker security settings for non-administrators on the environment's Setup page: bind mounts, privileged mode, host PID 1, device mappings, capabilities, sysctl and security-opt are hidden by default. The docs admit they cannot block every capability a container could be given: host PID 1 lets a user run commands as root on the host, and a device mapping gives complete access to a host disk. Hiding Stacks from non-administrators is their blunt option.
  • Internal passwords default to a 12-character minimum, and sessions last 8 hours unless you change it.

Treat every Portainer administrator login as SSH-as-root. Hand out non-admin accounts only with those security settings left on.

Ports and sizing

PortWhat uses itIn this setup
9443/tcpUI and API over HTTPS, self-signed certificate by defaultPublished on 127.0.0.1 only
8000/tcpTCP tunnel server for Edge agents; the docs call it optionalNot published
9000/tcpPlain HTTP, added only for legacy reasons with -p 9000:9000Not published
9001/tcpPortainer Agent on another Docker host, reached by the serverOnly if you add hosts that way

Portainer publishes no CPU or memory figure for the server itself, so size the machine for the containers it will manage. One storage note from the requirements page: a stack deployed from Git is cloned into the portainer_data volume, so large or numerous repositories take real disk, and the docs suggest SSD-class storage for larger deployments. Images are built for x86_64 and ARM64. The install guide wants the latest Docker, running as root, and warns against the snap build on Ubuntu; we install it from Docker's own repository.

Which machine to run it on

Portainer is light; the containers behind it set the size. Memory first, then cores. Every line on the dedicated server range is a whole machine with root, an unmetered port so image pulls never meet a transfer bill, and OS reinstalls from the control panel.

  • Moonshot cartridges: one small Docker host per job. Only the m510 and m710x carry a console; the other cartridges have power control only. The X-Gene ARM cartridge is excluded: it boots Ubuntu 16.04 and 18.04 only, too old for the Engine 28 check below.
  • Single socket: an application and its database containers on one box, with an out-of-band console (iLO, iDRAC or a Supermicro BMC) to undo a firewall mistake. The Ampere Altra line sits here and is ARM64, which Portainer's images support.
  • Dual socket: dozens of stacks side by side.

A racked, ready machine takes about 30 minutes from payment approval to handover; a build to order takes 4 to 24 hours. If one Docker host stops being enough, our Kubernetes clusters run on whole servers with us operating the control plane and you keeping root on every worker.

Install with 9443 on loopback

Docker Engine must already be installed from Docker's repository. Before anything else, start a tunnel from your workstation in a second terminal and leave it running: ssh -N -L 9443:127.0.0.1:9443 you@your-server (your own login, not a service account). The setup clock starts when the container first starts, so the tunnel comes first. Then, as root on the server (sudo -i):

sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable \
  && v=$(docker version --format '{{.Server.Version}}') && [ "${v%%.*}" -ge 28 ] \
  && echo "Docker Engine $v: loopback ports stay local" \
  && docker volume create portainer_data \
  && docker run -d -p 127.0.0.1:9443:9443 --name portainer --restart=always \
       -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data \
       portainer/portainer-ce:lts \
  && { sleep 5; docker logs portainer 2>&1 | grep setup_token \
       || echo "Portainer is running but no token yet: run docker logs portainer | grep setup_token"; } \
  || echo "stopped: read the line above before going on"

Two changes from the official docker run, both ours: 9443 is bound to 127.0.0.1, and -p 8000:8000 is dropped because no Edge agents are used (add it back if you need them). The docs also give a Compose file with the same volume; the same edits apply there (127.0.0.1:9443:9443, and delete the 8000 line).

A port mapping with no address is published on 0.0.0.0 and [::], and Docker-published traffic skips ufw's rules on IPv4, and on IPv6 too when Docker's own IPv6 networking is enabled. Bind it to 127.0.0.1 instead. Docker's docs warn that before Engine 28.0, machines on the same network segment could reach a port published to 127.0.0.1, which is why the block refuses an older engine.

Creating the administrator through the tunnel

  1. Within 5 minutes of the container starting, open https://localhost:9443 on your own computer. The browser warns about the self-signed certificate; on this private path that is expected.
  2. Paste the value after setup_token= from the block's last line. Since versions 2.43 and 2.39.4, a new instance refuses setup without it; the token works once.
  3. Create the administrator: the username defaults to admin and the password needs at least 12 characters.
  4. Skip Edge Compute unless Edge agents will connect. The wizard then picks up the local Docker environment.
  5. Missed the window? Portainer shuts its service down and the page stops answering. docker stop portainer && docker start portainer gives another 5 minutes; take the token from the newest setup_token= line in docker logs portainer.

The docs also offer --admin-password-file to preset the password at first start with no token, and --no-setup-token for networks you fully control. Neither is needed here: the tunnel keeps the instance unreachable until the administrator exists.

Exposing it: its own certificate or a reverse proxy

The smallest public footprint is none: keep using the tunnel. If people need Portainer without SSH, the docs give two routes.

  • Its own certificate. Recreate the container with --sslcert and --sslkey pointing at PEM files that include the full chain, or upload them under Settings, SSL certificate; certificates passed on the command line override uploaded ones. With Certbot, mount both the live and the archive directories read-only, because the docs say Docker has trouble with Certbot's symlinks. Publish 9443 on the public address and add ufw allow 9443/tcp: on IPv6 without Docker's IPv6 networking, ufw does filter the port.
  • A reverse proxy. Forward the Host header exactly as the browser sent it (proxy_set_header Host $http_host in nginx), send an X-Forwarded-Proto that matches the scheme the browser really used, and start Portainer with --trusted-origins https://portainer.example.com if the proxy rewrites the host. The classic fault: browser to proxy on plain HTTP, proxy to 9443 over HTTPS. Portainer then sets Secure cookies, the browser drops them, and every login answers 401. It also sends frame-ancestors 'none', so it will not load inside an iframe. For one proxy in front of several apps, see our Nginx Proxy Manager setup.

Publish 8000 only when Edge agents dial in; the Edge Agent on a managed host needs no open port. If you push your own images, a private registry on the same machine can be added to Portainer as a registry.

Backups, restore and updates

CE's built-in backup is Settings, Back up Portainer, Download backup file: a tar.gz, optionally password-protected, holding Portainer's database and the stack files it deployed. Scheduled backups and copies to S3 or Azure Blob are Business Edition features. That file can be restored only on a fresh instance during initial setup, so the token and the 5-minute window apply again. Neither it nor the archive below contains your containers, images or the data in their volumes; back those up per application.

Our addition is an archive of the whole portainer_data volume, taken with Portainer stopped. Stopping Portainer does not stop the containers it manages. Run as root, then copy the archive off the machine yourself:

set -o pipefail
install -d -m 700 /root/portainer-backups
D=$(docker volume inspect -f '{{.Mountpoint}}' portainer_data) \
  && A=/root/portainer-backups/portainer_data-$(date +%F-%H%M).tar.gz \
  && docker stop portainer \
  && tar czf "$A" -C "$D" . && tar -tzf "$A" >/dev/null && echo "backup ok: $A"
docker start portainer

To restore a volume archive, run this as root (sudo -i) with B set to it. The block refuses to reuse an existing set-aside name, checks B, stops Portainer, archives the current volume and verifies that copy before anything is deleted, then unpacks B and starts. To roll back, run it again with B set to the before-restore file it printed.

set -o pipefail
B=/root/portainer-backups/portainer_data-YYYY-MM-DD-HHMM.tar.gz
D=$(docker volume inspect -f '{{.Mountpoint}}' portainer_data)
S=/root/portainer-backups/before-restore-$(date +%F-%H%M%S).tar.gz
[ ! -e "$S" ] && [ "$S" != "$B" ] && tar -tzf "$B" >/dev/null && docker stop portainer \
  && tar czf "$S" -C "$D" . && tar -tzf "$S" >/dev/null && echo "set-aside ok: $S" \
  && find "$D" -mindepth 1 -delete && tar xzf "$B" -C "$D" \
  && docker start portainer && echo "restored from $B" \
  || echo "stopped at a failed step: if the old data was already removed, restore the set-aside copy named above; otherwise run docker start portainer"

Updates follow the docs' pattern: remove the container, keep the volume, run the new image, and Portainer upgrades its database on start; the docs recommend a backup first. One change of ours: pull before removing, so a failed download leaves the running container alone: docker image pull portainer/portainer-ce:lts && docker stop portainer && docker rm portainer, then the docker run line from the install block. Agents, if you use any, must match the server's version.

Licence: zlib, and how Business Edition is positioned

The LICENSE file in portainer/portainer is the zlib licence (copyright 2018 Portainer.io). Our general reading, not legal advice: it is permissive. You may use, change and redistribute the code, commercially too; you must not misrepresent its origin, altered versions must be plainly marked, and the notice must stay in any source distribution. It puts no duty on you to publish source.

Licence and product scope are separate questions. zlib lets a company run CE; the README says CE is neither intended for nor supported in production, and sells BE for that, with plans listed on Portainer's own site. The docs say a CE instance can be upgraded to BE and keeps its configuration.

Questions

Does a single Portainer host need port 8000?

No. The install guide calls the 8000 tunnel server optional and needed only for Edge agents. A server that manages its own Docker through the local socket uses 9443 alone, and in this setup even that stays on 127.0.0.1.

Will Portainer show containers I start with docker compose from the shell?

Yes. It reads the whole engine, so they appear, marked external with limited control in Portainer. They are administrator-only by default; access-control labels such as io.portainer.accesscontrol.public or io.portainer.accesscontrol.users open them to other Portainer users.

Can a company use Portainer CE?

The zlib licence allows any use, commercial included; that is our general reading, not legal advice. The project itself says CE is meant for non-production work, comes with no official support, and points businesses at Business Edition. Decide on those terms, not the licence alone.

Can CE back itself up on a schedule?

Not from its own settings. The docs list scheduled local backups and copies to S3 or Azure Blob as Business Edition features; CE downloads a backup file on demand. The volume archive on this page is a shell command, so run it before every update and keep copies off the machine.

Why does a new Portainer ask for a setup token?

Since 2.43 and 2.39.4, a fresh instance needs the one-time token printed in its log, so nobody else can claim it first. The admin must still be created within 5 minutes of the first start. The docs allow --no-setup-token only on networks you fully control; with the tunnel route there is no reason to use it.

A Docker host with root and a console

Pick a dedicated line, size its memory for your containers, and keep Portainer on loopback from the first boot.

See the machines

Related on Server Room

Other use cases