{"id":1085,"date":"2026-11-14T18:00:00","date_gmt":"2026-11-14T18:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1085"},"modified":"2026-11-14T18:00:00","modified_gmt":"2026-11-14T18:00:00","slug":"install-docker-portainer-centos-stream-10","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/14\/install-docker-portainer-centos-stream-10\/","title":{"rendered":"How to Install Docker Portainer on CentOS Stream 10"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Install docker portainer on centos stream 10 and manage local containers from a private HTTPS dashboard in about ten minutes. You will use Docker&#8217;s official RPM repository, keep Portainer&#8217;s database in a named volume, and reach the interface through an SSH tunnel instead of exposing its management port publicly.<\/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 CentOS Stream 10 VPS with at least 2 GB of RAM and 10 GB of free disk space for images, containers, and Portainer 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&#8217;s package repository and Docker Hub.<\/li>\n\n\n<li>An SSH client on your local computer. A domain is not needed for the private tunnel used here.<\/li>\n\n\n<li>A <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> if you need a root-access server for this Docker deployment.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">These commands use <code>sudo<\/code> for a non-root account. If you are logged in as <code>root<\/code>, remove <code>sudo<\/code> from the commands.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Confirm CentOS Stream 10 and update the VPS<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Checking the release and architecture first prevents a CentOS Stream repository from being applied to the wrong operating system.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>grep -E '^(PRETTY_NAME|VERSION_ID|PLATFORM_ID)=' \/etc\/os-release\nuname -m\nsudo dnf -y upgrade<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first command should identify CentOS Stream 10, and the architecture should normally be <code>x86_64<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PRETTY_NAME=\"CentOS Stream 10\"\nVERSION_ID=\"10\"\nPLATFORM_ID=\"platform:el10\"\nx86_64<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;s repository.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Remove old Docker packages and add Docker&#8217;s repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On a new VPS, run:<\/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<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>\/var\/lib\/docker\/<\/code>. Do not remove a runtime from a production host until you know which services depend on it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Install the repository helper and register Docker&#8217;s CentOS repository:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf install -y dnf-plugins-core ca-certificates\nsudo dnf config-manager --add-repo https:\/\/download.docker.com\/linux\/centos\/docker-ce.repo\nsudo dnf makecache --refresh<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/engine\/install\/centos\/\" target=\"_blank\" rel=\"noopener\">CentOS installation guide<\/a> lists CentOS Stream 10 as a maintained release and uses this repository URL. Confirm that DNF can see the packages before installing them:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dnf list --showduplicates docker-ce | tail -n 5<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see <code>docker-ce<\/code> entries from <code>docker-ce-stable<\/code>. The version and <code>.el10<\/code> package suffix can change as Docker publishes updates.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Install and test Docker Engine<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Installing the Compose plugin with Docker gives Portainer a complete Engine host and leaves the standard <code>docker compose<\/code> command available for later stacks.<\/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 systemctl is-active docker\nsudo systemctl is-enabled 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 will show an active, enabled service and version values that may differ from this example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>active\nenabled\nClient 29.x.y \/ Server 29.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 image. It checks daemon access, registry connectivity, and the container runtime, then removes the test container:<\/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 output should contain <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: Check firewalld and SELinux before deploying Portainer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">CentOS Stream 10 normally uses firewalld and SELinux; this setup keeps both enabled and avoids creating a public firewall exception for the dashboard.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>getenforce\nsudo systemctl is-active firewalld\nsudo firewall-cmd --get-active-zones<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Typical output is similar to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Enforcing\nactive\npublic\n  interfaces: eth0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Portainer container will bind its host port to <code>127.0.0.1<\/code>, 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Deploy Portainer CE with private HTTPS<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The named volume stores Portainer users, settings, and environment data outside the container, so replacing the container does not reset the dashboard.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Portainer&#8217;s <a href=\"https:\/\/docs.portainer.io\/start\/install-ce\/server\/docker\/linux\" target=\"_blank\" rel=\"noopener\">official Docker installation instructions<\/a> 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 <code>--privileged<\/code> because Portainer&#8217;s documentation requires it when SELinux is enabled.<\/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 long container ID. Confirm that Portainer is running and that the 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<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Open Portainer through an SSH tunnel<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The SSH local forward carries browser traffic to the loopback-only listener without opening Portainer in firewalld or on the VPS&#8217;s public interface.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run this command on your local computer, not inside the VPS shell. Replace <code>YOUR_SSH_USER<\/code> with your SSH account and <code>SERVER_IP<\/code> with the VPS address, then 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\">Open <code>https:\/\/127.0.0.1:9443<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If port 9443 is already being used 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 still listens privately 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\">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:<\/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 --silent --show-error --output \/dev\/null --write-out '%{http_code}\\n' 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 ...\n200<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Refresh the browser through the tunnel. You should see Portainer&#8217;s setup or dashboard page, and the local Docker environment should be available.<\/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\">CentOS Stream 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 CentOS Stream VPS for a private Docker management 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 missing, the repo file was not added, or metadata is stale. Inspect the enabled repositories and rebuild the cache:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf repolist --enabled\nsudo dnf makecache --refresh\ndnf list --showduplicates docker-ce<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If no <code>docker-ce-stable<\/code> entries appear, repeat the repository 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>permission denied while trying to connect to the Docker daemon socket<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s Unix socket is root-owned by default, and this guide deliberately keeps commands under <code>sudo<\/code> instead of granting every login user root-equivalent Docker access. Run the affected command with <code>sudo<\/code>; if you later add a trusted account to the <code>docker<\/code> group, reconnect the SSH session before testing it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>Portainer<\/code> is restarting or logs 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 unless the container is privileged. Check the host mode and recent logs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>getenforce\nsudo docker logs --tail 100 portainer\nsudo docker inspect --format '{{.HostConfig.Privileged}}' portainer<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The last command should print <code>true<\/code>. If it prints <code>false<\/code>, remove only the Portainer container and redeploy it with the exact <code>--privileged<\/code>, loopback binding, socket mount, and <code>portainer_data<\/code> volume from Step 5; the named volume preserves the stored configuration.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>bind: address already in use<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Another process is already using port 9443 on the VPS, so Docker cannot create Portainer&#8217;s listener. Identify the process, or use an unused loopback port such as 19443:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ss -lntp | grep ':9443'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you choose 19443, publish <code>127.0.0.1:19443:9443<\/code> in the container command, forward <code>19443:127.0.0.1:19443<\/code> over SSH, and browse to <code>https:\/\/127.0.0.1:19443<\/code>.<\/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 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.<\/li>\n\n\n<li>Treat the Docker socket, the Portainer administrator account, and membership in the <code>docker<\/code> group as root-equivalent access. Limit them to trusted operators.<\/li>\n\n\n<li>Back up the <code>portainer_data<\/code> volume before changing the image tag, and monitor free space under Docker&#8217;s data directories.<\/li>\n\n\n<li>Review your SSH exposure with VPSLake&#8217;s <a href=\"https:\/\/vpslake.com\/sshd-hardening-generator\">SSH hardening generator<\/a> before relying on the 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 CentOS Stream 10 use <code>dnf<\/code> or <code>yum<\/code> for Docker?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>dnf<\/code>. CentOS Stream 10 is an Enterprise Linux 10 release, and Docker&#8217;s current CentOS instructions use DNF&#8217;s repository plugin and the CentOS RPM repository.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why is Portainer not available at <code>https:\/\/SERVER_IP:9443<\/code>?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">That is intentional: the host-side binding is <code>127.0.0.1:9443<\/code>, which prevents direct public access. The SSH tunnel makes the service available only to the workstation that opened the tunnel.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is port 8000 required for a single Docker VPS?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. Port 8000 is Portainer&#8217;s optional TCP tunnel for Edge Agent features. A local Portainer Server managing the Docker Engine through <code>\/var\/run\/docker.sock<\/code> can leave it unpublished.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I update Portainer without losing containers and users?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes, provided you keep the <code>portainer_data<\/code> volume and do not delete Docker&#8217;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.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Does CentOS Stream 10 use `dnf` or `yum` for Docker?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Why is Portainer not available at `https:\/\/SERVER_IP:9443`?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Is port 8000 required for a single Docker VPS?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Can I update Portainer without losing containers and users?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install docker portainer on centos stream 10 with Docker Engine, persistent Portainer data, private HTTPS access, and VPS troubleshooting on fresh servers.","protected":false},"author":1,"featured_media":1086,"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":[110,119,118,137],"class_list":["post-1085","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-centos-stream-10","tag-containers","tag-docker","tag-portainer","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1085","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=1085"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1085\/revisions"}],"predecessor-version":[{"id":4036,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1085\/revisions\/4036"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1086"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1085"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1085"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1085"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}