Skip to content
+1 (813) 212-3723 support@layeronecloud.com
Self-hosting VPS guides

How to host Portainer CE on a VPS with Docker and HTTPS

Install Portainer CE on an Ubuntu VPS, keep its management port private, publish it through Nginx with HTTPS, and back up the configuration safely.

6 min read

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 A record for portainer.example.com pointing 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.

Official and LayerOne references

Still stuck

Chat with us from the portal.

Ask the assistant from the Chat bar. During business hours you can ask for a person and a human joins live.