Portainer web interface managing Docker containers on a VPS
Docker

Portainer Docker Install: Manage Containers from Your Browser

Docker is a fantastic way to run software on a VPS, right up to the moment you have six containers, three compose files in different folders, and only a vague memory of whether the logs command needed --tail or -n. Then a service misbehaves late in the evening, you are on your phone squinting at an SSH client, and you cannot remember which container is which. Portainer fixes exactly that: it is a web interface for Docker that shows every container, stack, image and volume on one page, lets you read logs with a search box, restart things with one click, and deploy new apps by pasting a compose file into a browser tab.

In this tutorial we do a complete Portainer Docker install on a fresh VPS, the way we would do it for a customer: Docker from the official repository, Portainer started from a compose file, admin access that is never exposed to the open internet, and then the everyday tasks you actually installed it for. Portainer Community Edition is open source under the zlib license, developed at github.com/portainer/portainer , with documentation on portainer.io .

What Portainer is, and the Docker words you need first

Before we touch the terminal, let us agree on a few terms, because Portainer’s menus use them everywhere:

  • Image: a read-only package that contains an application and everything it needs to run (files, libraries, default settings). For example louislam/uptime-kuma:1 is an image. The part after the colon is the tag, usually a version.
  • Container: a running copy of an image. It behaves like an isolated mini system with its own filesystem and processes, but it shares the kernel of your VPS, so it starts in seconds and uses little memory.
  • Volume: storage managed by Docker that survives when a container is deleted or recreated. Databases, uploads and settings live in volumes, which is why you can update an image without losing data.
  • Port: a number on which a program listens for network connections (SSH uses 22, HTTPS uses 443). “Publishing” a port means connecting a port on the VPS to a port inside a container.
  • Compose file and stack: a YAML file that describes one or more containers, their volumes and ports. Portainer calls a deployed compose project a stack.
  • Docker socket: the file /var/run/docker.sock, the door through which programs talk to the Docker engine. Whoever can use this door controls every container on the machine, which in practice means root access to the server.

Portainer itself is just another container. It mounts the Docker socket, talks to the Docker API and turns what it sees into a dashboard. It does not replace Docker or Compose: everything it does you could also do on the command line, which means you can switch back at any time and nothing is locked in.

Community Edition or Business Edition?

Portainer comes in two flavors. Community Edition (CE) is free and open source and covers everything in this guide: stacks, containers, logs, console, images, volumes, users and teams. Business Edition (BE) adds things like fine-grained role-based access control and some automation features, and has a free tier for a small number of nodes. For one VPS, or a handful, CE is exactly what you need, and that is what we install.

What you need

  • A VPS with Debian 13 or Ubuntu 24.04. 1 GB of RAM is enough for Portainer plus a few small apps. A KVM VPS is the simplest choice for Docker; on an unprivileged LXC container Docker only works if nesting (and usually keyctl) is enabled for that container, so if you are on LXC, ask support to enable it before you start.
  • SSH access as a regular user with sudo rights.
  • An SSH client on your computer: the terminal on Linux and macOS, PowerShell on Windows 10/11 (it includes ssh), or PuTTY.
  • Optional, only for Step 4: a domain or subdomain (for example portainer.example.com) with an A record pointing to your VPS IP.
  • About 30 minutes for the setup, then a few seconds for each daily task.
How the pieces connectDreamServer VPS (Debian 13 / Ubuntu 24.04)UFW firewallYour browserB: HTTPS 443A: SSH 22Caddy :443auto HTTPSsshd :22SSH tunnelPortainer CEport 9443Docker Enginedocker.sockStack 1Stack 2Volumes (data)portainer_dataAPI
Figure 1: two ways in. Path A is an SSH tunnel with no public port, path B is Caddy with a real HTTPS certificate. Portainer controls Docker through the socket.

The diagram is the whole idea of this guide. Portainer never listens on a public port. You reach it either through an SSH tunnel (path A, the simplest and safest, nothing to configure on the internet side) or through a reverse proxy called Caddy that terminates HTTPS with a free Let’s Encrypt certificate (path B, convenient when you want a normal URL). A reverse proxy is simply a web server that receives requests from the internet and forwards them to an app running behind it.

Portainer Docker install: the plan in 7 steps

Portainer Docker install: the 7 steps1234567InstallDockerStartPortainerSSH tunnel+ adminHTTPS proxy(optional)Deploya stackLogs andrestartsUpdateimagesOne-time setup, about 20 minutesEveryday work in the browser
Figure 2: Steps 1 to 4 are done once (Step 4 only if you want a public HTTPS address), Steps 5 to 7 are what you will do every week.

Step 1: Install Docker Engine and the Compose plugin

Debian and Ubuntu both ship a docker.io package, but it lags behind upstream and does not always include the modern Compose v2 plugin. We use Docker’s official repository instead, so you get current versions and security fixes directly from the project.

First, refresh the package lists and install two small helpers (on a minimal Debian 13 image curl is often missing):

sudo apt update
sudo apt install -y ca-certificates curl

Next, add Docker’s signing key and repository. The commands below detect your distribution automatically: on Debian, $ID is debian and the codename is trixie; on Ubuntu 24.04, $ID is ubuntu and the codename is noble. That is the only difference between the two systems for this step, so the same block works on both:

sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release
sudo curl -fsSL "https://download.docker.com/linux/$ID/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/$ID $VERSION_CODENAME stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update

The last apt update should now list a line containing download.docker.com. Install the engine and plugins:

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

On both Debian and Ubuntu the docker service is enabled and started automatically after installation. Let us verify:

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

The first command downloads a tiny test image and should print Hello from Docker! followed by a short explanation. The second should print something like Docker Compose version v2.x.x. If you see both, Docker is ready.

A note about the docker group: you can run sudo usermod -aG docker $USER to use Docker without sudo, but membership in that group is equivalent to root access. In this guide we keep using sudo, which is the more honest option on a server.

Step 2: Start Portainer with Docker Compose

We keep Portainer’s compose file in its own folder, /opt/portainer, so it is easy to find later. The command below creates the folder and writes the file in one go, which is safer than copy-pasting into an editor:

sudo mkdir -p /opt/portainer
sudo tee /opt/portainer/compose.yaml > /dev/null <<'EOF'
services:
  portainer:
    image: portainer/portainer-ce:lts
    container_name: portainer
    restart: unless-stopped
    ports:
      - "127.0.0.1:9443:9443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data

volumes:
  portainer_data:
EOF

Line by line, here is what this file says:

  • image: portainer/portainer-ce:lts uses the Community Edition from the LTS (long-term support) release line. It moves only to stable, supported versions, which is a better tradeoff for a server than :latest, which follows every short-term release. If you want full control, replace lts with the exact version number from the project’s releases page and change it deliberately when you upgrade.
  • restart: unless-stopped brings Portainer back after a reboot or crash, unless you stopped it yourself.
  • "127.0.0.1:9443:9443" publishes the HTTPS port only on localhost. This is the most important line. Nobody on the internet can reach it, only programs on the VPS itself, including the SSH tunnel we open in the next step.
  • The socket mount gives Portainer control over Docker. The portainer_data volume stores Portainer’s own database, users, settings and the compose files of the stacks you create.

Notice that there is no version: line at the top. It is obsolete in Compose v2 and only produces a warning. Start the container:

cd /opt/portainer
sudo docker compose up -d

You should see output similar to Network portainer_default Created, Volume portainer_portainer_data Created and Container portainer Started. The volume gets the prefix portainer_ because Compose names projects after their folder. Confirm that the port is bound to localhost only:

sudo docker compose ps
sudo ss -tlnp | grep 9443

The ss line must show 127.0.0.1:9443, not 0.0.0.0:9443. If it shows 0.0.0.0, re-check the ports: line, because Portainer would be visible to the whole internet.

Step 3: Open an SSH tunnel and create the admin account

Here is a detail that catches many people: a new Portainer instance waits only 5 minutes for you to create the admin account. After that it locks itself as a security measure, so a forgotten, freshly installed Portainer cannot be claimed by a stranger. If you started it in Step 2 more than 5 minutes ago, just run sudo docker restart portainer before continuing.

An SSH tunnel forwards a port from your computer, through the encrypted SSH connection, to a port on the VPS. On your own computer (not on the VPS), run:

ssh -L 9443:127.0.0.1:9443 youruser@YOUR_VPS_IP

This logs you into the VPS as usual, and as long as that window stays open, localhost:9443 on your computer leads to Portainer on the server. On Windows, PowerShell accepts the same command. In PuTTY, go to Connection, SSH, Tunnels, enter 9443 as source port and 127.0.0.1:9443 as destination, click Add, then connect. If port 9443 is already busy on your computer, use -L 19443:127.0.0.1:9443 and open port 19443 instead.

Now open https://localhost:9443 in your browser. You will get a certificate warning. That is expected: Portainer generates a self-signed certificate, which encrypts the traffic but is not signed by a public authority. Since the connection is already inside your SSH tunnel, it is safe to accept the warning for this address.

On the setup page:

  1. Choose a username. Avoid admin, it is the first name any attacker tries.
  2. Set a password of at least 12 characters (Portainer enforces this minimum). Use a password manager and generate something long.
  3. Optionally uncheck the anonymous statistics option if you prefer not to send usage data.
  4. Click Create user.

Portainer then asks how to connect to an environment. An environment is a Docker host that Portainer manages. Click Get Started to use the local environment, meaning the Docker engine on this same VPS, reached through the socket we mounted. You land on the Home page, where an environment named local appears. Click it and you will see the dashboard with counters for stacks, containers, images, volumes and networks.

Step 4: Optional, put Portainer behind HTTPS with Caddy

The SSH tunnel is perfectly fine for a single administrator. If you prefer a normal address such as https://portainer.example.com, maybe because you manage things from a tablet or share access with a colleague, add Caddy as a reverse proxy. Caddy obtains and renews Let’s Encrypt certificates automatically, with almost no configuration. Before you start, make sure the DNS A record of your subdomain already points to the VPS, otherwise the certificate request fails.

First, a word about the firewall, because it is often misunderstood. UFW (Uncomplicated Firewall) filters traffic for services running directly on the host, like SSH. Ports published by Docker bypass UFW rules, because Docker writes its own iptables rules. That is why our real protection for Portainer is not a firewall rule but the fact that we never publish 9443 publicly. UFW is still worth enabling for the host. On Ubuntu 24.04 it is installed but inactive; on Debian 13 it is usually not installed, so the first command adds it:

sudo apt install -y ufw
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

Always allow SSH before enabling UFW, or you lock yourself out. If your SSH daemon uses a different port, allow that port instead of 22. Ports 80 and 443 are needed by Caddy: 80 for the certificate challenge and redirects, 443 for HTTPS.

Now replace the compose file with a version that adds Caddy. Portainer keeps its localhost port, so the SSH tunnel still works as a backup way in:

sudo tee /opt/portainer/compose.yaml > /dev/null <<'EOF'
services:
  portainer:
    image: portainer/portainer-ce:lts
    container_name: portainer
    restart: unless-stopped
    ports:
      - "127.0.0.1:9443:9443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data

  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  portainer_data:
  caddy_data:
  caddy_config:
EOF

The caddy:2 tag follows the Caddy 2 major line, which receives fixes without breaking changes. Next, create the Caddyfile. Replace portainer.example.com with your own subdomain:

sudo tee /opt/portainer/Caddyfile > /dev/null <<'EOF'
portainer.example.com {
    reverse_proxy https://portainer:9443 {
        transport http {
            tls_insecure_skip_verify
        }
    }
}
EOF

Caddy reaches Portainer by its service name, portainer, over the private network that Compose creates for the project. Because Portainer’s internal certificate is self-signed, tls_insecure_skip_verify tells Caddy to accept it for this internal hop only. Visitors still get a valid public certificate from Caddy. Apply the changes and watch the logs:

cd /opt/portainer
sudo docker compose up -d
sudo docker compose logs -f caddy

Within a minute you should see a log line mentioning that the certificate was obtained successfully. Press Ctrl+C to stop following the logs, then open https://portainer.example.com. Your data and admin account are still there, because they live in the portainer_data volume, not in the container.

Step 5: Deploy your first stack from the browser

Time to use Portainer for real. We will deploy Uptime Kuma, a small uptime monitoring tool, as an example, but the process is identical for any compose-based app.

In Portainer, open the local environment, click Stacks in the left menu, then Add stack. Give it a name: stack names must be lowercase and may contain letters, digits, hyphens and underscores, so use uptime-kuma. Leave the build method on Web editor and paste:

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - "127.0.0.1:3001:3001"
    volumes:
      - uptime_kuma_data:/app/data

volumes:
  uptime_kuma_data:

Same principles as before: a pinned major version (:1), a restart policy, a localhost-only port and a named volume for the data. Below the editor there is an Environment variables section. Whenever an app needs secrets, such as a database password, define them there and reference them in the YAML as ${DB_PASSWORD}, instead of typing them directly into the file.

Click Deploy the stack. Portainer pulls the image and starts the container, then shows the stack in the list with the control level Total, meaning Portainer created it and can fully manage it. To open the app, add a second forward to your SSH command (you can chain several -L options in one connection):

ssh -L 9443:127.0.0.1:9443 -L 3001:127.0.0.1:3001 youruser@YOUR_VPS_IP

Then visit http://localhost:3001. You will notice that the Portainer stack itself, created from the terminal in Step 2, shows as Limited control. That is normal: Portainer can see and restart its containers, but it does not own the compose file, so edit that one from the terminal.

Tip: publish an app through Caddy as well

If you followed Step 4 and want status.example.com for this app, Caddy must share a network with it. Create a shared network once with sudo docker network create proxy. In the stack editor, add networks: [proxy] under the uptime-kuma service and, at the bottom, a networks: section declaring proxy with external: true. Do the same for the caddy service in /opt/portainer/compose.yaml, listing both default and proxy so it can still reach Portainer. Finally, add a block status.example.com { reverse_proxy uptime-kuma:3001 } to the Caddyfile and run sudo docker compose up -d followed by sudo docker compose restart caddy in /opt/portainer.

Step 6: Read logs and restart containers

Logs are the first thing you look at when something breaks, and this is where Portainer saves the most time. Click Containers, then the name of a container. On the details page, the Logs button opens the log viewer, which offers:

  • Auto-refresh, so new lines appear live, like docker logs -f.
  • Lines to choose how many lines to fetch, and Display timestamps to see when each event happened.
  • A Search box that filters lines as you type, perfect for finding error or a specific IP address.
  • Download logs, handy when you want to send the output to a developer or to our support team.

In the containers list, the small icons in the Quick Actions column take you straight to Logs, Inspect (the full configuration as JSON), Stats (live CPU, memory and network graphs) and Console. Console gives you a shell inside the container: click Connect with /bin/sh selected, since many small images do not include bash.

To restart, tick the checkbox next to one or more containers and click Restart. The same toolbar offers Start, Stop, Kill, Pause and Remove. For a whole application, open Stacks, select the stack and use Stop this stack and Start this stack, which handles all its containers in the right order. A restart does not touch volumes, so data is safe.

Step 7: Update images (and Portainer itself)

Updating means downloading a newer version of the image and recreating the container from it, while volumes keep the data. For a stack, open Stacks, click the stack, go to the Editor tab and click Update the stack. In the dialog, enable Re-pull image and redeploy, then confirm. Portainer pulls the newest image for the tag you pinned (for example the latest 1.x of Uptime Kuma) and recreates only what changed. When you are ready for a new major version, change the tag in the editor, read the app’s release notes first and keep a backup of its volume.

For a standalone container that is not part of a stack, open it and click Recreate, enable Re-pull image and confirm. For containers inside a stack, always prefer the stack editor, so the compose file stays the source of truth. Old image versions remain on disk after updates. Clean them under Images: filter by unused images, select them and click Remove.

Portainer should not update itself from its own interface, because it would have to stop the container that runs the page you are using. First, take a configuration backup: open Settings (grouped under Administration in recent versions) and use the backup section to download a backup file, optionally protected by a password. Then update from the terminal:

cd /opt/portainer
sudo docker compose pull
sudo docker compose up -d
sudo docker image prune -f

pull downloads the newest image for the lts tag, up -d recreates the container only if the image changed, and prune deletes dangling old images. Refresh the browser and log in again.

How much does it cost in resources?

A common worry is that a web dashboard eats the RAM your apps need. In practice Portainer is light, as the chart shows.

Approximate idle RAM usage on a fresh VPSDocker Engine~90 MBPortainer CE~45 MBCaddy (optional)~25 MBUptime Kuma stack~110 MB0306090120RAM in MB (approximate, idle)ComponentTotal around 270 MB: a 1 GB VPS still has plenty of room for your apps
Figure 3: approximate idle memory per component after following Steps 1 to 5. Real values vary with versions and load.

Portainer only does real work while you have the page open, so CPU usage at idle is close to zero. Disk usage is a few hundred MB for the image plus a small database in the volume.

Troubleshooting and common mistakes

“Your Portainer instance timed out for security purposes”

You did not create the admin account within 5 minutes of the first start. Run sudo docker restart portainer and complete Step 3 right away.

The browser cannot connect to localhost:9443

Check that the SSH window with the tunnel is still open and that you typed -L 9443:127.0.0.1:9443 on your own computer, not on the VPS. On the server, sudo docker compose ps in /opt/portainer should show the container as running, and sudo ss -tlnp | grep 9443 should show the listener.

“Port is already allocated” when deploying a stack

Another container already publishes that port on the host. Change the host side of the mapping, for example "127.0.0.1:3002:3001", or stop the other container.

Permission denied on /var/run/docker.sock

You ran a docker command without sudo and you are not in the docker group. Prefix the command with sudo.

Caddy does not get a certificate

Usually the DNS record does not point to the VPS yet, or port 80 is blocked somewhere. Check with sudo docker compose logs caddy. Let’s Encrypt limits repeated failures, so fix DNS first and then restart Caddy, rather than retrying in a loop.

A stack shows “Limited” control

It was created outside Portainer, with docker compose in a terminal. You can still view logs and restart containers, but edit the compose file where it lives on disk.

I forgot the admin password

Portainer offers an official helper image. From /opt/portainer run sudo docker compose stop portainer, then sudo docker run --rm -v portainer_portainer_data:/data portainer/helper-reset-password, note the new password it prints, and start Portainer again with sudo docker compose start portainer.

Docker will not start on an LXC VPS

In an unprivileged LXC container, Docker needs nesting enabled by the host. Error messages about overlay filesystems or permission denied when creating containers are the typical sign. Ask support to enable nesting, or use a KVM VPS.

Security notes

  • Portainer access equals root access. Through the Docker socket, a Portainer admin can start a container that mounts the host filesystem. Treat the Portainer password like the root password.
  • Never publish 9443, 9000 or 8000 publicly. Remember that Docker-published ports bypass UFW. Keep the 127.0.0.1: prefix, use the SSH tunnel or Caddy, and do not enable the Edge agent port 8000 unless you really use Edge agents.
  • Restrict the HTTPS address if you can. If you use Step 4 from a fixed IP, add an allowlist in the Caddyfile, inside the site block, before reverse_proxy: @outside not remote_ip 203.0.113.10 and respond @outside 403. Community Edition does not offer built-in two-factor login at the time of writing, so network restrictions are a valuable extra layer.
  • Shorten the session lifetime under Settings, Authentication, so a forgotten browser tab does not stay logged in for hours.
  • Give teammates standard user accounts, not admin, and assign them to teams with access only to the stacks they need.
  • Keep secrets out of YAML. Use the stack environment variables, and never paste production passwords into compose files shared in public repositories.
  • Back up two things: Portainer’s configuration (the backup file from Settings) and the volumes of your apps. Portainer’s backup does not include your application data.
  • Update regularly: Portainer via Step 7, the host via sudo apt update && sudo apt upgrade. On Ubuntu, a kernel update creates the file /var/run/reboot-required when a reboot is needed; Debian does not create that file by default, so after kernel upgrades plan a reboot yourself. Containers with restart: unless-stopped come back on their own.

So, is Portainer worth it?

If you run more than two or three containers on a VPS, yes. The Portainer Docker install takes about twenty minutes, costs around 45 MB of RAM, and turns the daily chores (reading logs, restarting a stuck service, updating an image, deploying a new app from a compose file) into a few clicks, without hiding what Docker does underneath. Keep it off the public internet with an SSH tunnel or a locked-down Caddy proxy, and it is as safe as your SSH access.

You can try everything in this guide on a VPS from our own data center in Romania, ready in minutes. If you would rather have someone else install Docker, harden the server and keep Portainer and your stacks updated, our server administration team can take care of it for you.

FAQ

Is Portainer free?

Portainer Community Edition is free and open source, and it covers everything in this tutorial. The Business Edition adds enterprise features and has its own licensing, including a free tier for a small number of nodes.

Does Portainer replace Docker Compose?

No. Portainer uses Compose under the hood when it deploys a stack. Your stacks are ordinary compose files, so you can always take them back to the command line.

If I remove Portainer, do my containers stop?

No. Containers are managed by the Docker engine, not by Portainer. Removing the Portainer container only removes the dashboard. Stack files created in Portainer live in its volume, so keep that volume (or a backup) if you might reinstall.

Can I manage several servers from one Portainer?

Yes. Install the Portainer Agent on the other servers and add them as environments. The agent listens on port 9001, which you should only open to the IP of your Portainer server or over a private network, never to everyone.

Does Portainer work on an LXC VPS?

Portainer works wherever Docker works. On an unprivileged LXC container Docker needs nesting enabled; on a KVM VPS everything works out of the box.

Trusted By & Member Of

We are proud members of leading internet infrastructure organizations.

RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX