DEV Community

Cover image for How to Install Docker on a VPS: From a Clean Ubuntu Server to Your First Persistent Container
Aeza
Aeza

Posted on

How to Install Docker on a VPS: From a Clean Ubuntu Server to Your First Persistent Container

Getting Docker to print “Hello from Docker!” is easy. Making sure its ports, privileges, and data behave as expected months later is the real job.

A successful hello-world test tells you the Docker Engine can start a container. It doesn't tell you whether a published port is reachable from the Internet, whether your data survives a container replacement, or whether a user in the docker group effectively has root access.

This guide follows a clean Ubuntu 24.04 LTS VPS from the initial checks through installation from Docker's official repository, a test Nginx container, a Docker Compose deployment with a named volume, and a backup that we actually restore and verify.

You'll need a virtual machine, SSH access, and sudo. Ubuntu 26.04 uses the same overall installation approach, but package version strings differ. Debian and other distributions have different repository instructions; don't mix them.

1. Check Your VPS Before Installing Anything

Docker containers share the host system's kernel. A KVM VPS generally has its own guest kernel, while container-based virtualization may restrict features Docker needs. Reinstalling Docker won't fix a limitation imposed by the hosting environment.

When choosing a VPS hosting provider for Docker workloads, look beyond CPU and advertised bandwidth: check virtualization compatibility, storage capacity, available inodes, administrator access, network controls, and backup options.

Start by recording the installation session. The script utility captures commands and output—don't enter passwords or tokens into the recorded terminal session.

mkdir -m 700 docker-check
cd docker-check
script -q -a install.typescript

date -Is
cat /etc/os-release
dpkg --print-architecture
uname -r
systemd-detect-virt
free -h
df -hT /
df -i /
Enter fullscreen mode Exit fullscreen mode

Check available RAM, free disk space, and inode capacity. Inodes represent filesystem objects; a server can run out of them even when gigabytes remain free. Container images, layers, and logs can grow faster than expected.

Keep the session recording open until the end of the procedure, then exit with exit.

2. Check for Conflicting Docker Packages

For a new installation, use Docker's official Ubuntu repository so Engine, the CLI, and plugins come from a consistent source and APT validates repository signatures. Ubuntu's docker.io package is maintained separately and may use a different release schedule.

First inspect any existing packages:

dpkg-query -W -f='${binary:Package}\t${Status}\n' \
  docker.io docker-compose docker-compose-v2 docker-doc \
  docker-buildx podman-docker containerd runc
Enter fullscreen mode Exit fullscreen mode

A “package not found” result simply means that package isn't installed. If docker.io is present, simulate removal before performing it:

sudo apt -s remove docker.io
sudo apt remove docker.io
Enter fullscreen mode Exit fullscreen mode

Review other conflicts similarly. On an existing Kubernetes or production host, removing containerd could interrupt active workloads. Treat replacing packages as a separate migration, not a routine cleanup.

Removing Docker packages normally does not delete images, containers, or volumes stored under /var/lib/docker.

3. Add Docker's Official APT Repository and Verify Its Key

Install the basic prerequisites:

sudo apt update
sudo apt install ca-certificates curl gnupg
Enter fullscreen mode Exit fullscreen mode

Download the Docker signing key to your working directory first:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o docker.asc

gpg --show-keys --fingerprint docker.asc
Enter fullscreen mode Exit fullscreen mode

At the time of the source guide, the Docker Release (CE deb) key fingerprint is:

9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88

Compare all 40 hexadecimal characters against Docker's currently published official fingerprint. If it differs, stop and investigate; don't disable signature verification just to get past an error.

After verification, install the key and a modern deb822 repository definition:

sudo install -d -m 0755 /etc/apt/keyrings
sudo install -m 0644 docker.asc /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
apt-cache policy docker-ce docker-ce-cli
Enter fullscreen mode Exit fullscreen mode

For Ubuntu 24.04, confirm the candidate comes from download.docker.com, stable, and noble. Signed-By confines trust in that key to this repository; don't work around signature errors by disabling APT verification.

4. Install Docker Engine and Compose

The Russian source fixes Docker Engine at 29.9.0, current as of its reference date, October 14, 2026. Docker publishes newer patches, so check which precise build is available before following this pinned example. Preserve the version epoch (5:) and Ubuntu build suffix.

apt-cache madison docker-ce

docker_version='5:29.9.0-1~ubuntu.24.04~noble'

sudo apt install docker-ce="$docker_version" \
  docker-ce-cli="$docker_version" containerd.io \
  docker-buildx-plugin docker-compose-plugin

sudo systemctl enable --now docker.service containerd.service
sudo systemctl is-active docker
sudo systemctl is-enabled docker
sudo docker version
sudo docker compose version

dpkg-query -W docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin
Enter fullscreen mode Exit fullscreen mode

The five packages provide the Docker Engine, its command-line interface, the container runtime, image-building capabilities, and the Compose plugin. Modern Compose uses docker compose with a space, not the older standalone docker-compose command.

Look for Client and Server sections in docker version. Without a Server section, the client cannot reach the daemon. active means the service is running; enabled means it is configured to start on boot.

5. Inspect Docker's Runtime and Storage

Before running the test container, save the engine's settings and diagnostics:

sudo docker info
sudo docker info --format '{{.DriverStatus}}'
sudo docker system df
sudo journalctl -u docker -u containerd -b --no-pager -n 100
Enter fullscreen mode Exit fullscreen mode

On fresh Docker Engine 29 installations, image content is managed through containerd and its overlayfs snapshotter. Some older upgraded installations can retain the overlay2 driver. Don't switch storage configurations casually: existing images and containers may no longer appear under a different storage backend.

Containerd image content can reside in /var/lib/containerd, while Docker volumes and other daemon data are kept under /var/lib/docker. Changing Docker's data-root setting does not necessarily relocate containerd's image store. Monitor both storage locations.

Check Cgroup Driver, Cgroup Version, and Logging Driver in docker info to understand resource limits and logging behavior. If the daemon fails to start, review its systemd journal. Avoid copying another machine's entire daemon.json file without checking its options.

6. The Docker Group Is Essentially Root Access

The examples deliberately use sudo. The Docker daemon normally operates with root privileges. A user authorized to control it can mount sensitive host paths into a container and alter files on the host. Therefore, membership in the docker group is effectively root-level access.

Do not grant access to /var/run/docker.sock broadly or mount it into containers without a carefully considered reason.

If you need a genuinely non-root daemon, investigate Rootless Docker, in which the daemon and containers operate under a normal user with user namespaces.

Rootless mode has trade-offs. User-level systemd services may need loginctl enable-linger to continue running after logout. Resource controls depend on cgroup v2 and suitable delegation, and exposing host ports below 1024 requires additional configuration. Simply running an application as a non-root user inside a normal container is different from using a rootless Docker daemon.

Compose itself does not introduce a separate security boundary: it uses the permissions of the Docker endpoint to which it connects.

7. A Published Docker Port May Bypass UFW

The port mapping 8080:80 publishes host port 8080 to container port 80. Without a bind address, the host side can listen on all interfaces. A Dockerfile's EXPOSE instruction only describes a port; it does not publish it.

For a local-only test, use:

127.0.0.1:8080:80

The service is then reachable from the VPS itself and, with an SSH tunnel, from your workstation, but is not directly exposed on the VPS public interface.

Security warning: Docker manages bridge-network forwarding and NAT rules. Traffic to published container ports can bypass inbound rules managed by UFW. A UFW deny 8080 rule alone does not prove the container port is inaccessible.

When Docker uses the iptables firewall backend, administrators can add filtering rules in DOCKER-USER, evaluated before Docker's own forwarding rules. At that point destination NAT has already happened, so matching the original destination port may require conntrack.

Don't confuse Ubuntu's iptables-nft compatibility interface with Docker's separate native nftables firewall backend. Under the native nftables backend, there is no DOCKER-USER chain; use its appropriate firewall controls instead.

If your VPS is behind provider NAT, the public IP might not actually be assigned to a host interface, so binding a published port to that address may fail.

8. Test Nginx and Save Its Exact Image Digest

Use an official Nginx Alpine image as a test. The original article uses nginx:1.30.6-alpine; check that the tag is available in the registry, and use an available stable patch tag if it isn't.

sudo docker pull nginx:1.30.6-alpine

nginx_image=$(sudo docker image inspect nginx:1.30.6-alpine \
  --format '{{index .RepoDigests 0}}')

printf '%s\n' "$nginx_image"

sudo docker run -d --name vps-web-test \
  -p 127.0.0.1:8080:80 --log-driver local "$nginx_image"

curl --fail --retry 10 --retry-connrefused --retry-delay 1 \
  http://127.0.0.1:8080/

sudo docker port vps-web-test
sudo docker logs --tail 30 vps-web-test

sudo docker stop vps-web-test
sudo docker rm -v vps-web-test
Enter fullscreen mode Exit fullscreen mode

An image tag may be republished. The registry digest, represented by an immutable content reference containing sha256, identifies the image artifact pulled for that platform. Record it for reproducible deployments.

A successful curl proves the HTTP server answered; the container log should contain the request. docker rm -v removes anonymous volumes associated with that container, not its named volumes or the image itself.

Test External Access Separately

If you need to publish a service to the Internet, run a separate test container bound to the intended interface and limit access to your administrative source IP in an effective Docker-aware firewall policy or the provider's firewall.

From another computer, test:

curl --connect-timeout 5 http://YOUR_VPS_ADDRESS:8080/
Enter fullscreen mode Exit fullscreen mode

Test a permitted client and a disallowed client. The authorized request should succeed and the unauthorized request should fail. Document both observations, then remove the public test container.

9. Persist the Site Using Docker Compose

A disposable container can be deleted without concern when it holds no important information. Real application data needs a separate lifecycle. Docker named volumes survive container replacement.

Create a file named compose.yaml:

name: vps-demo

services:
  web:
    image: ${NGINX_IMAGE:?Set image with digest}
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - site:/usr/share/nginx/html:ro
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  site:
    name: vps-demo-site
Enter fullscreen mode Exit fullscreen mode

Store the image reference obtained earlier in .env, validate the configuration, then start the service:

printf 'NGINX_IMAGE=%s\n' "$nginx_image" > .env

sudo docker compose config --quiet
sudo docker compose up -d

curl --fail --retry 10 --retry-connrefused --retry-delay 1 \
  http://127.0.0.1:8080/
Enter fullscreen mode Exit fullscreen mode

On first use, Docker normally populates an empty named volume from image files at the mounted path. Thereafter the volume exists independently of the container. Recreating the service attaches the same volume; changing the Nginx image does not replace the content already stored there.

Before upgrading, record the previously used digest. Afterward test HTTP responses and inspect logs. Rolling back an image does not roll back the volume's data. That's a separate backup and restore operation.

The Compose json-file rotation limits apply to container output; application log files written elsewhere need their own retention policy.

10. Back Up the Volume—and Test Restoration

For a static site, stop writes before taking an archive. In this example, stop Nginx, archive the named volume with a temporary container, and restart it:

sudo docker compose stop web

sudo docker run --rm -v vps-demo-site:/data:ro -v "$PWD":/backup \
  "$nginx_image" tar -C /data -czf /backup/site.tar.gz .

sudo docker compose start web
Enter fullscreen mode Exit fullscreen mode

A backup is only meaningful when you can restore it. Create a temporary restoration volume:

sudo docker volume create vps-demo-site-restore

sudo docker run --rm -v vps-demo-site-restore:/data -v "$PWD":/backup:ro \
  "$nginx_image" tar -C /data -xzf /backup/site.tar.gz
Enter fullscreen mode Exit fullscreen mode

Now compare file checksums between the original and restored volumes:

sudo docker run --rm -v vps-demo-site:/a:ro -v vps-demo-site-restore:/b:ro \
  "$nginx_image" sh -c 'cd /a && find . -type f -exec sha256sum {} + | sort > /tmp/a &&
  cd /b && find . -type f -exec sha256sum {} + | sort > /tmp/b &&
  diff /tmp/a /tmp/b && echo OK'

sudo docker volume rm vps-demo-site-restore
Enter fullscreen mode Exit fullscreen mode

This procedure works as an example for static files at rest. A live database needs a consistent database-aware backup, or must be shut down cleanly; archiving its active storage volume can produce an unusable copy.

Move the archive off the VPS. A backup stored on the same failed disk is not a disaster recovery plan.

11. Don't Accidentally Delete the Volume

To remove the application's containers and Compose networking while keeping its data:

sudo docker compose down
Enter fullscreen mode Exit fullscreen mode

Be careful with the -v flag. docker compose down -v also removes the Compose project's named volumes, including the volume holding the site in this example. Never add it automatically to cleanup commands.

12. Final Checks Before Production

The Nginx test shows Docker can launch a container and answer HTTP requests. Real readiness takes more:

  • Confirm the Docker Engine and services recover after a reboot.
  • Verify which addresses and ports are actually accessible externally.
  • Test permitted and forbidden source addresses, not just a local curl.
  • Restrict Docker daemon and socket access to trusted administrators.
  • Record pinned image digests and deployment versions.
  • Keep persistent data in managed volumes or equivalent storage.
  • Monitor logs, available disk space, and inode usage.
  • Store backups externally and verify restoration.
  • Document compose.yaml, .env, session logs, and recovery instructions securely.

Finish the recorded installation session with:

exit
Enter fullscreen mode Exit fullscreen mode

If your application has database migrations, test a rollback before performing an upgrade. An older restored backup cannot recover changes made after the backup was created.

Conclusion

Installing Docker from its official repository is relatively simple. Operating Docker securely on a VPS requires attention to privileges, firewalls, data persistence, and recovery.

A Docker VPS is ready for real workloads only when it survives a restart, exposes exactly the ports you intended, and allows you to restore application data successfully—not merely when hello-world works.


Official References

The command examples follow the October 2026 reference configuration. Verify current package availability, image tags, version identifiers, and firewall behavior for your environment before using them on a production host.

Top comments (0)