Docker on Ubuntu: Complete Installation and Usage Guide
Docker is an open-source containerization platform that packages applications and their dependencies into isolated, portable units called containers. Unlike virtual machines, containers share the host OS kernel, making them significantly lighter, faster to start, and more resource-efficient β a critical distinction for anyone running workloads on a VPS Hosting environment where compute resources directly affect cost and performance.
This guide covers the full Docker installation process on Ubuntu 20.04, 22.04, and 24.04 LTS, including post-installation hardening, Docker Compose workflows, and production-relevant command patterns that most tutorials omit.
Prerequisites and System Requirements
Before executing a single command, verify the following:
- Ubuntu version: 20.04 LTS (Focal), 22.04 LTS (Jammy), or 24.04 LTS (Noble). The `lsb_release -cs` command used during repository setup will automatically detect your codename.
- Architecture: `amd64`, `arm64`, or `armhf` are all supported by Docker's official repository.
- Kernel version: Docker requires Linux kernel 3.10 or higher. Run `uname -r` to confirm.
- User privileges: `sudo` or root access is mandatory for installation and daemon management.
- Disk space: At minimum 2 GB free on the partition hosting `/var/lib/docker`, which is where Docker stores images, containers, volumes, and build cache. On production systems, mount this directory on a dedicated partition or volume.
Critical pre-installation step: If you previously installed Docker from Ubuntu's default `apt` repository (the `docker.io` package), remove it first to avoid conflicts with the official Docker CE packages:
“`bash
sudo apt remove docker docker-engine docker.io containerd runc
“`
Step 1: Update System Packages
Bring the package index and installed packages to their latest versions before adding any new repository:
“`bash
sudo apt update
sudo apt upgrade -y
“`
This ensures that `apt`'s dependency resolver works against current package metadata and that your base system libraries are not outdated β a common source of subtle runtime errors with containerized applications.
Step 2: Install Docker Engine from the Official Repository
Ubuntu's default `apt` repositories ship a package called `docker.io`, which is maintained by Canonical and typically lags several versions behind Docker's official release. For production use, always install from Docker's own repository.
2.1 Install Transport and Verification Dependencies
“`bash
sudo apt install apt-transport-https ca-certificates curl software-properties-common gnupg lsb-release -y
“`
Why `gnupg`? Starting with Ubuntu 22.04, `gpg` is not always present by default. Including it explicitly prevents the GPG key import from failing silently.
2.2 Add Docker's Official GPG Key
“`bash
sudo install -m 0755 -d /usr/share/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg –dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg
“`
The `chmod a+r` step is frequently skipped in tutorials but is necessary on systems where `apt` runs under a restricted user context β without it, the package manager cannot read the keyring and will throw a `NO_PUBKEY` error during `apt update`.
2.3 Add the Docker Stable Repository
“`bash
echo
"deb [arch=$(dpkg –print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg]
https://download.docker.com/linux/ubuntu
$(lsb_release -cs) stable" |
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
“`
The `arch=$(dpkg –print-architecture)` substitution is essential on ARM-based servers. Hardcoding `amd64` here is a common mistake that causes silent package resolution failures on ARM instances.
2.4 Install Docker Engine, CLI, and Plugins
“`bash
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
“`
Package breakdown:
| Package | Role |
|---|
| — | — |
|---|
| `docker-ce` | Docker Engine daemon (`dockerd`) |
|---|
| `docker-ce-cli` | Client CLI (`docker` command) |
|---|
| `containerd.io` | Low-level container runtime (OCI-compliant) |
|---|
| `docker-buildx-plugin` | Extended build capabilities (multi-platform, BuildKit) |
|---|
| `docker-compose-plugin` | Compose V2 integrated as a Docker CLI plugin |
|---|
Note on `containerd.io`: This is not the same as the `containerd` package in Ubuntu's default repository. Docker's `containerd.io` is a specific, tested version of the containerd runtime. Mixing the two is a known source of daemon startup failures.
Step 3: Verify the Installation
Confirm the daemon is active and enabled to start on boot:
“`bash
sudo systemctl status docker
sudo systemctl enable docker
“`
Check the installed version:
“`bash
sudo docker –version
sudo docker info
“`
`docker info` is more informative than `–version` alone β it reveals the storage driver in use (typically `overlay2`), the cgroup driver (`systemd` vs `cgroupfs`), and the number of CPUs and memory Docker can access.
Run the canonical smoke test:
“`bash
sudo docker run hello-world
“`
A successful run prints a "Hello from Docker!" message and confirms that the Docker daemon, image pull from Docker Hub, and container execution are all functioning correctly.
Step 4: Configure Docker for Non-Root Access
By default, the Docker socket (`/var/run/docker.sock`) is owned by `root` and the `docker` group. Any user not in the `docker` group must use `sudo` for every Docker command.
“`bash
sudo usermod -aG docker $USER
“`
Apply the group membership without logging out:
“`bash
newgrp docker
“`
Verify:
“`bash
docker run hello-world
“`
Security warning: Membership in the `docker` group is effectively equivalent to passwordless `sudo`. A user in the `docker` group can trivially mount the host filesystem into a container and escape all filesystem-level access controls. On multi-tenant systems or shared servers, consider using rootless Docker instead:
“`bash
dockerd-rootless-setuptool.sh install
“`
Rootless mode runs the Docker daemon and containers under an unprivileged user namespace, dramatically reducing the attack surface. It is the recommended configuration for any environment where multiple users share the same host.
Step 5: Essential Docker Commands Reference
Image Management
“`bash
Pull a specific image version from Docker Hub
docker pull nginx:1.27-alpine
List locally cached images
docker images
Remove a specific image
docker rmi nginx:1.27-alpine
Remove all dangling (untagged) images to reclaim disk space
docker image prune
Remove all unused images (not just dangling)
docker image prune -a
“`
Production tip: Always pull versioned tags (e.g., `nginx:1.27-alpine`) rather than `latest` in any automated or production workflow. The `latest` tag is mutable β it can point to a different image after a registry push, breaking reproducibility.
Container Lifecycle
“`bash
Run a container interactively with a pseudo-TTY
docker run -it ubuntu:22.04 /bin/bash
Run a container in detached mode with port mapping and a name
docker run -d -p 8080:80 –name my-nginx nginx:1.27-alpine
List running containers
docker ps
List all containers (including stopped)
docker ps -a
Stop a running container gracefully (SIGTERM, then SIGKILL after timeout)
docker stop my-nginx
Start a stopped container
docker start my-nginx
Remove a stopped container
docker rm my-nginx
Force-remove a running container (sends SIGKILL immediately)
docker rm -f my-nginx
View real-time logs
docker logs -f my-nginx
Execute a command inside a running container
docker exec -it my-nginx /bin/sh
“`
Resource and System Inspection
“`bash
Display real-time resource usage statistics
docker stats
Inspect detailed container metadata (JSON)
docker inspect my-nginx
Display disk usage by Docker objects
docker system df
Remove all stopped containers, unused networks, dangling images, and build cache
docker system prune
“`
`docker system prune` is one of the most important maintenance commands for long-running servers. Without periodic cleanup, Docker's build cache and stopped containers can consume tens of gigabytes on an active development or CI host.
Step 6: Docker Compose β Multi-Container Application Orchestration
Docker Compose V2 (the `docker-compose-plugin` installed earlier) is invoked as `docker compose` (with a space), not the legacy `docker-compose` (with a hyphen). Both syntaxes work if you have the plugin installed, but V2 is the current standard.
6.1 Understanding the Compose File Structure
Create a project directory and a `compose.yml` file (the preferred filename in Compose V2; `docker-compose.yml` remains supported for backward compatibility):
“`bash
mkdir ~/my-web-app && cd ~/my-web-app
nano compose.yml
“`
A production-realistic example with Nginx and a backend service:
“`yaml
services:
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./html:/usr/share/nginx/html:ro
depends_on:
- app
restart: unless-stopped
app:
image: node:20-alpine
working_dir: /usr/src/app
volumes:
- ./app:/usr/src/app
command: node server.js
environment:
- NODE_ENV=production
restart: unless-stopped
“`
Key Compose directives explained:
- `restart: unless-stopped` β The container restarts automatically after a crash or system reboot, unless it was explicitly stopped by the operator. This is the correct policy for long-running services; `always` will restart even intentionally stopped containers.
- `depends_on` β Controls startup order but does not wait for the service to be *ready* (e.g., a database accepting connections). For readiness gating, use `healthcheck` combined with `condition: service_healthy`.
- `volumes` with `:ro` β Mounting configuration files as read-only prevents a compromised container process from modifying its own configuration.
6.2 Compose Workflow Commands
“`bash
Start all services in detached mode
docker compose up -d
View logs for all services
docker compose logs -f
View logs for a specific service
docker compose logs -f web
List running Compose services
docker compose ps
Scale a specific service to multiple replicas
docker compose up -d –scale app=3
Stop services without removing containers
docker compose stop
Stop and remove containers, networks, and volumes
docker compose down –volumes
Rebuild images before starting (useful after code changes)
docker compose up -d –build
“`
6.3 Verify the Running Service
“`bash
curl -I http://localhost:8080
“`
A `200 OK` response confirms Nginx is serving correctly. For services running on a remote VPS Hosting instance, replace `localhost` with your server's public IP address and ensure the relevant port is open in your firewall (`ufw allow 8080/tcp`).
Step 7: Docker Networking Fundamentals
Understanding Docker's networking model is essential for building multi-container applications that communicate securely.
Default network drivers:
| Driver | Use Case |
|---|
| — | — |
|---|
| `bridge` | Default for standalone containers; isolated network namespace on the host |
|---|
| `host` | Container shares the host's network stack; maximum performance, zero isolation |
|---|
| `none` | No network access; useful for batch processing or security-sensitive workloads |
|---|
| `overlay` | Multi-host networking for Docker Swarm clusters |
|---|
| `macvlan` | Assigns a MAC address to the container; appears as a physical device on the network |
|---|
Creating and using a custom bridge network:
“`bash
Create an isolated network
docker network create my-app-network
Run containers attached to the custom network
docker run -d –name db –network my-app-network postgres:16-alpine
docker run -d –name api –network my-app-network my-api-image
Containers on the same custom bridge network can resolve each other by name
Inside 'api', you can connect to 'db' using the hostname 'db'
“`
Custom bridge networks provide automatic DNS resolution between containers by container name. The default `bridge` network does not β this is a critical distinction that causes connection failures when developers assume containers on the default network can reach each other by name.
Step 8: Persistent Data with Docker Volumes
Containers are ephemeral by design. Any data written inside a container's filesystem is lost when the container is removed. For persistent storage, use volumes or bind mounts.
“`bash
Create a named volume
docker volume create pgdata
Use the volume with a container
docker run -d
–name postgres-db
-e POSTGRES_PASSWORD=securepassword
-v pgdata:/var/lib/postgresql/data
postgres:16-alpine
List volumes
docker volume ls
Inspect a volume (shows mount point on host)
docker volume inspect pgdata
Remove unused volumes
docker volume prune
“`
Volumes vs. bind mounts:
| Feature | Named Volume | Bind Mount |
|---|
| — | — | — |
|---|
| Managed by Docker | Yes | No |
|---|
| Host path required | No | Yes |
|---|
| Portable across hosts | Yes (with volume drivers) | No |
|---|
| Best for | Database data, application state | Development code, config files |
|---|
| Backup mechanism | `docker run –volumes-from` | Standard filesystem tools |
|---|
Step 9: Keeping Docker Updated
Docker's official repository handles updates through the standard `apt` mechanism:
“`bash
sudo apt update
sudo apt upgrade -y
“`
To update only Docker-related packages without upgrading the entire system:
“`bash
sudo apt install –only-upgrade docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
“`
Check the Docker release notes before major version upgrades, particularly for changes to the storage driver, cgroup version handling, or deprecated API versions that may affect existing `compose.yml` files.
Docker vs. Alternative Containerization Approaches
| Feature | Docker Engine | Podman | LXC/LXD | containerd (standalone) |
|---|
| — | — | — | — | — |
|---|
| Daemon architecture | Centralized daemon | Daemonless | Daemon-based | Daemon-based |
|---|
| Rootless support | Yes (v20+) | Native | Limited | Yes |
|---|
| Docker Compose support | Native | Via `podman-compose` | No | No |
|---|
| OCI compliance | Yes | Yes | No (LXC format) | Yes |
|---|
| Kubernetes integration | Via CRI-dockerd shim | Native CRI | No | Native CRI |
|---|
| Windows/macOS support | Docker Desktop | Limited | No | No |
|---|
| Best fit | General development and production | Security-focused, rootless | System containers, VMs | Kubernetes nodes |
|---|
For teams running containerized workloads at scale on bare metal, a Dedicated Servers environment gives you full control over kernel parameters, storage I/O scheduling, and network configuration β all of which directly affect container density and performance.
Production Hardening Checklist
Before running Docker in a production environment, address the following:
Daemon configuration (`/etc/docker/daemon.json`):
“`json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"userns-remap": "default",
"live-restore": true,
"no-new-privileges": true
}
“`
- `log-opts` with `max-size` and `max-file`: Without log rotation, Docker's JSON log files will fill your disk. This is one of the most common causes of unexpected server outages on containerized hosts.
- `userns-remap: "default"`: Enables user namespace remapping, so container root (UID 0) maps to an unprivileged UID on the host.
- `live-restore: true`: Allows containers to keep running if the Docker daemon crashes or is restarted during an upgrade β critical for zero-downtime maintenance.
- `no-new-privileges: true`: Prevents container processes from gaining additional privileges via `setuid` or `setgid` binaries.
Network and firewall considerations:
Docker manipulates `iptables` directly and bypasses `ufw` rules by default. A container with a published port (`-p 8080:80`) will be accessible from the internet even if `ufw deny 8080` is set. To enforce `ufw` rules over Docker's `iptables` manipulation, add `"iptables": false` to `daemon.json` and manage routing manually, or use Docker's `–network host` with explicit `ufw` rules.
For projects requiring HTTPS termination, pair your containerized application with a properly configured reverse proxy (Nginx or Traefik) and a valid certificate. SSL Certificates are a prerequisite for any production web service running behind a containerized stack.
If your workload involves machine learning inference, model serving, or GPU-accelerated data processing inside containers, the NVIDIA Container Toolkit integrates directly with Docker Engine. GPU Hosting provides the underlying hardware required for these workloads.
For teams managing multiple projects with web-based container management, VPS with cPanel offers a managed control panel environment that can complement Docker-based deployments for simpler application stacks.
Key Technical Takeaways and Decision Matrix
When to use Docker on a VPS vs. a dedicated server:
- Use a VPS for development environments, staging, and low-to-medium traffic production workloads where container density is 10β50 containers.
- Use a dedicated server when container density exceeds 50 instances, when you need predictable I/O performance (no noisy-neighbor effect), or when kernel parameter tuning (`sysctl`) is required for your workload.
Operational checklist before going live:
- Configure log rotation in `daemon.json` (`max-size`, `max-file`)
- Enable `live-restore` to survive daemon restarts without container downtime
- Use named volumes, not bind mounts, for stateful service data
- Pin image versions in all `compose.yml` files β never use `latest` in production
- Enable `userns-remap` or run rootless Docker on multi-tenant hosts
- Audit `iptables` rules after Docker installation to confirm firewall policy is not bypassed
- Set `restart: unless-stopped` on all long-running services
- Run `docker system prune` on a scheduled cron job to prevent disk exhaustion
- Use custom bridge networks for all inter-container communication β never rely on the default bridge for service discovery
FAQ
Does Docker on Ubuntu use `systemd` or `cgroupfs` as the cgroup driver by default?
Since Docker Engine 20.10, the default cgroup driver on `systemd`-based systems (including all modern Ubuntu LTS releases) is `systemd`. This aligns with Kubernetes requirements and avoids the instability caused by running two cgroup managers simultaneously. You can verify with `docker info | grep -i cgroup`.
What is the difference between `docker compose down` and `docker compose stop`?
`docker compose stop` halts running containers but preserves them and their associated networks. `docker compose down` stops containers and then removes them along with the networks Compose created. Adding `–volumes` to `down` also removes named volumes defined in the Compose file β use this flag with caution in production, as it permanently deletes persistent data.
Why does Docker bypass `ufw` firewall rules on Ubuntu?
Docker inserts its own `iptables` rules in the `DOCKER` and `DOCKER-USER` chains, which are evaluated before `ufw`'s `INPUT` chain rules. This means a port published with `-p` is reachable from the internet regardless of `ufw` policy. The correct mitigation is to add rules to the `DOCKER-USER` chain directly, or to bind published ports to a specific interface (e.g., `-p 127.0.0.1:8080:80`) when external access is not required.
How do I limit the CPU and memory a Docker container can consume?
Use resource constraint flags at runtime: `docker run –memory="512m" –cpus="1.5" my-image`. In Compose, set these under the `deploy.resources` key (Compose V2) or the top-level `mem_limit` and `cpus` keys. Without limits, a single runaway container can exhaust host resources and bring down all other containers on the same host.
Can I run Docker inside a Docker container (Docker-in-Docker)?
Yes, but it is strongly discouraged for production use. The common pattern for CI pipelines is to mount the host Docker socket (`-v /var/run/docker.sock:/var/run/docker.sock`) into the CI container, which gives the container full control over the host's Docker daemon β a significant security risk. A safer alternative is to use BuildKit's `–allow security.insecure` flag or purpose-built tools like Kaniko or Buildah, which build OCI images without requiring a Docker daemon.
