Portainer Community Edition gives you a web interface for Docker containers, images, networks, volumes, and Compose stacks. It is convenient precisely because it can control Docker, which also makes the Portainer account and its Docker-socket mount equivalent to root access on this server. Do not treat it like an ordinary public dashboard.
This guide installs the upstream portainer/portainer-ce:lts image on Ubuntu,
binds Portainer's HTTP listener to loopback, and lets Nginx provide the only
public entry point at https://portainer.example.com.
VPS size, CPU, storage, and price
| Item | Planning value |
|---|---|
| Minimum RAM | 1 GB for the operating system, Docker, and Portainer alone; this is a practical floor, not an upstream hard limit |
| Recommended RAM | 2 GB when the same VPS also runs a few light containers |
| Expected CPU usage | About 0.02–0.10 vCPU idle; short bursts of 0.25–1 vCPU while refreshing environments, reading logs, or deploying stacks |
| Storage requirement | Allow 10 GB for Ubuntu, Docker, Portainer, and logs, then add all container images, writable layers, volumes, and backup staging space |
| Exact LayerOne SKU | gc.micro — slug layerone-starter, 1 vCPU, 2 GB RAM, 40 GB disk |
| Expected monthly cost | $0.0068 per active hour; about $4.96 per 730 hours against the advertised $5.00 730-hour equivalent |
A 31-day month has 744 hours, so the same continuously allocated server is
about $0.0068 × 744 = $5.06. Stopping a VPS does not release its reserved
resources; destroy it to stop server metering. Workload, transfer, storage
growth, and optional services vary, and LayerOne bills servers
hourly.
The CPU estimate is a planning range, not a guarantee. Containers managed by Portainer consume their own CPU, RAM, and disk in addition to the Portainer service.
Assumptions
- Ubuntu 22.04 or 24.04, with a public IPv4 address
- A DNS
Arecord forportainer.example.compointing to that address - Docker Engine and the Compose plugin installed from Docker's repository; use Running Docker first
- A non-root sudo user and SSH key authentication; see Harden a new server
Replace portainer.example.com and ADMIN_IP everywhere below. ADMIN_IP
means the public /32 address from which you administer the VPS, not the VPS's
own address.
1. Apply the cloud and guest firewall rules
In the LayerOne cloud firewall, use inbound default DROP and allow:
| Source | Protocol | Port | Purpose |
|---|---|---|---|
ADMIN_IP/32 |
TCP | 22 | SSH administration |
0.0.0.0/0 |
TCP | 80 | ACME HTTP validation and redirect |
0.0.0.0/0 |
TCP | 443 | Portainer HTTPS UI and API |
Do not expose TCP 9000 or 9443. Portainer uses 8000 only for Edge Agent tunnels, so do not open it unless you deliberately deploy that feature. Mirror the policy with UFW:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from ADMIN_IP to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Docker-published ports can bypass UFW. The Compose file below therefore binds
port 9000 to 127.0.0.1, while the LayerOne cloud
firewall provides the outer boundary.
2. Create the Portainer Compose project
sudo install -d -m 0750 /opt/portainer
sudo chown "$USER":"$USER" /opt/portainer
cd /opt/portainer
Create /opt/portainer/compose.yaml:
services:
portainer:
image: portainer/portainer-ce:lts
container_name: portainer
restart: always
command:
- --http-enabled
- --trusted-origins=portainer.example.com
ports:
- "127.0.0.1:9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
volumes:
portainer_data:
The lts tag follows Portainer's long-term-support channel. The Docker socket
is intentionally mounted because Portainer cannot manage the local Docker
engine without it. Anyone who takes over this container can control the host.
--http-enabled is safe here only because the host publishes port 9000 on
loopback; Nginx supplies public TLS. Do not turn on Force HTTPS only in
Portainer while Nginx uses this HTTP upstream, or the proxy will lose its
backend. Replace the trusted origin when you replace the example hostname.
Validate and start it:
docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 portainer
curl -I http://127.0.0.1:9000
A redirect or successful HTTP response proves the private listener is alive.
From another machine, http://SERVER_IP:9000 should time out or refuse the
connection.
3. Put Nginx and Let's Encrypt in front
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
Create /etc/nginx/sites-available/portainer.example.com:
server {
listen 80;
server_name portainer.example.com;
location / {
proxy_pass http://127.0.0.1:9000;
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_read_timeout 3600;
}
}
Enable it and issue the certificate:
sudo ln -s /etc/nginx/sites-available/portainer.example.com /etc/nginx/sites-enabled/portainer.example.com
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d portainer.example.com
sudo certbot renew --dry-run
The long read timeout keeps interactive container consoles from being closed by the proxy.
4. Complete and verify the first run
Open https://portainer.example.com, create a long unique administrator
password, and select the local Docker environment. Portainer deliberately times
out an unclaimed first-run session; if it reports that the instance timed out,
restart it and finish setup promptly:
cd /opt/portainer
docker compose restart portainer
Then verify all three layers:
docker inspect -f '{{.State.Status}}' portainer
curl -I http://127.0.0.1:9000
curl -I https://portainer.example.com
Deploy a harmless test container, confirm that its logs and console work, then remove it. Never paste registry credentials, environment secrets, or support bundles into a public ticket.
Security checklist
- Give Portainer access only to trusted administrators and use external authentication or multi-factor authentication when your edition and identity provider support it.
- Keep TCP 9000, 9443, and the Docker socket off the public network.
- Do not mount the Docker socket into unrelated containers. Socket access is effectively host-root access, even if the container process is not root.
- Put secrets in Docker secrets or protected environment files rather than in stack definitions committed to a public repository.
- Review Portainer users, teams, API keys, registries, and webhooks regularly.
Back up, restore, and upgrade
In Portainer, Settings → Back up Portainer downloads an optionally encrypted
archive of the configuration in /data. That archive includes Portainer's
metadata and stack definitions, but not the containers, images, application
volumes, or bind-mounted data it manages. Back those up separately and transfer
every backup off this VPS. LayerOne does not take VPS snapshots or backups; see
Backups and snapshots.
Restore a Portainer archive only during the initialization of a fresh Portainer instance with an empty data volume. Test that process before an emergency.
For an LTS update:
cd /opt/portainer
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 portainer
Read the release notes first and take both the Portainer configuration backup and workload-specific backups before changing versions.