Install docker portainer on almalinux 9 and manage Docker from a private HTTPS dashboard in about ten minutes. This setup uses Docker’s RPM repository, keeps Portainer data in a named volume, and avoids exposing the management UI directly to the internet.
Prerequisites
- A 64-bit AlmaLinux 9 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 normal account with workingsudoprivileges. - Outbound HTTPS access to Docker’s package repository and Docker Hub.
- An SSH client on your workstation. A domain is not required because the dashboard will be reached through an SSH tunnel.
- A VPSLake Linux VPS if you need a root-access AlmaLinux server for this Docker setup.
These commands use sudo for a regular SSH account. If you are logged in as root, remove sudo from the commands.
Step 1: Confirm AlmaLinux 9 and update the host
Checking the release and CPU architecture first prevents a repository mismatch before Docker is installed.
grep -E '^(PRETTY_NAME|VERSION_ID|ID_LIKE)=' /etc/os-release
uname -m
sudo dnf -y upgrade
The release should identify AlmaLinux 9, normally with an x86_64 architecture:
PRETTY_NAME="AlmaLinux 9.x (Emerald Puma)"
VERSION_ID="9.x"
ID_LIKE="rhel centos fedora"
x86_64
The minor version and kernel release will vary as AlmaLinux publishes updates. Resolve any DNS, mirror, or signature error before continuing.
Step 2: Add Docker’s RHEL-compatible repository
AlmaLinux follows the RHEL package layout, so Docker’s RHEL repository provides the Engine packages needed here. The official Docker RHEL installation guide is the reference for this repository method.
Install the DNF repository helper and add Docker’s stable repository:
sudo dnf install -y dnf-plugins-core ca-certificates
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf makecache
Confirm that DNF can see Docker packages before asking it to install them:
dnf list --showduplicates docker-ce | tail -n 5
You should see one or more docker-ce entries from the Docker repository. The exact version changes over time, so do not copy a version from an example into a production command.
Step 3: Install and test Docker Engine
Installing the Compose plugin with the Engine gives Portainer a complete Docker host and leaves you with the standard docker compose command for future stacks.
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker version --format 'Client {{.Client.Version}} / Server {{.Server.Version}}'
sudo docker compose version
Expected output has an active client/server pair and a Compose version, although the numbers will differ:
Client 29.x.y / Server 29.x.y
Docker Compose version v2.x.y
Run Docker’s disposable test image. It checks the daemon, registry access, and container runtime without leaving a stopped test container:
sudo docker run --rm hello-world
The final lines should contain Hello from Docker! and Your installation appears to be working correctly.
Step 4: Check firewalld and SELinux before publishing anything
Portainer will listen only on the VPS loopback address, so you do not need an inbound 9443 or 8000 firewall rule for this single-host setup.
Check the security services without disabling either one:
getenforce
sudo systemctl is-active firewalld
Typical output is:
Enforcing
active
If firewalld is active, leave its existing SSH rule in place and do not add a public Portainer rule. If your VPS uses a custom SSH port, preserve that provider or host configuration; this guide does not change it.
Step 5: Deploy Portainer CE with persistent data
The named volume holds Portainer’s database, users, and settings separately from the container. Portainer’s official Docker installation guide uses the same Docker socket and data-volume pattern.
On AlmaLinux with SELinux enforcing, --privileged is the documented requirement for Portainer’s Docker-socket integration. It keeps SELinux enabled, while the loopback binding keeps the HTTPS interface private.
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 container ID. Check both the running state and the host-side binding:
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. A browser warning during first login is expected when you use the private tunnel below.
Step 6: Open Portainer through an SSH tunnel
The tunnel forwards a local workstation port to the VPS-only listener, so the dashboard can be used without exposing 9443 or the optional Edge Agent port 8000.
Run this command on your local workstation, not in the VPS shell. Replace the uppercase values and keep the 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 strong unique administrator password, and choose the local Docker environment.
If port 9443 is already occupied on your workstation, change only the local side:
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 listener remains on 9443 and remains private.
Verify Docker and Portainer
These checks prove that Docker starts at boot, Portainer has its restart policy, the listener is bound to loopback, and the HTTPS service answers locally:
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 --head --max-time 10 https://127.0.0.1:9443
Expected results include:
active
enabled
always
LISTEN ... 127.0.0.1:9443 ...
HTTP/1.1 200 OK
Refresh the browser through the tunnel. You should see Portainer’s setup or dashboard page and the local Docker environment. For a safer SSH baseline, you can also use VPSLake’s SSH hardening generator.
Troubleshooting
No match for argument: docker-ce
DNF cannot see Docker’s repository, or its metadata is stale. Inspect the enabled repositories and refresh the cache:
sudo dnf repolist --enabled | grep -i docker
sudo dnf makecache
dnf list --showduplicates docker-ce
If no Docker repository appears, run the dnf config-manager --add-repo command from Step 2 again and confirm that the server can resolve download.docker.com over HTTPS.
package podman ... conflicts with ...
A preinstalled Podman or standalone runtime can conflict with Docker’s packages. On a new VPS, remove the conflicting package and retry; on a host already running containers, identify its workloads first rather than removing the active runtime blindly.
sudo dnf remove -y podman runc
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Removing packages does not remove the named portainer_data volume, but do not use this fix on an established production host without a migration plan.
Portainer exits or logs an SELinux denial
Portainer needs access to the Docker socket, and SELinux can block that access unless the container is privileged. Inspect the logs and recreate the container with the exact --privileged and loopback options from Step 5:
getenforce
sudo docker logs --tail 80 portainer
Do not disable SELinux to make the dashboard start. The container can be replaced without losing settings because portainer_data is separate.
The browser cannot connect to 127.0.0.1:9443
The SSH tunnel must run on your workstation and stay open. Restart it, then confirm that the VPS container is running and the service is listening:
sudo docker ps --filter name=portainer
sudo ss -lntp | grep ':9443'
The port column should show 127.0.0.1:9443->9443/tcp. Do not browse to https://SERVER_IP:9443; this setup intentionally does not publish Portainer publicly.
Hardening
- Keep Portainer bound to loopback. If you later need team access, put it behind a reverse proxy with a real certificate, authentication, and a narrowly scoped firewalld rule.
- Treat the Docker socket, Portainer administrator account, and any Docker-admin access as root-equivalent. Limit SSH and dashboard access to trusted users.
- Back up the
portainer_datavolume before changing the container image, and keep enough disk space for Docker images and volumes. - Never expose Docker’s unauthenticated TCP API on port 2375.
FAQ
Do I need to open port 9443 in firewalld?
No. Docker publishes Portainer on 127.0.0.1, not on the VPS’s public interface. The SSH tunnel carries browser traffic to that private listener, so no new inbound firewall rule is needed.
Why does this guide use the lts Portainer image tag?
The LTS tag favors a longer-lived stable release line for a server dashboard. The named volume means the Portainer database and settings remain available when you replace the container during a planned upgrade.
Can I use a domain name instead of an SSH tunnel?
Yes, but that is a different exposure model. You would need a reverse proxy, a trusted TLS certificate, access controls, and a firewalld rule limited to the intended traffic; a domain is unnecessary for the private setup here.
What is Portainer port 8000 used for?
Port 8000 is the optional tunnel service for Portainer Edge Agents. A Portainer server managing the Docker Engine on the same VPS through /var/run/docker.sock does not need that port, so this guide leaves it unpublished.