Portainer Community Edition (CE) is a web interface for Docker that lets you manage containers, images, volumes, networks and Compose stacks without typing every docker command by hand. In this tutorial you will install Docker Engine from the official repository on Ubuntu 24.04, run Portainer CE as a container bound to localhost, and publish it through Nginx with a free Let's Encrypt certificate. You will finish by deploying a test stack and, optionally, connecting a second Docker host with the Portainer Agent.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 1 GB of RAM and 10 GB of free disk space.
  • A non-root user with sudo privileges.
  • A domain or subdomain (this guide uses portainer.your_domain) with a DNS A record pointing to your_server_ip.
  • Ports 22, 80 and 443 reachable from the Internet.

Step 1 - Installing Docker Engine

Ubuntu's own docker.io package lags behind upstream, so install Docker from Docker's official repository. First add the repository key:

sudo apt update
sudo apt install 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

Then add the repository itself:

sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF

Install the engine and the Compose plugin:

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Confirm the service is running:

sudo systemctl is-active docker
sudo docker version --format '{{.Server.Version}}'
active
28.4.0

Your version number will differ. The commands in this guide use sudo docker. If you prefer to run Docker without sudo, add your user to the docker group with sudo usermod -aG docker $USER and log in again, keeping in mind that membership in that group is equivalent to root access.

Step 2 - Running the Portainer container

Portainer stores its database, users and settings in /data, so create a named volume for it:

sudo docker volume create portainer_data

Start Portainer CE. The HTTP port 9000 is published only on 127.0.0.1, so the UI is never exposed directly: Nginx will be the only way in. The Docker socket is mounted so Portainer can manage the local engine:

sudo docker run -d \
  --name portainer \
  --restart=always \
  -p 127.0.0.1:9000:9000 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:lts

The lts tag follows Portainer's long-term support releases. Check that the container is up and that Portainer answers locally:

sudo docker ps --filter name=portainer --format '{{.Names}}  {{.Status}}  {{.Ports}}'
curl -sI http://127.0.0.1:9000 | head -n 1
portainer  Up 12 seconds  127.0.0.1:9000->9000/tcp, 8000/tcp, 9443/tcp
HTTP/1.1 200 OK

Step 3 - Configuring Nginx as a reverse proxy

Install Nginx and allow SSH and web traffic through UFW:

sudo apt install nginx
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Portainer uses WebSockets for the container console and live logs, so the proxy must forward the Upgrade headers. Create a server block:

sudo nano /etc/nginx/sites-available/portainer
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name portainer.your_domain;

    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        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_read_timeout 3600s;
    }
}

client_max_body_size allows uploading images and backup files through the UI. Enable the site, test the syntax and reload:

sudo ln -s /etc/nginx/sites-available/portainer /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Step 4 - Enabling HTTPS with Let's Encrypt

Install Certbot with its Nginx plugin. It obtains the certificate and adds the TLS configuration and an HTTP to HTTPS redirect to the server block you just created:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d portainer.your_domain

Follow the prompts (email address and terms of service). When it finishes, confirm that automatic renewal works:

sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/portainer.your_domain/fullchain.pem (success)

The certbot.timer systemd timer installed by the package runs the renewal twice a day, so no cron job is needed.

Step 5 - Creating the admin user

Open https://portainer.your_domain in your browser. Portainer asks you to create the initial administrator: choose a username (not admin if you want to avoid guessable names) and a password of at least 12 characters.

After logging in, click Get Started. Portainer detects the local Docker socket and creates an environment called local. Click it to open the dashboard, which shows the number of containers, images, volumes and networks on the host. The portainer container itself appears in Containers.

Step 6 - Deploying a stack

Stacks are Docker Compose projects managed by Portainer. To test one, go to Stacks, click Add stack, name it whoami and paste this into the Web editor:

services:
  whoami:
    image: traefik/whoami
    restart: unless-stopped
    ports:
      - "127.0.0.1:8081:80"

Click Deploy the stack. Portainer pulls the image and starts the container. Verify it from the server:

curl -s http://127.0.0.1:8081 | head -n 3
Hostname: 3f1c2b9d8e7a
IP: 127.0.0.1
IP: 172.18.0.2

From the stack page you can edit the Compose file and redeploy, open container logs, or use Console to get a shell inside a container. Delete the whoami stack when you are done.

Step 7 - Adding a remote host with the Portainer Agent (optional)

To manage another Docker server from the same UI, run the Portainer Agent on that server. Use the same release track as the Portainer server (lts here):

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

Replace your_private_ip with an address of the remote host on a private network that the Portainer server can reach. The agent gives full control of that Docker engine, so never publish port 9001 on a public interface.

In Portainer, go to Environments, click Add environment, select Docker Standalone, start the wizard and choose Agent. Enter a name and your_private_ip:9001 as the environment address, then click Connect. The new environment appears on the home page with its status and Docker version.

Step 8 - Backing up and updating Portainer

Portainer CE includes a backup feature: go to Settings, scroll to Back up Portainer, optionally set a password, and click Download backup file. Store the file off the server. You can also archive the whole data volume from the command line:

sudo docker run --rm -v portainer_data:/data:ro -v "$PWD":/backup alpine \
  tar czf /backup/portainer_data-$(date +%F).tar.gz -C /data .
ls -lh portainer_data-*.tar.gz

To update Portainer, pull the new image and recreate the container with the same options. Your data survives because it lives in the portainer_data volume:

sudo docker pull portainer/portainer-ce:lts
sudo docker stop portainer
sudo docker rm portainer
sudo docker run -d \
  --name portainer \
  --restart=always \
  -p 127.0.0.1:9000:9000 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:lts

Check the version shown at the bottom of the left menu after logging in again. Update the agents on remote hosts the same way.

Troubleshooting

  • "Your Portainer instance timed out for security purposes": the five-minute window for creating the admin expired. Run sudo docker restart portainer and reload the page.
  • 502 Bad Gateway from Nginx: Portainer is not listening. Check sudo docker ps -a --filter name=portainer and sudo docker logs portainer.
  • Console or logs do not load: the WebSocket headers are missing. Make sure the map block and the Upgrade/Connection headers are present, then run sudo nginx -t && sudo systemctl reload nginx.
  • Agent environment shows as down: from the Portainer server run nc -zv your_private_ip 9001. If it fails, check the private network and any firewall between the hosts.

Conclusion

Portainer CE is now running on Ubuntu 24.04, reachable only through Nginx over HTTPS, with the local Docker engine ready to manage and a workflow for stacks, backups and updates. As next steps, create additional users under Users with limited access, move your existing Compose projects into Portainer stacks, and schedule the volume backup command with a systemd timer or cron.