I self-hosted Portainer CE on Ubuntu 24.04 — I missed the admin setup window

I self-hosted Portainer CE on Ubuntu 24.04 — I missed the admin setup window

I wanted a Docker UI without handing the socket to a SaaS console. Portainer CE started; I delayed the first admin page and the setup window closed. After a container restart I created the admin, put Caddy in front of 9443, and the local environment listed containers.

· Updated · 7 min read #self-hosted #open-source #deployment #docker #vps #management #ui #containers #portainer

 Self-hosted Portainer CE management UI

Caption: Portainer CE in front of Docker: stacks, containers, optional remote agents.

Why I wanted this on my server

I was tired of SSH-ing for every restart and log tail. Portainer CE is a web UI for Docker: containers, images, volumes, Compose stacks, extra hosts via agents. Solo operators keep inventory across one or two servers; agencies give trusted operators a path that is still not raw root SSH for every log tail. The server mounts the Docker socket (or talks to agents). That is root-shaped power. HTTPS, a strong admin password, and few admin seats.

Not a SaaS console. The socket stays on my VPS. An admin can start containers with host mounts, read volume secrets, and alter networks. I planned firewall rules before exposing 9443 publicly.

What I actually installed

Ubuntu 24.04 LTS, portainer/portainer-ce:lts, Compose at /opt/portainer/portainer-compose.yaml, named volume portainer_data. UI on 9443 (self-signed until a reverse proxy). Optional Edge tunnel 8000 — I would drop it if unused. Hostname portainer.example.com.

Hardware: 1–2 vCPU / 1–2 GB for Portainer itself on a host that already runs a few stacks; 2–4 vCPU / 4 GB on the Portainer server if many environments. 10 GB+ SSD for images and portainer_data. Off-server backups of the volume plus Compose. Optional second Docker host to practice remote Agent enrollment.

Security I would not skip: :lts instead of floating :latest, no raw Docker TCP without mutual TLS, Compose and backup archives private, public firewall only SSH/80/443 when a reverse proxy fronts Portainer (otherwise restrict 9443 to VPN or trusted IPs), limited admin seats.

Docker Engine from Docker’s apt repo — not snap Docker. Portainer warns about snap socket/path issues.

Where it broke

On a fresh Ubuntu 24.04 box this install is famous for the initial admin creation window expiring.

I opened :9443, got distracted, came back, and could not create the first user. Official behavior: finish setup promptly after first boot. Fix:

cd /opt/portainer
docker compose -f portainer-compose.yaml restart

Then complete admin creation on the next visit. Do not delete portainer_data to “reset password” as a shortcut — back up the volume first and follow current CE docs for a reset.

Second: browser certificate warning on 9443. Expected. Default cert is self-signed. I put Caddy in front with tls_insecure_skip_verify to the upstream, or you upload a cert in Settings.

Third: snap Docker. Portainer warns about socket/path issues. I install Engine from Docker’s apt repository. hello-world before Portainer.

Local environment empty: /var/run/docker.sock not mounted or Docker not running (systemctl status docker).

Other documented issues: container exits immediately — port 9443 already bound (ss -tlnp | grep 9443) or a bad volume mount. Remote agent will not connect — 9001 reachable from the Portainer server only, agent running, Environment URL host:9001 without a scheme, matching AGENT_SECRET if customized. SELinux denials on RHEL-family hosts need extra labels; this path is Ubuntu. I keep production Compose in Git even when I deploy from the UI. Day-to-day: restart a failed container, stream logs, prune unused images before disk fills, switch environments when I operate more than one host.

 Portainer CE Docker Compose topology

Caption: One server container, data volume, socket, 9443, optional 8000.

The working install

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl ufw

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Only if you will hit Portainer TLS directly without a reverse proxy:
# sudo ufw allow 9443/tcp
sudo ufw enable
sudo ufw status

1. Install Docker Engine

sudo apt-get update -qqy
sudo apt-get install ca-certificates curl -qqy
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

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 -qqy
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -qqy

sudo usermod -aG docker "$USER"
newgrp docker

docker --version
docker compose version
docker run --rm hello-world

2. Create the Portainer directory and Compose file

sudo mkdir -p /opt/portainer
sudo chown "$USER":"$USER" /opt/portainer
cd /opt/portainer
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  # Remove if you do not intend to use Edge Agents

volumes:
  portainer_data:
    name: portainer_data

networks:
  default:
    name: portainer_network

For a reverse proxy on the same host, publish "127.0.0.1:9443:9443" and leave 8000 unpublished unless Edge Agents need it.

3. Start Portainer CE

cd /opt/portainer
docker compose -f portainer-compose.yaml up -d
docker compose -f portainer-compose.yaml ps
docker ps --filter name=portainer
docker compose -f portainer-compose.yaml logs --tail=100

Port 9443 already bound: ss -tlnp | grep 9443.

4. Complete initial admin setup

Open https://YOUR_SERVER_IP:9443 or https://portainer.example.com:9443. Create the admin immediately. Connect the local Docker environment when prompted.

5. Put HTTPS in front with a reverse proxy

portainer.example.com {
  reverse_proxy https://127.0.0.1:9443 {
    transport http {
      tls_insecure_skip_verify
    }
  }
}
curl -I https://portainer.example.com

Configuration

Socket mount can create privileged containers. Restrict who logs in.

Drop Edge if unused:

cd /opt/portainer
# edit portainer-compose.yaml, remove the 8000 mapping
docker compose -f portainer-compose.yaml up -d

Non-admin users for day-to-day. Unique accounts.

Remote agent on a second Linux Docker host:

docker run -d \
  -p 9001:9001 \
  --name portainer_agent \
  --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /var/lib/docker/volumes:/var/lib/docker/volumes \
  portainer/agent:lts

Allow TCP 9001 only from the Portainer server. In the UI: Environments → Add → Docker Standalone → Agent. Environment URL like remote-docker.example.com:9001host:port, no https://. Custom AGENT_SECRET must match on server and agent.

 Portainer server and remote agents

Caption: One server, local socket, extra hosts on agent port 9001.

Important tips

The Environment URL for an agent is host:9001 with no scheme. I pasted https://… and it would not connect. Also: 502 from Caddy is often TLS mismatch to the self-signed upstream — the tls_insecure_skip_verify block is the documented workaround, not a hack I invented.

Usage

  • Admin login, local environment lists the containers you expect.
  • Non-admin user cannot change admin settings.
  • Stacks → Add stack: tiny whoami, deploy, remove.
  • Keep production Compose in Git even when you click Deploy.
  • After reboot, docker compose -f /opt/portainer/portainer-compose.yaml ps still shows Portainer up.
  • If a remote agent is in play, it stays Connected and 9001 is not open to the world.

I use Portainer for incident restarts and log tails. Image pulls and recreates wait for a maintenance window. Prune unused images before the disk fills. Switch environments when I operate more than one Docker host. I still would not share one admin password across contractors.

 Portainer backup and upgrade workflow

Caption: portainer_data volume, Compose file, off-server copy, LTS pull.

Backup, expose, next step

cd /opt/portainer
docker compose -f portainer-compose.yaml stop

sudo mkdir -p /var/backups/portainer
docker run --rm \
  -v portainer_data:/data:ro \
  -v /var/backups/portainer:/backup \
  alpine:3.20 \
  sh -c 'cd /data && tar czf /backup/portainer-data.tgz .'

cp portainer-compose.yaml /var/backups/portainer/
rsync -a /var/backups/portainer/ backup-user@backup.example.net:/srv/backups/portainer/

docker compose -f portainer-compose.yaml start

Backup tarballs hold auth material.

cd /opt/portainer
docker compose -f portainer-compose.yaml pull
docker compose -f portainer-compose.yaml up -d
docker compose -f portainer-compose.yaml ps
docker compose -f portainer-compose.yaml logs -f --tail=100

What I have running now: Portainer CE :lts on Ubuntu, admin created after a restart, Caddy on 443, local Docker via the socket, 8000 unpublished. Backup tarballs hold auth material — I treat them as secrets. Next: a volume restore drill, fewer admin seats, maybe one remote agent with 9001 locked to the server IP, SSO or VPN if this becomes a team console. I track the :lts image so the control plane stays rebuildable.

Did you hit the same wall?

I got stuck on the expired initial admin setup window and had to restart the container to create the first user. Did you hit the same thing, or a different one — snap Docker, agent URL with https://, 9443 already bound? Tell me in the comments. I read them.

Need this done on your server?

I deploy and harden Laravel/CodeCanyon apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.