{"id":1081,"date":"2026-11-14T09:00:00","date_gmt":"2026-11-14T09:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1081"},"modified":"2026-11-14T09:00:00","modified_gmt":"2026-11-14T09:00:00","slug":"install-docker-portainer-almalinux-8","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/14\/install-docker-portainer-almalinux-8\/","title":{"rendered":"How to Install Docker Portainer on AlmaLinux 8"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install Docker Portainer on AlmaLinux 8 and manage your containers through a private HTTPS dashboard in about ten minutes. You will install Docker Engine from its official RPM repository, keep Portainer data persistent, and reach the UI through an SSH tunnel instead of exposing its management port to the internet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>A 64-bit AlmaLinux 8 VPS with at least 2 GB of RAM and 10 GB of free disk space for images and container data.<\/li>\n\n\n<li>SSH access as <code>root<\/code>, or a regular account with working <code>sudo<\/code> privileges.<\/li>\n\n\n<li>Outbound HTTPS access to Docker Hub and Docker&#8217;s RPM repository.<\/li>\n\n\n<li>An SSH client on your local computer. This guide does not require a domain because Portainer stays on the VPS loopback address.<\/li>\n\n\n<li>A <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> with root access if you need a server for this installation.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The commands use <code>sudo<\/code> for a non-root account. If you are already logged in as <code>root<\/code>, remove <code>sudo<\/code> from each command.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm AlmaLinux 8 and update the host<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Confirming the release first prevents you from applying instructions meant for Ubuntu or a different Enterprise Linux release.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>grep -E '^(PRETTY_NAME|VERSION_ID|ID_LIKE)=' \/etc\/os-release\nuname -m\nsudo dnf -y upgrade<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The release should show AlmaLinux 8 and the architecture should normally be <code>x86_64<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PRETTY_NAME=\"AlmaLinux 8.x (Cerulean Leopard)\"\nVERSION_ID=\"8.x\"\nID_LIKE=\"rhel centos fedora\"\nx86_64<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The minor version and kernel packages can change as AlmaLinux publishes updates. If DNF reports repository, DNS, or signature errors, fix those before installing Docker.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Add Docker&#8217;s repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s RPM repository supplies a compatible Engine, containerd, Buildx, and Compose plugin set. Docker documents the RHEL repository method in its <a href=\"https:\/\/docs.docker.com\/engine\/install\/rhel\/\" target=\"_blank\" rel=\"noopener\">official RHEL installation guide<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On a new VPS, remove packages that commonly conflict with Docker&#8217;s runtime, then add the repository:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf remove -y docker \\\n  docker-client \\\n  docker-client-latest \\\n  docker-common \\\n  docker-latest \\\n  docker-latest-logrotate \\\n  docker-logrotate \\\n  docker-engine \\\n  podman \\\n  runc\nsudo dnf install -y dnf-plugins-core ca-certificates\nsudo dnf config-manager --add-repo https:\/\/download.docker.com\/linux\/rhel\/docker-ce.repo\nsudo dnf makecache<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">DNF may say that some packages were not installed; that is expected on a fresh machine. Do not remove Podman or <code>runc<\/code> from an established host until you have checked which workloads depend on them. Docker data under <code>\/var\/lib\/docker\/<\/code> is not deleted by package removal.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check that the repository exposes Docker packages before continuing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dnf list docker-ce --showduplicates | tail -n 5<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see one or more <code>docker-ce<\/code> entries from Docker&#8217;s repository. The exact version changes over time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Install and start Docker Engine<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Installing the Compose plugin at the same time gives you the standard <code>docker compose<\/code> command for future stacks managed through Portainer.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\nsudo systemctl enable --now docker\nsudo docker version --format 'Client {{.Client.Version}} \/ Server {{.Server.Version}}'\nsudo docker compose version<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected output has matching client and server versions, followed by a Compose version:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Client 2x.x.y \/ Server 2x.x.y\nDocker Compose version v2.x.y<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run Docker&#8217;s disposable test container. It verifies the daemon, registry access, and container runtime without leaving a stopped test container behind:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo docker run --rm hello-world<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The final lines should include <code>Hello from Docker!<\/code> and <code>Your installation appears to be working correctly.<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Keep Portainer private on AlmaLinux 8<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Portainer&#8217;s web UI uses HTTPS on port 9443, but binding it to loopback means only local processes\u2014and an SSH tunnel you create\u2014can reach it. This also avoids adding an inbound 9443 rule to firewalld.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check the host security state before deploying the container:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>getenforce\nsudo firewall-cmd --state<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Typical output is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Enforcing\nrunning<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Leave SELinux enabled. If firewalld is active, it does not need a Portainer rule because the service will listen only on <code>127.0.0.1<\/code>; do not open 9443 publicly for this setup.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Deploy Portainer CE with persistent storage<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The named volume stores Portainer users, settings, and environment details outside the container, so recreating the container does not reset the dashboard. Portainer&#8217;s <a href=\"https:\/\/docs.portainer.io\/start\/install-ce\/server\/docker\/linux\" target=\"_blank\" rel=\"noopener\">official Docker installation guide<\/a> documents the Docker socket and data-volume arrangement used here.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On an SELinux-enabled host, <code>--privileged<\/code> is required for Portainer to manage the local Docker environment through the mounted Unix socket. The UI remains private because the host-side port is explicitly bound to loopback.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo docker volume create portainer_data\nsudo docker run -d \\\n  --name portainer \\\n  --restart=always \\\n  --privileged \\\n  --publish 127.0.0.1:9443:9443 \\\n  --volume \/var\/run\/docker.sock:\/var\/run\/docker.sock \\\n  --volume portainer_data:\/data \\\n  portainer\/portainer-ce:lts<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Docker prints a container ID. Confirm the container is running and that its published address is private:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo docker ps --filter name=portainer --format 'table {{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>NAMES      IMAGE                       STATUS       PORTS\nportainer  portainer\/portainer-ce:lts Up ...        127.0.0.1:9443-&gt;9443\/tcp<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Open the dashboard through SSH<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">An SSH local-forward carries your browser connection to the private listener without changing the VPS firewall.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run this on your local computer, not inside the VPS shell. Replace the uppercase values and keep this terminal open:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -N -L 9443:127.0.0.1:9443 YOUR_SSH_USER@SERVER_IP<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Browse to <code>https:\/\/127.0.0.1:9443<\/code>. Portainer uses a self-signed certificate on first launch, so your browser will show a certificate warning for this private connection. Continue to the page, create a strong unique administrator password, and select the local Docker environment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If port 9443 is already occupied on your computer, change only the local port:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -N -L 19443:127.0.0.1:9443 YOUR_SSH_USER@SERVER_IP<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>https:\/\/127.0.0.1:19443<\/code> in that case. The VPS-side service remains on 9443.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verify Docker and Portainer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run these checks on the VPS to confirm Docker starts at boot, the container has its restart policy, and the HTTPS endpoint answers locally:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl is-active docker\nsudo systemctl is-enabled docker\nsudo docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' portainer\nsudo ss -lntp | grep ':9443'\ncurl --insecure --head --max-time 10 https:\/\/127.0.0.1:9443<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected results include:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>active\nenabled\nalways\nLISTEN ... 127.0.0.1:9443 ...\nHTTP\/1.1 200 OK<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Portainer page should load through your SSH tunnel, and the local Docker environment should appear after setup.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Run Docker and Portainer\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">AlmaLinux VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Run Docker and Portainer<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Get a root-access AlmaLinux VPS for a private HTTPS container dashboard.<\/p>\n\t<\/div>\n\t<a class=\"vl-cta-banner__button\" href=\"\/buy-vps\" aria-label=\"View Linux VPS plans\">View Linux VPS plans<\/a>\n<\/aside>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><code>No match for argument: docker-ce<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">DNF cannot see Docker&#8217;s repository, usually because the repository helper was not installed or the repository metadata is stale. Refresh and inspect the enabled list:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf repolist --enabled | grep -i docker\nsudo dnf makecache\ndnf list docker-ce --showduplicates<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If no Docker repository appears, repeat the <code>dnf config-manager --add-repo<\/code> command in Step 2 and confirm that the VPS can resolve <code>download.docker.com<\/code> over HTTPS.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>package podman ... conflicts with ...<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">AlmaLinux may have Podman or a standalone runtime installed already. On a new VPS, rerun the removal command from Step 2; on an active host, keep the existing runtime until you have planned a migration because Docker and Podman should not be changed casually under running workloads.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Portainer exits immediately or Docker reports an SELinux denial<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Portainer needs the Docker socket, and SELinux can block the required container access. Confirm the host is enforcing SELinux, inspect the recent log, and recreate the container with the <code>--privileged<\/code> flag shown in Step 5:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>getenforce\nsudo docker logs --tail 80 portainer<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do not solve this by disabling SELinux. The documented privileged container setting keeps the host policy enabled while providing Portainer the access it needs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>ssh: connect to host ... Connection refused<\/code> or a blank browser page<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The tunnel command must run on your workstation and its local port must match the URL you open. Start it again, leave that terminal running, then check the VPS listener:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo docker ps --filter name=portainer\nsudo ss -lntp | grep ':9443'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The port listing should show <code>127.0.0.1:9443<\/code>, not a public address. If the container is stopped, read <code>sudo docker logs portainer<\/code> before recreating it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Hardening<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Keep the Portainer socket and dashboard on loopback. If you later publish it through a reverse proxy, use a real certificate, restrict firewalld to trusted sources, and protect the hostname with strong authentication.<\/li>\n\n\n<li>Treat Portainer administrator access and membership in the <code>docker<\/code> group as root-equivalent. This guide keeps Docker commands under <code>sudo<\/code> instead of broadening access for every local user.<\/li>\n\n\n<li>Back up the <code>portainer_data<\/code> volume before upgrades and keep Docker images on a disk with enough free space.<\/li>\n\n\n<li>Use VPSLake&#8217;s <a href=\"https:\/\/vpslake.com\/sshd-hardening-generator\">SSH hardening generator<\/a> to review SSH settings before relying on a tunnel for administration.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Does AlmaLinux 8 use <code>dnf<\/code> or <code>yum<\/code> for this installation?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>dnf<\/code>. AlmaLinux 8 supports the DNF command and the Docker documentation&#8217;s RHEL repository method, so there is no reason to substitute older <code>yum<\/code> instructions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why does Portainer need the Docker socket?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The socket is how Portainer sends management requests to the local Docker Engine. Mounting it gives the dashboard control over containers, images, networks, and volumes, so only trusted administrators should access Portainer.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need Portainer&#8217;s port 8000?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Port 8000 is used for Portainer Edge Agent tunnel features. A single VPS managed through its local Docker socket can leave that port unpublished.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How can I upgrade Portainer without losing the dashboard?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Keep the <code>portainer_data<\/code> volume and replace only the container with the newer <code>portainer\/portainer-ce:lts<\/code> image. Back up the volume first, and never remove it as part of a routine image upgrade because it contains the Portainer database and configuration.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Does AlmaLinux 8 use `dnf` or `yum` for this installation?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Use `dnf`. AlmaLinux 8 supports the DNF command and the Docker documentation's RHEL repository method, so there is no reason to substitute older `yum` instructions.\"}}, {\"@type\": \"Question\", \"name\": \"Why does Portainer need the Docker socket?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"The socket is how Portainer sends management requests to the local Docker Engine. Mounting it gives the dashboard control over containers, images, networks, and volumes, so only trusted administrators should access Portainer.\"}}, {\"@type\": \"Question\", \"name\": \"Do I need Portainer's port 8000?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. Port 8000 is used for Portainer Edge Agent tunnel features. A single VPS managed through its local Docker socket can leave that port unpublished.\"}}, {\"@type\": \"Question\", \"name\": \"How can I upgrade Portainer without losing the dashboard?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Keep the `portainer_data` volume and replace only the container with the newer `portainer\/portainer-ce:lts` image. Back up the volume first, and never remove it as part of a routine image upgrade because it contains the Portainer database and configuration.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install Docker Portainer on AlmaLinux 8 with DNF, SELinux-safe settings, a private HTTPS dashboard, persistent data, and checks for a fresh VPS.","protected":false},"author":1,"featured_media":1082,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"csco_singular_sidebar":"","csco_page_header_type":"","csco_page_load_nextpost":"","footnotes":""},"categories":[5,9],"tags":[94,119,118,137],"class_list":["post-1081","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-almalinux-8","tag-containers","tag-docker","tag-portainer","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1081","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/comments?post=1081"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1081\/revisions"}],"predecessor-version":[{"id":4030,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1081\/revisions\/4030"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1082"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1081"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1081"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1081"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}