{"id":1067,"date":"2026-11-12T13:00:00","date_gmt":"2026-11-12T13:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1067"},"modified":"2026-11-12T13:00:00","modified_gmt":"2026-11-12T13:00:00","slug":"install-docker-portainer-ubuntu-26-04","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/12\/install-docker-portainer-ubuntu-26-04\/","title":{"rendered":"How to Install Docker &#038; Portainer on Ubuntu 26.04"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">This guide shows you how to install Docker Portainer on Ubuntu 26.04 and reach Portainer CE through a private HTTPS dashboard in about ten minutes. You will finish with Docker Engine, Compose v2, persistent Portainer data, and no publicly exposed management port.<\/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 Ubuntu 26.04 (Resolute) 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 normal Linux account with working <code>sudo<\/code> privileges.<\/li>\n\n\n<li>Outbound HTTPS access to Ubuntu&#8217;s repositories, Docker&#8217;s package repository, and Docker Hub.<\/li>\n\n\n<li>An SSH client on your workstation for the local tunnel. A domain is optional because this private setup uses <code>127.0.0.1<\/code>; a public reverse proxy needs a domain and trusted TLS certificate later.<\/li>\n\n\n<li>A <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> if you need a root-access Ubuntu server for Docker workloads.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The commands below use <code>sudo<\/code>. If you are 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 Ubuntu 26.04 and update the host<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Checking the release and architecture first prevents you from adding a repository for the wrong Ubuntu version.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>grep -E '^(PRETTY_NAME|VERSION_ID|VERSION_CODENAME|UBUNTU_CODENAME)=' \/etc\/os-release\ndpkg --print-architecture\nsudo apt update\nsudo apt upgrade -y<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The release should identify Resolute and the architecture should be <code>amd64<\/code> or another Docker-supported 64-bit architecture:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PRETTY_NAME=\"Ubuntu 26.04 LTS\"\nVERSION_ID=\"26.04\"\nVERSION_CODENAME=resolute\namd64<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the first command reports a different release, stop here. Mixing Docker packages or Ubuntu repositories between releases can leave APT unable to resolve dependencies.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Install Docker Engine from the official repository<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Docker repository provides Docker Engine, containerd, Buildx, and the Compose plugin as one supported package set for Ubuntu 26.04.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Remove distribution packages only if they are present; they can conflict with Docker&#8217;s packages. This does not remove existing images, containers, or volumes under <code>\/var\/lib\/docker\/<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc\nsudo apt install -y ca-certificates curl\nsudo install -m 0755 -d \/etc\/apt\/keyrings\nsudo curl -fsSL https:\/\/download.docker.com\/linux\/ubuntu\/gpg -o \/etc\/apt\/keyrings\/docker.asc\nsudo chmod a+r \/etc\/apt\/keyrings\/docker.asc\nsudo tee \/etc\/apt\/sources.list.d\/docker.sources &gt; \/dev\/null &lt;&lt;EOF\nTypes: deb\nURIs: https:\/\/download.docker.com\/linux\/ubuntu\nSuites: $(. \/etc\/os-release &amp;&amp; echo \"${UBUNTU_CODENAME:-$VERSION_CODENAME}\")\nComponents: stable\nArchitectures: $(dpkg --print-architecture)\nSigned-By: \/etc\/apt\/keyrings\/docker.asc\nEOF\nsudo apt update\nsudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\nsudo systemctl enable --now docker<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The final command starts Docker and enables it for future boots. Confirm the daemon, Engine, and Compose plugin before deploying Portainer:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl is-active 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 vary as Docker releases new packages, but it should look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>active\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 small test image to validate registry access and the container runtime:<\/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\">Near the end of the output, <code>Hello from Docker!<\/code> confirms that the daemon pulled and ran a container successfully. The <code>--rm<\/code> flag deletes the stopped test container.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s <a href=\"https:\/\/docs.docker.com\/engine\/install\/ubuntu\/\" target=\"_blank\" rel=\"noopener\">official Ubuntu installation guide<\/a> documents this repository format and the supported 26.04 release.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Allow one trusted account to run Docker<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Docker&#8217;s Unix socket is root-owned by default, so adding your administration account to the <code>docker<\/code> group avoids repeated <code>sudo<\/code> prompts after you reconnect.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run these commands only as the regular sudo-enabled SSH user who will manage containers. Skip this step if you deliberately administer the server as <code>root<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo groupadd --force docker\nsudo usermod -aG docker \"$USER\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Log out of SSH completely and reconnect, then verify the new group membership:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>id -nG\ndocker info --format 'Server {{.ServerVersion}}'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The group list should contain <code>docker<\/code>, and <code>docker info<\/code> should print the Engine version without <code>sudo<\/code>. Membership grants root-equivalent control over the host, so include only trusted administrator accounts.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Deploy Portainer CE with persistent storage<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The named volume keeps Portainer users, settings, and environment data when you recreate or upgrade the container.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The following deployment uses Portainer&#8217;s LTS image, binds HTTPS to the VPS loopback address, and omits port 8000 because this single-host setup does not use Edge Agents. If you skipped Step 3, prefix these Docker commands with <code>sudo<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker volume create portainer_data\ndocker run -d \\\n  --name portainer \\\n  --restart=always \\\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 should print a long container ID. Check that Portainer is running and that its port is bound only to loopback:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>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 first HTTPS connection. Its <a href=\"https:\/\/docs.portainer.io\/start\/install-ce\/server\/docker\/linux\" target=\"_blank\" rel=\"noopener\">official Docker installation guide<\/a> covers the LTS image, the Docker socket, and the persistent data volume.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Open Portainer through an SSH tunnel<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The loopback binding keeps port 9443 off the public interface; the SSH tunnel lets your workstation reach it without adding a firewall exception.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run this command on your local computer, not inside the VPS shell. Replace the uppercase placeholders, then leave this terminal open:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -N -L 9443:127.0.0.1:9443 YOUR_LINUX_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. Accept the browser warning for the self-signed certificate, create a strong unique administrator password, and finish the initial Portainer setup. If port 9443 is already in use on your workstation, use <code>19443:127.0.0.1:9443<\/code> in the SSH command and browse to <code>https:\/\/127.0.0.1:19443<\/code> instead.<\/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 is healthy, its restart policy is set, and the private HTTPS listener answers locally on the VPS:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl is-active docker\nsystemctl is-enabled docker\ndocker inspect --format '{{.HostConfig.RestartPolicy.Name}}' portainer\ncurl --insecure --head --max-time 10 https:\/\/127.0.0.1:9443<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>active\nenabled\nalways\nHTTP\/1.1 200 OK<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Reconnect the SSH tunnel if necessary and reload <code>https:\/\/127.0.0.1:9443<\/code>. The Portainer dashboard should show the local Docker environment and the administrator account you just created.<\/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\">Ubuntu 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 Ubuntu VPS for a private Docker and Portainer setup.<\/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>Package 'docker-ce' has no installation candidate<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">APT is usually reading the wrong suite or the Docker source file was not created correctly. Check that Ubuntu reports <code>resolute<\/code>, then inspect the source and refresh the package list:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>grep -E '^(Suites|URIs|Components|Architectures|Signed-By):' \/etc\/apt\/sources.list.d\/docker.sources\nsudo apt update\napt-cache policy docker-ce<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The source should use <code>https:\/\/download.docker.com\/linux\/ubuntu<\/code>, <code>resolute<\/code>, and <code>stable<\/code>. Do not substitute <code>noble<\/code> or install packages from a different Ubuntu release.<\/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\">Your current SSH session was opened before the group change, or you added a different account. Log out completely, reconnect as the account you modified, and confirm <code>docker<\/code> appears in <code>id -nG<\/code>. Use <code>sudo docker info<\/code> as a temporary diagnostic, not as a replacement for checking the group membership.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The browser cannot connect to <code>127.0.0.1:9443<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The dashboard is intentionally private, so visiting <code>https:\/\/SERVER_IP:9443<\/code> directly will not work. Start the tunnel from your workstation and keep it running:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -N -L 9443:127.0.0.1:9443 YOUR_LINUX_USER@SERVER_IP<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On the VPS, <code>docker ps<\/code> should show <code>127.0.0.1:9443-&gt;9443\/tcp<\/code>. If it does not, inspect the startup error with <code>docker logs portainer<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Portainer starts with a fresh setup screen after recreation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The container is using a different or missing data volume. Confirm that <code>portainer_data<\/code> exists and is mounted at <code>\/data<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker volume inspect portainer_data\ndocker inspect --format '{{range .Mounts}}{{println .Name .Destination}}{{end}}' portainer<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do not delete the volume during troubleshooting. It contains Portainer&#8217;s database and removing it intentionally resets the installation.<\/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 bound to <code>127.0.0.1<\/code> and administer it through SSH. If a team needs public access, place it behind a reverse proxy with a trusted certificate, authentication controls, and a restricted source-IP firewall rule.<\/li>\n\n\n<li>Do not publish Docker&#8217;s unauthenticated TCP API on port 2375. Docker warns that published container ports can bypass ordinary UFW expectations, so review the packet-filtering guidance before exposing application ports.<\/li>\n\n\n<li>Treat both the Docker socket mount and <code>docker<\/code> group membership as root-equivalent access. Remove old administrator accounts and protect their SSH keys.<\/li>\n\n\n<li>Back up the <code>portainer_data<\/code> volume before changing the Portainer image or host storage layout.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need to open port 9443 in UFW?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. This guide binds Portainer to the VPS loopback interface and reaches it through SSH, so there is no public 9443 listener to allow. If you later bind the service to a public address, restrict the source IP and use a trusted TLS certificate before exposing it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why is port 8000 not included in the Docker command?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Port 8000 is Portainer&#8217;s optional Edge Agent tunnel. A Portainer server managing its local Docker Engine through <code>\/var\/run\/docker.sock<\/code> does not need that port, so leaving it unpublished reduces the attack surface.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use a domain name instead of an SSH tunnel?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes, but the safer public design is a reverse proxy in front of Portainer with HTTPS, access controls, and a narrow firewall rule. Point the proxy at the loopback listener and do not expose Docker&#8217;s socket or an unauthenticated management endpoint.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Where does Portainer store its configuration?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>portainer_data<\/code> Docker volume stores Portainer&#8217;s database, users, settings, and environment configuration. Keeping that volume mounted at <code>\/data<\/code> allows you to replace the container without losing the dashboard state.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Do I need to open port 9443 in UFW?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"No. This guide binds Portainer to the VPS loopback interface and reaches it through SSH, so there is no public 9443 listener to allow. If you later bind the service to a public address, restrict the source IP and use a trusted TLS certificate before exposing it.\"}}, {\"@type\": \"Question\", \"name\": \"Why is port 8000 not included in the Docker command?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Port 8000 is Portainer's optional Edge Agent tunnel. A Portainer server managing its local Docker Engine through `\/var\/run\/docker.sock` does not need that port, so leaving it unpublished reduces the attack surface.\"}}, {\"@type\": \"Question\", \"name\": \"Can I use a domain name instead of an SSH tunnel?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"Yes, but the safer public design is a reverse proxy in front of Portainer with HTTPS, access controls, and a narrow firewall rule. Point the proxy at the loopback listener and do not expose Docker's socket or an unauthenticated management endpoint.\"}}, {\"@type\": \"Question\", \"name\": \"Where does Portainer store its configuration?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"The `portainer_data` Docker volume stores Portainer's database, users, settings, and environment configuration. Keeping that volume mounted at `\/data` allows you to replace the container without losing the dashboard state.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"install docker portainer on ubuntu 26.04 with Docker Engine, a private HTTPS dashboard, persistent storage, and copy-paste verification steps for a fresh VPS.","protected":false},"author":1,"featured_media":1068,"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":[119,118,137,104],"class_list":["post-1067","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-containers","tag-docker","tag-portainer","tag-ubuntu-26-04","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1067","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=1067"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1067\/revisions"}],"predecessor-version":[{"id":4001,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1067\/revisions\/4001"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1068"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1067"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1067"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1067"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}