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 /
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
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
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
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
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
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
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
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
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/
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
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/
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
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
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
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
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
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
- Docker Engine installation on Ubuntu
- Docker Engine 29 release notes
- Docker Engine security
- Rootless Docker
- Docker Compose documentation
- Docker firewall and packet filtering
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)