Install docker portainer on centos stream 10 and manage local containers from a private HTTPS dashboard in about ten minutes. You will use Docker’s official RPM repository, keep Portainer’s database in a named volume, and reach the interface through an SSH tunnel instead of exposing its management port publicly.
Prerequisites
- A 64-bit CentOS Stream 10 VPS with at least 2 GB of RAM and 10 GB of free disk space for images, containers, and Portainer data.
- SSH access as
root, or a regular account with workingsudoprivileges. - Outbound HTTPS access to Docker’s package repository and Docker Hub.
- An SSH client on your local computer. A domain is not needed for the private tunnel used here.
- A VPSLake Linux VPS if you need a root-access server for this Docker deployment.
These commands use sudo for a non-root account. If you are logged in as root, remove sudo from the commands.
Step 1: Confirm CentOS Stream 10 and update the VPS
Checking the release and architecture first prevents a CentOS Stream repository from being applied to the wrong operating system.
grep -E '^(PRETTY_NAME|VERSION_ID|PLATFORM_ID)=' /etc/os-release
uname -m
sudo dnf -y upgrade
The first command should identify CentOS Stream 10, and the architecture should normally be x86_64:
PRETTY_NAME="CentOS Stream 10"
VERSION_ID="10"
PLATFORM_ID="platform:el10"
x86_64
Minor package and kernel versions change as the Stream repositories move forward. If DNF reports a DNS, mirror, or signature error, fix that before adding Docker’s repository.
Step 2: Remove old Docker packages and add Docker’s repository
Docker’s official packages can conflict with older distribution-provided Docker packages, so remove only the old Docker package names before installing the supported RPM set.
On a new VPS, run:
sudo dnf remove -y docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
DNF may report that some packages are not installed; that is normal. Removing these packages does not automatically erase images, containers, volumes, or networks in /var/lib/docker/. Do not remove a runtime from a production host until you know which services depend on it.
Install the repository helper and register Docker’s CentOS repository:
sudo dnf install -y dnf-plugins-core ca-certificates
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf makecache --refresh
Docker’s CentOS installation guide lists CentOS Stream 10 as a maintained release and uses this repository URL. Confirm that DNF can see the packages before installing them:
dnf list --showduplicates docker-ce | tail -n 5
You should see docker-ce entries from docker-ce-stable. The version and .el10 package suffix can change as Docker publishes updates.
Step 3: Install and test Docker Engine
Installing the Compose plugin with Docker gives Portainer a complete Engine host and leaves the standard docker compose command available for later stacks.
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo systemctl is-active docker
sudo systemctl is-enabled docker
sudo docker version --format 'Client {{.Client.Version}} / Server {{.Server.Version}}'
sudo docker compose version
Expected output will show an active, enabled service and version values that may differ from this example:
active
enabled
Client 29.x.y / Server 29.x.y
Docker Compose version v2.x.y
Run Docker’s disposable test image. It checks daemon access, registry connectivity, and the container runtime, then removes the test container:
sudo docker run --rm hello-world
The final output should contain Hello from Docker! and Your installation appears to be working correctly.
Step 4: Check firewalld and SELinux before deploying Portainer
CentOS Stream 10 normally uses firewalld and SELinux; this setup keeps both enabled and avoids creating a public firewall exception for the dashboard.
getenforce
sudo systemctl is-active firewalld
sudo firewall-cmd --get-active-zones
Typical output is similar to:
Enforcing
active
public
interfaces: eth0
The Portainer container will bind its host port to 127.0.0.1, so do not add 9443 or 8000 to a public firewalld zone. Keep your existing SSH rule in place because the tunnel in the next step uses SSH.
Step 5: Deploy Portainer CE with private HTTPS
The named volume stores Portainer users, settings, and environment data outside the container, so replacing the container does not reset the dashboard.
Portainer’s official Docker installation instructions use port 9443 for the HTTPS interface and make port 8000 optional for Edge Agent tunnel features. The command below omits 8000, binds 9443 only to loopback, and uses --privileged because Portainer’s documentation requires it when SELinux is enabled.
sudo docker volume create portainer_data
sudo docker run -d \
--name portainer \
--restart=always \
--privileged \
--publish 127.0.0.1:9443:9443 \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume portainer_data:/data \
portainer/portainer-ce:lts
Docker prints a long container ID. Confirm that Portainer is running and that the published address is private:
sudo docker ps --filter name=portainer --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
NAMES IMAGE STATUS PORTS
portainer portainer/portainer-ce:lts Up ... 127.0.0.1:9443->9443/tcp
Portainer generates a self-signed certificate for its initial HTTPS service. That warning is expected for the local tunnel; use a real certificate and a reverse proxy only if you later make the dashboard public.
Step 6: Open Portainer through an SSH tunnel
The SSH local forward carries browser traffic to the loopback-only listener without opening Portainer in firewalld or on the VPS’s public interface.
Run this command on your local computer, not inside the VPS shell. Replace YOUR_SSH_USER with your SSH account and SERVER_IP with the VPS address, then keep this terminal open:
ssh -N -L 9443:127.0.0.1:9443 YOUR_SSH_USER@SERVER_IP
Open https://127.0.0.1:9443 in your browser. Continue past the self-signed certificate warning, create a unique administrator password of at least 12 characters, and select the automatically detected local Docker environment.
If port 9443 is already being used on your computer, change only the local port:
ssh -N -L 19443:127.0.0.1:9443 YOUR_SSH_USER@SERVER_IP
Use https://127.0.0.1:19443 in that case. The VPS still listens privately on 9443.
Verify Docker and Portainer
These checks prove that Docker starts at boot, Portainer has its restart policy, the service is not listening publicly, and the HTTPS endpoint answers on the VPS:
sudo systemctl is-active docker
sudo systemctl is-enabled docker
sudo docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' portainer
sudo ss -lntp | grep ':9443'
curl --insecure --silent --show-error --output /dev/null --write-out '%{http_code}\n' https://127.0.0.1:9443
Expected results include:
active
enabled
always
LISTEN ... 127.0.0.1:9443 ...
200
Refresh the browser through the tunnel. You should see Portainer’s setup or dashboard page, and the local Docker environment should be available.
Troubleshooting
No match for argument: docker-ce
DNF cannot see Docker’s repository, usually because the repository helper was missing, the repo file was not added, or metadata is stale. Inspect the enabled repositories and rebuild the cache:
sudo dnf repolist --enabled
sudo dnf makecache --refresh
dnf list --showduplicates docker-ce
If no docker-ce-stable entries appear, repeat the repository command in Step 2 and confirm that the VPS can resolve download.docker.com over HTTPS.
permission denied while trying to connect to the Docker daemon socket
Docker’s Unix socket is root-owned by default, and this guide deliberately keeps commands under sudo instead of granting every login user root-equivalent Docker access. Run the affected command with sudo; if you later add a trusted account to the docker group, reconnect the SSH session before testing it.
Portainer is restarting or logs an SELinux denial
Portainer needs the Docker socket, and SELinux can block the required container access unless the container is privileged. Check the host mode and recent logs:
getenforce
sudo docker logs --tail 100 portainer
sudo docker inspect --format '{{.HostConfig.Privileged}}' portainer
The last command should print true. If it prints false, remove only the Portainer container and redeploy it with the exact --privileged, loopback binding, socket mount, and portainer_data volume from Step 5; the named volume preserves the stored configuration.
bind: address already in use
Another process is already using port 9443 on the VPS, so Docker cannot create Portainer’s listener. Identify the process, or use an unused loopback port such as 19443:
sudo ss -lntp | grep ':9443'
If you choose 19443, publish 127.0.0.1:19443:9443 in the container command, forward 19443:127.0.0.1:19443 over SSH, and browse to https://127.0.0.1:19443.
Hardening
- Keep Portainer on loopback. If several administrators need access, put it behind a reverse proxy with a trusted TLS certificate, strong authentication, and a firewalld rule limited to the required HTTPS traffic.
- Treat the Docker socket, the Portainer administrator account, and membership in the
dockergroup as root-equivalent access. Limit them to trusted operators. - Back up the
portainer_datavolume before changing the image tag, and monitor free space under Docker’s data directories. - Review your SSH exposure with VPSLake’s SSH hardening generator before relying on the tunnel for administration.
FAQ
Does CentOS Stream 10 use dnf or yum for Docker?
Use dnf. CentOS Stream 10 is an Enterprise Linux 10 release, and Docker’s current CentOS instructions use DNF’s repository plugin and the CentOS RPM repository.
Why is Portainer not available at https://SERVER_IP:9443?
That is intentional: the host-side binding is 127.0.0.1:9443, which prevents direct public access. The SSH tunnel makes the service available only to the workstation that opened the tunnel.
Is port 8000 required for a single Docker VPS?
No. Port 8000 is Portainer’s optional TCP tunnel for Edge Agent features. A local Portainer Server managing the Docker Engine through /var/run/docker.sock can leave it unpublished.
Can I update Portainer without losing containers and users?
Yes, provided you keep the portainer_data volume and do not delete Docker’s application data. Replace the container during a planned upgrade, then reuse the same volume and socket mount; your Portainer database remains separate from the container image.