Portainer provides a web interface for managing Docker containers, images, networks, volumes, and Compose stacks. In this tutorial, you will install Portainer Community Edition on a Raff Technologies Linux VM running Ubuntu 24.04, keep the management UI off the public Docker port, place it behind Nginx and Let's Encrypt, create the first administrator, manage the local Docker environment, deploy a test stack, back up Portainer's configuration, and verify the deployment end to end.
Portainer has privileged access to the Docker daemon when you mount /var/run/docker.sock, so the Portainer administrator effectively has control over the Docker host. Treat the dashboard as an administrative interface, not a public application. This guide therefore binds Portainer's HTTPS port to localhost and publishes it through an authenticated administrator-facing hostname such as portainer.example.com instead of opening port 9443 directly to the internet.
For production workloads, Portainer recommends its LTS release stream. As of September 2026, Portainer 2.45.0 LTS includes the fix for a critical Docker API authorization issue affecting earlier supported releases, so use the current lts image and verify the running version after deployment. You need Docker with Compose v2, Nginx, a domain pointed to the VM, and a non-root user with sudo privileges.
Step 1 — Confirm Ubuntu, Docker, and DNS
Check the operating system:
cat /etc/os-release
Check Docker and Compose:
docker --version docker compose version
Check that the management hostname resolves to the VM:
dig +short A portainer.example.com
If Docker is not installed, follow How to Install Docker on Ubuntu 24.04. Use Docker Engine installed through Docker's supported Linux installation path rather than the Ubuntu Snap package; Portainer's current Docker-on-Linux guidance warns that Docker installed via Snap can cause compatibility issues. If Nginx is missing, use How to Install Nginx on Ubuntu 24.04.
Verify: Ubuntu should report 24.04, Docker Compose v2 should respond, the DNS A record should return the VM's public IPv4 address, and your Docker installation should not be the Snap-packaged variant.
Step 2 — Create Persistent Portainer Storage
Create the Docker volume that stores Portainer's configuration database and settings:
docker volume create portainer_data
Inspect it:
docker volume inspect portainer_data
This volume is separate from the Portainer container. Removing and recreating the container does not delete the configuration unless you also remove the volume.
Verify: docker volume inspect portainer_data should return the volume metadata without an error.
Step 3 — Deploy Portainer CE on the LTS Release Stream
Run Portainer with its management HTTPS port bound only to localhost:
docker run -d \ --name portainer \ --restart=always \ -p 127.0.0.1:9443:9443 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer_data:/data \ portainer/portainer-ce:lts
Portainer's official Docker installation uses port 9443 for the HTTPS UI. Port 8000 is optional and is only required when you use Edge Agent tunnel features, so this single-host deployment does not publish it.
The Docker socket mount gives Portainer broad control over the Docker daemon. Anyone who gains Portainer administrator access can perform actions equivalent to administrative Docker access, so protect this service accordingly.
Check the container:
docker ps --filter name=portainer
Review startup logs:
docker logs --tail=50 portainer
Verify: The container should be running and sudo ss -ltnp | grep 9443 should show 127.0.0.1:9443, not 0.0.0.0:9443.
Step 4 — Verify the Portainer Version Before Continuing
Check the installed Portainer version:
docker exec portainer /portainer --version
As of September 2026, do not run an affected 2.40.x through 2.44.x build. Portainer 2.45.0 LTS and 2.39.7 LTS contain the August 2026 fix for the Docker API authorization bypass. The lts tag should track the supported LTS stream, but verifying the actual running version prevents an unexpectedly stale local image from going unnoticed.
If the version is stale, pull the LTS image and recreate Portainer:
docker pull portainer/portainer-ce:lts docker stop portainer docker rm portainer
Then rerun the deployment command from Step 3.
Verify: The reported version should be a supported, patched LTS release. At the time of this update, that means 2.45.0 LTS or a later patched release.
Step 5 — Configure Nginx as the Public Entry Point
Create an Nginx server block:
sudo nano /etc/nginx/sites-available/portainer.example.com
Add:
server { listen 80; listen [::]:80; server_name portainer.example.com; location / { proxy_pass https://127.0.0.1:9443; proxy_http_version 1.1; 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_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_ssl_verify off; proxy_read_timeout 600s; proxy_send_timeout 600s; } }
Portainer generates a self-signed certificate for its internal HTTPS listener by default. Because this upstream connection stays on loopback, Nginx can proxy to it while public users receive the trusted Let's Encrypt certificate configured in the next step. The Upgrade and Connection headers preserve WebSocket-based management paths such as interactive console sessions. proxy_ssl_verify off applies only to the local self-signed upstream connection in this configuration.
Enable the site:
sudo ln -s /etc/nginx/sites-available/portainer.example.com \ /etc/nginx/sites-enabled/portainer.example.com sudo nginx -t sudo systemctl reload nginx
Verify: sudo nginx -t should report successful syntax and curl -I http://portainer.example.com should reach the Nginx virtual host.
Step 6 — Add HTTPS and Keep Portainer's Direct Port Private
If UFW is active, make sure SSH remains allowed before changing firewall rules:
sudo ufw status sudo ufw allow OpenSSH
Allow Nginx traffic:
sudo ufw allow 'Nginx Full'
Do not add a public UFW rule for port 9443 because Docker is already binding it to localhost only. This keeps the direct Portainer backend private, but the Nginx hostname can still be reachable from the public internet. Treat that hostname as a privileged administrative endpoint; for higher-security environments, restrict it with a VPN, trusted-source IP policy, or another deliberate access-control layer.
Install Certbot and request a certificate:
sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx --redirect -d portainer.example.com
Test certificate renewal:
sudo certbot renew --dry-run
Verify: curl -I https://portainer.example.com should complete over HTTPS, HTTP should redirect to HTTPS, and sudo ss -ltnp | grep 9443 should still show the backend bound to localhost only.
Step 7 — Create the First Administrator Within the Setup Window
Open:
https://portainer.example.com
Create the first administrator. Portainer requires the initial password to be at least 12 characters. Use a unique password stored in a password manager.
Portainer intentionally times out an uninitialized installation if the first administrator is not created within five minutes. If the instance reports that it timed out for security purposes, restart it:
docker stop portainer docker start portainer
Then complete the initial setup promptly.
After the administrator is created, Portainer should automatically detect the local Docker environment. Select Get Started to manage it.
Verify: You should be able to sign in over the public HTTPS hostname and see the local Docker environment without a browser certificate warning.
Step 8 — Inspect the Local Docker Environment Safely
Open the local environment in Portainer and review:
- Containers
- Images
- Networks
- Volumes
- Stacks
Use the UI for routine inspection and lifecycle actions, but remember that Portainer is an administrative control plane. Avoid granting dashboard access merely as a substitute for application-level access.
From the command line, compare what Docker sees:
docker ps -a docker volume ls docker network ls
The resources shown by these commands should correspond with the Portainer dashboard.
Verify: An existing Docker container should appear in both docker ps -a and the Portainer Containers view with matching state.
Step 9 — Deploy and Remove a Test Docker Compose Stack
In Portainer, open Stacks and create a stack named portainer-test. Use the web editor with:
services: web: image: nginx:alpine restart: unless-stopped ports: - "127.0.0.1:8080:80"
Deploy the stack. The test container is deliberately bound to localhost so you do not need to expose another public firewall port just to verify the stack workflow.
Test it from the VM:
curl -I http://127.0.0.1:8080
Then remove the test stack from Portainer when the check is complete.
Confirm cleanup:
sudo ss -ltnp | grep 8080 || echo 'test port removed'
Verify: The local curl should return an Nginx HTTP response before cleanup, and port 8080 should no longer be listening after the test stack is removed.