Install Portainer on a VPS: Manage Every Docker Container from Your Browser
The problem: Docker that only exists in a terminal
Once the container count on a VPS grows past a handful, managing everything from the terminal starts to feel cramped. One service goes down and you need its exact container name, then its logs, then a check that its restart policy is right. Add a new stack and you open the Compose file, type the command, then verify every service actually came up. None of that is wrong — the terminal does all of it. The friction shows up when you need the whole picture at a glance: which containers are running, which ones keep restarting, how much resource each uses, and which volume belongs to which service.
Portainer CE fills that gap with a web dashboard. Portainer itself runs as a container, talks to the Docker daemon through the socket, and shows containers, images, volumes, networks, and stacks in an interface you can click through. You can still drop into the terminal whenever you like, but routine jobs — reading logs, restarting a container, deploying a stack — become much faster to scan.
One thing needs to be clear from the start: Portainer holds full control over Docker on that host. Anyone who can log in can launch new containers, including privileged ones. That is why this install is deliberately conservative: the UI port is exposed only as far as it must be, the admin account is strong, and wide-open public access is never presented as an acceptable default.
Prerequisites
Three things should be in place before you start:
- A VPS with Docker and Docker Compose installed and running. Check with
docker --versionanddocker ps. If Docker is missing, install it from Docker's official repository for your distribution first, then come back. - SSH access to the VPS with sudo privileges.
- Port 9443 free. Check with
sudo ss -tlnp | grep 9443. No output means the port is clear.
Portainer CE is free and covers a single host or a few small hosts comfortably. The paid Business edition adds finer role-based access control, but for a homelab or a single VPS, CE is enough.
Step 1: Create the Portainer data volume
Portainer stores the admin account, environment settings, and stack history inside the container at /data. If that directory is not persisted, deleting the container deletes all of that configuration. So the first step is a Docker volume named portainer_data:
docker volume create portainer_dataExpected output: a single line with the volume name itself.
portainer_dataConfirm it with:
docker volume inspect portainer_dataThe output is a JSON object showing the volume name and its mountpoint on the host. You do not need to memorize the mountpoint; what matters is that this volume gets reused every time the Portainer container is recreated, including during upgrades.
The common failure here is running the command as a user who is not in the docker group, so Docker refuses the daemon connection with a permission error. Fix it by using sudo, or by adding the user with sudo usermod -aG docker $USER and logging back in before continuing.
Step 2: Run Portainer CE
The official single-host Docker install uses the portainer/portainer-ce image with the lts (Long Term Support) tag. That tag is chosen because it tracks the stable line, not every feature release:
docker run -d \
-p 9443:9443 \
--name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:ltsWhat each flag does, so nothing here is a blind paste:
-druns the container in the background.-p 9443:9443publishes the Portainer UI and API over HTTPS on port 9443. Portainer generates a self-signed certificate on first start, so the browser will show a certificate warning. That is normal at this stage and revisited in the hardening step.--name portainergives it a memorable name for laterdocker logsanddocker restartcommands.--restart=alwaysbrings Portainer back after a VPS reboot.-v /var/run/docker.sock:/var/run/docker.sockconnects Portainer to the host Docker daemon. This mount is intentionally read-write: Portainer needs to create and manage containers, and a read-only mount would break that.-v portainer_data:/dataattaches the volume from step 1.portainer/portainer-ce:ltsis the official Portainer CE image.
Two other ports are deliberately left unpublished. Port 8000 is the Edge Agent tunnel, only relevant when you manage remote environments through agents. Port 9000 is the legacy HTTP port; on modern Portainer, HTTP is disabled by default and normal access goes over HTTPS on 9443. Publishing either without a reason only widens the attack surface.
Wait a few seconds, then verify:
docker ps --filter name=portainerExpected output looks roughly like this: status Up, and the PORTS column showing 9443->9443:
CONTAINER ID IMAGE STATUS PORTS NAMES
a1b2c3d4e5f6 portainer/portainer-ce:lts Up 8 seconds 0.0.0.0:9443->9443/tcp portainerIf the status is Exited, do not just rerun the command. Look at the cause first:
docker logs portainerThe two usual culprits are port 9443 already taken by another container, or a problem with the portainer_data volume. The log normally names either one plainly. Fix the cause, remove the failed container with docker rm portainer, then run the docker run command again.
Step 3: Create the admin account and connect the local environment
Open this in your browser:
https://YOUR-VPS-IP:9443Replace YOUR-VPS-IP with the VPS public IP. The browser will warn about the self-signed certificate. At this early stage you can proceed past it (usually via "Advanced" then "Proceed"), provided you are genuinely opening your own VPS IP and not a link handed to you by someone else.
On the first screen, Portainer asks you to create an admin account. Choose a long, unique password and store it in a password manager. This admin-creation window is time-limited: leave it too long and Portainer locks account creation for security, forcing a container restart to try again. So create the account right after the container is up.
After logging in, pick the local environment. On a single-host install, Portainer usually detects local Docker right away and lists it as the local environment. Click it and you land on the dashboard: the container list, CPU and memory usage, and the status of each service.
If the local environment does not appear, check two things: whether the socket mount in the docker run command is exactly right (/var/run/docker.sock on both sides), and whether the Portainer container is Up. Without a correct socket, Portainer has nothing to manage.
Step 4: Read logs and manage containers from the UI
From the Containers menu, click any container name to open its details. Three views get the most use:
- Logs shows the container output live — the replacement for
docker logs -f container-namein the terminal, with refresh and search. When a service crashes, this is the first place worth opening. - Inspect shows the full container configuration, from image and environment variables to mounts and networks.
- Console opens a shell inside the running container, handy for checking config files without SSH-ing to the host and running
docker execby hand.
To stop, restart, or remove a container, tick its checkbox and use the action buttons above the list. Portainer's status column makes trouble visible fast: a container stuck in a restart loop usually shows up immediately through its status and a suspiciously short uptime. That is a cue to open the logs, not to hammer restart.
One habit is worth keeping: before removing a container from the UI, check whether its data lives in a named volume or a bind mount. Data that only exists in the container's writable layer disappears with it. The working rule is the same as in step 1 — stateful services always get a named volume.
Step 5: Deploy a stack from the UI
A stack in Portainer is a Docker Compose file managed through the UI. It is the tidiest way to add a new service without going back to the terminal each time.
Go to Stacks, click "Add stack", give it a name, and paste the Compose file into the editor. Here is a small test stack using the official nginx image, just to prove the flow works:
services:
web-uji:
image: nginx:alpine
container_name: web-uji
restart: unless-stopped
ports:
- "8080:80"Click "Deploy the stack". Expected result: Portainer creates the web-uji container and the stack list shows it as running. You can also verify from the terminal:
docker ps --filter name=web-ujiAnd from a browser, open http://YOUR-VPS-IP:8080. The Nginx welcome page means the stack works. Once you are done testing, delete this test stack from the Stacks menu so it does not linger: Portainer removes its containers and cleans up the stack's network.
For production stacks, two patterns are common. The first is pasting Compose straight into the Portainer editor, as in the example. The second is pointing Portainer at a Git repository holding the Compose file, so stack changes live in version control. The second pattern is tidier for stacks that change often; the first is fine for services rarely touched.
Common stack-deploy failures are YAML indentation mistakes or a service name colliding with an existing container. Portainer shows the Docker Compose error on the same screen; read the line it points to, fix it in the editor, and deploy again. Do not rename a volume already used by another stack without thinking, or a service may end up reading the wrong data.
Step 6: Lock down access — the step you must not skip
This is what separates a tidy install from one that invites trouble. Remember, Portainer controls Docker completely. Leaving https://YOUR-VPS-IP:9443 open to the whole internet and relying on the admin password alone is a risk you do not need to take, however strong that password is.
Pick one of these three patterns, depending on your situation:
Pattern A — Firewall it to your IP only. If your home or office IP is fairly stable, this is the simplest option. With UFW:
sudo ufw allow from YOUR-IP to any port 9443 proto tcp
sudo ufw enableReplace YOUR-IP with the public IP you connect from. The downside is that a changing IP forces you to update the rule. The upside is no extra component.
Pattern B — Do not publish the port at all; use an SSH tunnel. Stop the container and rerun it with -p 127.0.0.1:9443:9443 in place of -p 9443:9443. Then from your own machine:
ssh -L 9443:localhost:9443 user@YOUR-VPS-IPWhile that SSH session lives, open https://localhost:9443 in your local browser. Portainer is unreachable from the internet entirely — only through the tunnel.
Pattern C — Put it behind a reverse proxy with a valid certificate. This fits best when several people need access. Run Nginx as a reverse proxy, issue a Let's Encrypt certificate for a dedicated subdomain (for example portainer.your-domain.com), and do not publish port 9443 publicly at all — the proxy talks to Portainer over the internal network. A valid certificate removes the self-signed warning, and you can add another authentication layer in front of the proxy.
Whichever pattern you pick, three rules still apply: never use a weak or reused admin password, never hand the admin account to someone who only needs to view status, and update Portainer regularly. Updating is simple precisely because the data lives in the volume:
docker stop portainer
docker rm portainer
docker pull portainer/portainer-ce:ltsThen run the docker run command from step 2 again with the same flags. The portainer_data volume keeps your accounts and stacks intact.
The most common security slip is leaving port 9443 open while postponing admin account creation. During that window, anyone who finds the port could create the admin account themselves. So the order cannot be reversed: start the container, create the admin account immediately, then sort out long-term access.
Summary table
| Part | Value / command | Note |
|---|---|---|
| Image | portainer/portainer-ce:lts | Portainer CE Long Term Support line |
| Data volume | portainer_data:/data | Stores accounts, settings, and stacks |
| Docker socket | /var/run/docker.sock:/var/run/docker.sock | Full control over the host Docker — treat like root access |
| UI port | 9443 (HTTPS) | Self-signed certificate on first start |
| Ports left closed | 8000, 9000 | Edge Agent tunnel and legacy HTTP; open only if genuinely needed |
| Restart | --restart=always | Portainer returns after reboot |
| Admin account | Create immediately after the container starts | The creation window can lock if left idle |
| Update | stop, rm, pull, run again | Data survives because it lives in the volume |
Final checklist
- The
portainer_datavolume exists and shows up indocker volume inspect. - The portainer container is Up and port 9443 is listening.
- The admin account uses a long, unique password stored in a password manager.
- The local environment is visible and the container list renders on the dashboard.
- One test stack deployed and removed again without leftovers.
- Access to port 9443 is restricted: IP-only firewall, SSH tunnel, or reverse proxy — not open to the whole internet.
- The Portainer update routine is noted, and the commands above are understood.
Once every box is ticked, your VPS has a single place to see and steer its whole container fleet, without memorizing container names in the terminal.
Which service will you move under Portainer first — a stack that is already running, or a new service you have been putting off because wrangling its Compose file from the terminal felt like a chore? Tell us in the comments, and mention which lockdown pattern you picked: firewall, SSH tunnel, or reverse proxy.
Baca juga:
If Portainer becomes your container control center, the guide on Docker Compose for self-hosting from scratch helps tidy up the Compose files you will later deploy as stacks, while the walkthrough on one VPS, many services with an Nginx reverse proxy covers the access-hardening pattern from step 6.


Komentar
Posting Komentar