VPS Tutorials
Deploying the Portainer Docker Management Panel on a VPS (2026): A Complete Guide to Containers, Stacks, Logs and Secure Access
This tutorial is for users who already run Docker on a VPS but don't want to type SSH commands every day to manage containers. You'll learn how to install Portainer CE, open only the minimum ports, access the management panel over HTTPS, manage containers and Compose Stacks, view logs, handle Volumes, and understand why Portainer is convenient but cannot replace server security hardening.
Deploying the Portainer Docker Management Panel on a VPS (2026): A Complete Guide to Containers, Stacks, Logs and Secure Access
Many VPS users start with a single docker compose up -d. At first there's just one blog, one database, one reverse proxy, and the command line is manageable; but once you have a dozen containers, scheduled tasks, test environments, monitoring, backup scripts and multiple Compose projects, flipping through logs, checking containers and editing environment variables over SSH alone becomes a real chore.
Portainer's positioning is clear: it provides a web management panel for Docker and Docker Compose. It can't understand Docker for you, and it shouldn't become an unprotected management backend exposed to the public internet, but it's very well suited to a single VPS or a small number of them: checking container status, restarting services, managing Stacks, inspecting Volumes, viewing logs, and quickly confirming port mappings and image versions.
This article explains from scratch: how to install Portainer CE on a VPS, which ports to open, how to initialize the administrator account, how to use it to manage Docker Compose projects, which operations are suitable to do in the panel, and which operations should still stay in the command line and Git repositories.
If you match any of the following situations, Portainer will be very useful:
- Docker is already installed on your VPS and multiple containers run long-term.
- You frequently need to view container logs, restart services, and check environment variables and port mappings.
- You use Docker Compose but don't want to SSH into the server every time to check status.
- You want to give colleagues without an ops background a relatively safe visual entry point.
- You have multiple VPSs and need to view multiple Docker environments in one place.
If your VPS only runs one WordPress and you hardly ever touch containers, Portainer isn't necessary. It suits the stage where "containers have multiplied and command-line management is starting to get messy."
Portainer can:
- View container running status, images, networks and Volumes.
- Start, stop and restart containers.
- View real-time logs and the container console.
- Create and manage Docker Compose Stacks.
- Manage environment variables, port mappings and mounted directories.
- Connect to the local Docker or remote Docker environments.
Portainer cannot do this for you:
- Automatically fix broken Compose files.
- Replace backup, monitoring and alerting systems.
- Replace the Linux firewall, SSH security and system updates.
- Guarantee that the application itself has no security vulnerabilities.
- Manage database migrations and data recovery that you don't understand.
Simply put, Portainer is a Docker management entry point, not a VPS security solution, and not the whole of a production release process.
Before starting, it's recommended to prepare:
- An Ubuntu 22.04 / 24.04 or Debian 12 VPS.
- Docker Engine already installed.
- A regular sudo user.
- The server firewall enabled.
- Optional: a domain, for example
portainer.example.com.
Confirm Docker is available:
docker version
docker ps
If it reports insufficient permissions, you can first use:
sudo docker ps
It's not recommended to add all users to the docker group just for convenience. Adding a user to the docker group is basically equivalent to giving that user root-level capabilities, because it can mount host directories, control containers and access the Docker socket.
Portainer officially recommends using a Docker volume to store data. First create the data volume:
sudo docker volume create portainer_data
Then start Portainer CE:
sudo 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:latest
The key parameters here are:
-p 9443:9443: exposes Portainer's built-in HTTPS management port.--restart=always: automatically restores after a server reboot./var/run/docker.sock:/var/run/docker.sock: lets Portainer manage the local Docker.portainer_data:/data: stores Portainer configuration, user and environment information.
Check the container status:
sudo docker ps --filter name=portainer
View the startup logs:
sudo docker logs --tail=100 portainer
Visit in your browser:
https://你的服务器IP:9443
On first access, the browser may warn that the certificate is not issued by a public CA; this is because Portainer uses a self-signed certificate by default. If you're only doing a temporary initialization, you can continue for now; for formal use, it's recommended to handle this later with a domain and reverse proxy.
During initialization you need to do three things:
- Create the administrator account.
- Set a strong password.
- Select the local Docker environment.
Don't reuse your SSH password, email password or cloud provider password for the administrator password. Portainer can control Docker and has very high privileges; once it's compromised, an attacker can easily go on to control the host.
The simplest way is to open 9443 directly, but this is not the most robust long-term solution.
If it's only for temporary personal use, you can restrict the firewall like this:
sudo ufw allow from 你的固定IP to any port 9443 proto tcp
If you don't have a fixed IP, this is more recommended:
- Use Caddy/Nginx as a reverse proxy to Portainer.
- Access it behind Cloudflare, Tailscale, WireGuard or Zero Trust.
- At the very least, don't expose Portainer as an unprotected public internet entry point.
If you use UFW, first confirm SSH won't get locked out:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Then don't open all management ports at will.
Suppose you already have Caddy and the domain is portainer.example.com. You can bind Portainer only to a local port:
First remove the old container:
sudo docker stop portainer
sudo docker rm portainer
Restart, listening only on the local machine:
sudo docker run -d \
-p 127.0.0.1:9443:9443 \
--name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
Caddyfile example:
portainer.example.com {
reverse_proxy https://127.0.0.1:9443 {
transport http {
tls_insecure_skip_verify
}
}
}
tls_insecure_skip_verify is used here because the upstream Portainer uses a self-signed certificate. Public users access the proper HTTPS provided by Caddy, and the connection from Caddy to the local Portainer only happens on the local loopback address.
After modifying, run:
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
This way, externally you only need to access https://portainer.example.com instead of directly accessing https://IP:9443.
Portainer is great for daily observation and lightweight operations:
- Check whether a container is Running.
- Restart a stuck container.
- View the last 1000 lines of logs.
- Confirm image tags, port mappings and mounted directories.
- Temporarily enter the container console to troubleshoot issues.
- Delete stopped test containers.
But don't treat Portainer as the only release method. For production environments, this is more recommended:
- Keep Compose files in a Git repository.
- Manage important environment variables with
.envor Secrets. - Verify locally or in a test environment before releasing.
- Update through Git, CI/CD or a clear change process.
Portainer lets you "see" and "operate", but the configuration source should ideally still be traceable.
Stacks in Portainer correspond to Docker Compose projects. You can paste Compose content in the web UI, or use a Compose file from a Git repository.
A simple Compose example:
services:
uptime-kuma:
image: louislam/uptime-kuma:latest
container_name: uptime-kuma
restart: unless-stopped
ports:
- "127.0.0.1:3001:3001"
volumes:
- uptime_kuma:/app/data
volumes:
uptime_kuma:
This example has two key points:
- The port is bound to
127.0.0.1, avoiding exposing the application port directly to the public internet. - Data is placed in a Docker volume, so it won't be lost when upgrading the container.
If you already manage services with Compose on the command line, it's not recommended to hand the same project to both Portainer and the command line and keep changing it back and forth. It's best to define one primary entry point: either Git + Compose is authoritative and Portainer only observes, or the Stack is managed by Portainer.
Portainer can see Docker volumes, but it is not a backup tool.
The common mistake is: seeing a volume in Portainer and assuming the data is safe. In reality, the volume is still on this VPS's disk. If the disk fails, the volume is accidentally deleted, or the system is reinstalled, the data can still be lost.
Recommendations:
- Back up databases using the method recommended by the application, e.g. PostgreSQL with
pg_dump. - Regularly sync file-type data to object storage or another machine.
- Regularly archive important Docker volumes.
- Do recovery drills after backing up; don't just check that the backup file exists.
You can view volumes from the command line:
sudo docker volume ls
sudo docker volume inspect portainer_data
Portainer's UI is good for viewing and assisting management, but the backup strategy still needs to be designed separately.
Portainer's Logs page is good for quickly viewing container output. Common troubleshooting steps:
- First check whether the container status is Running.
- If it's Restarting, check the exit code and recent logs.
- If the application won't open, check the port mapping and reverse proxy configuration.
- If data is lost, check whether the volume mount path has changed.
- If it fails after an update, review the image tag and environment variables.
Keep the command line too:
sudo docker ps -a
sudo docker logs --tail=200 容器名
sudo docker inspect 容器名
When the Portainer page itself won't open, the command line is the last rescue entry point.
The basic process for updating Portainer CE is:
sudo docker pull portainer/portainer-ce:latest
sudo docker stop portainer
sudo docker rm portainer
sudo docker run -d \
-p 127.0.0.1:9443:9443 \
--name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
Because the data is in the portainer_data volume, deleting the container won't delete the configuration. But it's still recommended to back up before updating:
sudo docker run --rm \
-v portainer_data:/data \
-v "$PWD":/backup \
alpine tar czf /backup/portainer_data_backup.tar.gz -C /data .
If you don't like latest, you can pin a version tag to reduce the risk of unexpected upgrades. For production environments, pinning the version is more recommended, and upgrade after testing confirms it.
First confirm whether the container is running:
sudo docker ps --filter name=portainer
sudo docker logs --tail=100 portainer
Then confirm the port is listening:
sudo ss -lntp | grep 9443
If you bound it to 127.0.0.1:9443, accessing IP:9443 from the public internet won't work anyway; you need to access it through the reverse proxy domain.
Check whether the Docker socket is mounted:
sudo docker inspect portainer | grep docker.sock
The startup command must include:
-v /var/run/docker.sock:/var/run/docker.sock
Without this mount, Portainer cannot control the local Docker.
Common causes:
- Compose YAML indentation errors.
- Wrong image name or image pull failure.
- Port already in use.
- Insufficient permissions on the Volume path.
- Missing variables in
.env.
First validate the Compose file on the command line:
docker compose config
This command can help you find most YAML and variable problems.
Yes, after mounting the Docker socket, Portainer has very high privileges. Whoever can log in to Portainer can basically manage the containers on this machine, and even further affect the host through containers.
So you must:
- Use a strong password.
- Don't share the administrator account.
- Restrict access sources.
- Regularly update Portainer.
- Don't expose 9443 to internet-wide scanning.
For a single VPS, I prefer this structure:
公网用户
↓
Caddy / Nginx :443
↓
127.0.0.1:9443
↓
Portainer
↓
/var/run/docker.sock
↓
本机 Docker Engine
Application containers should also bind to local ports as much as possible, and then be exposed to the public internet by Caddy/Nginx:
app.example.com -> Caddy -> 127.0.0.1:3000
portainer.example.com -> Caddy -> 127.0.0.1:9443
This reduces the public exposure surface and concentrates all entry points on 80/443.
Portainer is not a replacement for Docker, but a supplement to the Docker management experience. For a single VPS, its most valuable aspect is: you can view containers, logs, Volumes, Stacks and running status without logging in over SSH every time.
But its privileges are high, so it can't be exposed to the public internet carelessly. The correct approach is: use a Docker volume to store Portainer data, initialize the administrator account with a strong password, restrict 9443 to the local machine or trusted sources, and then access it through Caddy/Nginx, VPN, Zero Trust, etc.
If your VPS already runs multiple Docker services, Portainer can significantly reduce daily maintenance costs; if you only have one simple container, it isn't necessary, but as a visual tool for learning Docker status, networking and Volumes, it's well worth installing once.