On Ubuntu 26.04 LTS (Resolute Raccoon) Docker is the most widely used platform for container-based isolation of server services. Unlike full virtual machines, containers share the host system's Linux kernel. Isolation of processes, filesystems and network stacks is native through Linux kernel mechanisms: Control Groups (cgroups v2) and Kernel Namespaces.
The following setup puts official Docker Community Edition (CE) onto Ubuntu 26.04 LTS. It covers safe handling of the Docker GPG key under /etc/apt/keyrings, daemon configuration for automated log rotation, permission control through the docker group, and how that interacts with packet filters and firewalls such as UFW.
❗ Important note: The audience is Linux administrators and IT professionals who already have basic Ubuntu and command-line experience. You should be comfortable in the terminal and understand what package management means. At the same time the material is suitable for beginners in container technology who want to learn Docker from the ground up.
⚠️ Privilege level of the Docker daemon: The Docker daemon (
dockerd) runs with full root rights by default. Anyone with access to the Docker socket/var/run/docker.sockhas de facto unrestricted root access to the base system. Run containers in production on the least-privilege principle and grant group memberships restrictively.
1. Architecture and how it works: containers vs. virtual machines
To run Docker stably in server operation, the distinction from hypervisor-based virtualization (such as KVM or Proxmox VE) is essential. Virtual machines emulate complete hardware resources and boot a standalone guest operating system. Containers, by contrast, wrap processes on the already running host kernel:
┌─────────────────────────────────────────────────────────────┐
│ Virtualization vs. containerization │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Virtual machines (hypervisor virtualization): │
│ ┌─────────────────┬─────────────────┬───────────┐ │
│ │ App A | Libs │ App B | Libs │ App C... │ │
│ ├─────────────────┼─────────────────┼───────────┤ │
│ │ Guest OS │ Guest OS │ Guest OS │ │
│ ├─────────────────┴─────────────────┴───────────┤ │
│ │ Hypervisor (type 1/2, e.g. KVM, ESXi, QEMU) │ │
│ ├───────────────────────────────────────────────┤ │
│ │ Host operating system and hardware │ │
│ └───────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ │
│ 2. Containers (kernel isolation via ns and cgroups): │
│ ┌─────────────────┬─────────────────┬───────────┐ │
│ │ App A | Libs │ App B | Libs │ App C... │ │
│ ├─────────────────┴─────────────────┴───────────┤ │
│ │ Docker Engine / container runtime (containerd)│ │
│ ├───────────────────────────────────────────────┤ │
│ │ Shared host Linux kernel (namespaces/cgroups) │ │
│ ├───────────────────────────────────────────────┤ │
│ │ Host OS (Ubuntu 26.04 LTS) and hardware │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
The Docker Engine itself is several modular layers:
┌─────────────────────────────────────────────────────────────┐
│ Docker Engine architecture and component stack │
│ 1. Docker CLI (command line): │
│ docker run / docker build / docker compose │
│ └──────────────┬─────────────────────────────────────────┤
│ ▼ REST API via /var/run/docker.sock │
│ 2. Docker daemon (dockerd): │
│ Image builds, network drivers, storage (overlay2) │
│ └──────────────┬─────────────────────────────────────────┤
│ ▼ gRPC interface │
│ 3. Container runtime (containerd): │
│ Image distribution, snapshot management, metadata │
│ └──────────────┬─────────────────────────────────────────┤
│ ▼ OCI specification │
│ 4. OCI runtime (runc): │
│ Creates namespaces (pid, net, mnt) and cgroups v2 │
└─────────────────────────────────────────────────────────────┘
Core kernel mechanisms behind Docker
- Namespaces: Isolate visibility. The
pidnamespace makes a process inside the container see its own PID list (starting at PID 1). Thenetnamespace assigns the container its own virtual network interfaces and routing tables.mntisolates mount points,ipcshared memory regions anduseruser IDs. - Control Groups (cgroups v2): Limit and monitor resources. Through cgroups you set how much memory, CPU cycles or disk I/O a container may consume at most, so faulty applications cannot create a denial-of-service condition.
- Storage driver (
overlay2): Builds a writable layer filesystem on copy-on-write (CoW). The base image stays immutable (read-only), while changes are stored in a thin upper layer (upperdir).
Distinction: when containers and when VMs?
| Criterion | Docker container | Virtual machine (KVM / Proxmox) |
|---|---|---|
| Kernel | Shared host kernel | Own kernel per guest |
| Start time | Milliseconds to seconds | 10 to 60 seconds (boot) |
| RAM overhead | Low (only actual process demand) | High (host reserves fixed guest RAM) |
| Security boundary | Shared kernel isolation (namespaces) | Strict hardware virtualization |
| Operating systems | Linux only on a Linux host | Any (Linux, BSD, Windows) |
| Kernel modules | Not loadable by unprivileged containers | Full kernel control in the guest |
2. Check system requirements and prepare the base system
Before installing the official Docker repository, check system architecture, kernel version and remove any leftover packages from earlier distribution exports.
Hardware and architecture check
Docker requires a 64-bit architecture (x86_64 or amd64, or aarch64 / arm64). Run the query in the terminal:
# Verify system architecture and kernel version
uname -m && uname -r
The output must return x86_64 or aarch64. For production servers with several running containers, a minimum of 2 vCPUs and 2–4 GB RAM is recommended so system buffers and container overlays sit cleanly in memory.
Verify the distribution codename of Ubuntu 26.04 LTS:
# Query distribution data
lsb_release -a
Remove leftovers and distribution default packages
Ubuntu ships historical packages such as docker.io or outdated containerd builds in the default package sources. To rule out version conflicts with official upstream packages from Docker Inc., remove them completely:
# Clean old distribution packages
sudo apt remove -y docker docker-engine docker.io containerd runc
If the reply says individual packages were not installed, that is the expected state.
Update the system and install base tools
Bring the package index up to date and install the tools required for download and signature verification:
# Update the base system
sudo apt update && sudo apt upgrade -y
# Install required helper tools
sudo apt install -y ca-certificates curl gnupg lsb-release
❗ Check kernel state: If
apt upgradeinstalled a new Linux kernel, a reboot (sudo reboot) is advisable before the Docker installation. Only then do kernel namespaces andcgroups v2run on the latest security baseline.
3. Add the official Docker APT repository
The official repository guarantees access to current Docker CE versions, security patches and official plugins such as Buildx and Compose.
Set up the GPG keyring safely
Under current Debian and Ubuntu practice, repository keys are no longer stored via the obsolete apt-key or globally under /etc/apt/trusted.gpg, but in a dedicated directory under /etc/apt/keyrings with strict file permissions:
# Create the keyring directory with restrictive permissions
sudo install -m 0755 -d /etc/apt/keyrings
# Download the official Docker GPG key and store it as dearmored ASCII
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
# Ensure read rights for all system services and APT
sudo chmod a+r /etc/apt/keyrings/docker.asc
Add the repository to the APT source list
Register the package source with a reference to the downloaded signing key:
# Set up the Docker APT source
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Refresh the APT package index so the new packages are read:
# Sync the package index with the new source
sudo apt update
In the console output, the line Get:... https://download.docker.com/linux/ubuntu ... InRelease confirms that APT contacted the repository successfully.
4. Install Docker Engine and container tools
Install the full Docker toolset consisting of the engine, the CLI, containerd and the official plugins:
# Install Docker CE, containerd and modern plugins
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
The installed packages do the following:
docker-ce: The actual daemon service (dockerd).docker-ce-cli: The command-line interface (docker) that talks to the daemon REST API.containerd.io: The open-source container runtime to the OCI standard.docker-buildx-plugin: Modern build tooling for multi-architecture images (Moby BuildKit).docker-compose-plugin: The native Compose command (docker compose) as a direct CLI plugin.
Verify service status and version
Check the runtime status of the systemd service:
# Query Docker service status
sudo systemctl status docker --no-pager
The output must show Active: active (running). Check the installed versions:
# Check the engine version
sudo docker version
# Check the Docker Compose version
docker compose version
💡 Note on Docker Compose v2: Modern Docker Compose is designed as a CLI plugin and is invoked with a space (
docker compose). The historical, separate Python binarydocker-compose(with a hyphen) is obsolete and is no longer used.
Run the installation function test
Start the standardised verification container:
# Run the official function-test container
sudo docker run --rm hello-world
This call makes the Docker Engine pull the hello-world image from Docker Hub, instantiate a short-lived container and print the confirmation Hello from Docker! to stdout. The --rm parameter tells Docker to remove the container from the filesystem immediately after it exits.
5. Permission model: set up Docker without sudo
By default the Docker daemon binds its control socket /var/run/docker.sock to user root and group docker. Regular system users cannot talk to the socket without root rights.
To run Docker commands day to day without a leading sudo, add the administrative user to the docker system group:
# Add the current user to the docker group
sudo usermod -aG docker $USER
For the group change to take effect in the running shell, either log in again or activate the group rights directly in the current terminal session:
# Apply group membership in the current shell immediately
newgrp docker
Verify unprivileged access without sudo:
# Test run as a normal user
docker run --rm alpine echo "Container runs without sudo"
⚠️ Security impact of the docker group: Adding a user to the
dockergroup grants effective root on the host. A user with access to the Docker socket can mount arbitrary host directories (/etc,/root) into a container (docker run -v /:/host-root ...) and manipulate them. Add only trusted administration accounts.
6. Daemon configuration: logging, storage and performance
Without targeted configuration, Docker writes container logs to disk without a limit by default. On production services with busy traffic this sooner or later fills the root partition.
Put a central configuration file /etc/docker/daemon.json in place to cap logging and pin the storage driver explicitly:
# Create the configuration directory
sudo mkdir -p /etc/docker
# Create the central daemon configuration
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"live-restore": true
}
EOF
The configured directives do the following:
log-driver: json-file: Writes standard logs in JSON format, compatible withdocker logs.max-size: 10mandmax-file: 3: Rotates logs automatically at 10 MB and keeps at most 3 generations. A container thus never occupies more than 30 MB of disk for logs.storage-driver: overlay2: The recommended, performant storage driver for Linux filesystems (ext4,xfs).live-restore: true: Keeps containers running even when the Docker daemon is restarted (for example during package updates or service restarts). That cuts downtime.
Reload the Docker daemon to activate the settings:
# Restart the Docker daemon
sudo systemctl restart docker
# Verify the applied configuration
docker info | grep -E '(Logging Driver|Storage Driver|Live Restore)'
7. Firewall integration and kernel parameters (UFW vs. Docker)
One of the most common security traps in Linux server operation is the automatic interplay of Docker and the UFW packet-filter firewall.
Understand the UFW bypass trap
When publishing a port (-p 8080:80), Docker rewrites the PREROUTING chain of iptables/nftables directly. Those rules fire before UFW filter rules are evaluated. A port you have not opened in UFW is still reachable from outside if a container is bound with -p 0.0.0.0:Port:Port.
❗ Security rule for port bindings: Always bind container ports for internal services (for example databases or caches) explicitly to localhost (
127.0.0.1) when they must not be exposed to the internet:# Safe: the service listens only on the local loopback interface docker run -d -p 127.0.0.1:3306:3306 mariadb
Enable kernel parameters for bridge network traffic
So that packet filters such as iptables can filter traffic over virtual Linux bridges (docker0) properly, the matching kernel modules and sysctl flags must be set:
# Store sysctl parameters for bridge network traffic
sudo tee /etc/sysctl.d/99-docker-bridge.conf > /dev/null <<'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
# Load the parameters immediately without a reboot
sudo sysctl --system
8. Practical use: container lifecycle and Docker Compose
After the base installation, two typical operating scenarios: manual container control through the CLI, and declarative multi-container operation via Docker Compose.
Practical container lifecycle on a web service
🔧 Practical example:
Start an Nginx web server as a background service (detached mode), bind it to port 8080, inspect the logs and run an interactive command inside the container:
# Start the Nginx container in the background
docker run -d --name webserver -p 8080:80 nginx:alpine
# Check running status and port mappings
docker ps
# View real-time logs of the container
docker logs webserver
# Run a command directly in the container namespace
docker exec -it webserver nginx -v
# Test HTTP reachability from the host
curl -I http://127.0.0.1:8080
# After a successful test, stop and remove the container cleanly
docker stop webserver && docker rm webserver
Declarative operation with Docker Compose
In professional server operation, container configurations are rarely started through long ad-hoc commands, but versioned in a compose.yaml file.
Create a project directory and the configuration file:
# Create the project directory
mkdir -p ~/compose-demo && cd ~/compose-demo
# Create the declarative Compose configuration
tee compose.yaml <<'EOF'
services:
web:
image: nginx:alpine
container_name: demo-web
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
redis:
image: redis:alpine
container_name: demo-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis-data:/data
volumes:
redis-data:
EOF
# Prepare a local HTML directory with a demo file
mkdir -p html
echo "<h1>Docker Compose on Ubuntu 26.04 is running</h1>" > html/index.html
# Start the multi-container stack in the background
docker compose up -d
# Check the status of every service in the stack
docker compose ps
# Shut the stack down when finished
docker compose down
9. Storage management and cleanup workflows
In day-to-day operation, image updates, stopped test containers and build caches accumulate orphaned data under /var/lib/docker.
Analyse storage use
Use the built-in Docker commands to see how much space images, containers, volumes and the build cache occupy:
# Show detailed storage use
docker system df
The output breaks down exactly which share of storage is in active use and what percentage is marked Reclaimable.
Targeted cleanup of orphaned resources
🔧 Practical example:
After several deployments a server shows high storage use in the build cache and unused images. Clean the system in a controlled way:
# Remove unused containers, networks and untagged dangling images
docker system prune -f
# Thorough cleanup: remove all unused images (not only dangling images)
docker image prune -a --filter "until=168h" -f
⚠️ Watch volumes: The command
docker system prunedoes not delete Docker volumes by default, to prevent accidental data loss (for example database data). Only the explicit extra--volumesremoves orphaned volumes permanently.
10. Autostart and systemd hardening
So that Docker and every container with restart policies (restart: unless-stopped or restart: always) come up reliably after a host reboot, enable the systemd services:
# Ensure autostart for Docker Engine and containerd
sudo systemctl enable docker.service containerd.service
End-to-end verification of the whole system
Finally run a complete test that combines name resolution, filesystems and process isolation:
# Full end-to-end function test with Alpine Linux
docker run --rm alpine sh -c "uname -a && ping -c 1 -W 2 dns.google"
If the console shows the kernel output plus a successful ping transfer (1 packets transmitted, 1 packets received), the Docker Engine on Ubuntu 26.04 LTS is working correctly with functioning DNS and bridge network forwarding.
Command Reference (Cheatsheet)
The following table summarises the most important administration commands for daily Docker work:
| Command | Category | Purpose and operational effect |
|---|---|---|
docker run -d --name web -p 8080:80 nginx |
Lifecycle | Starts a container in the background with port forwarding from host 8080 to 80 |
docker ps |
Monitoring | Lists every currently running container with ID, image, status and ports |
docker ps -a |
Monitoring | Lists every container (including exited instances with exit codes) |
docker logs -f --tail 100 <container> |
Logging | Follows stdout/stderr of the container in a stream, limited to 100 lines |
docker exec -it <container> sh |
Debugging | Opens an interactive shell inside the running container namespace |
docker stop <container> |
Lifecycle | Sends SIGTERM (after timeout SIGKILL) for a clean shutdown |
docker rm <container> |
Lifecycle | Deletes stopped containers and frees their writable layer |
docker images |
Storage | Shows every locally cached container image with tags and sizes |
docker rmi <image> |
Storage | Removes a container image from the local cache (/var/lib/docker) |
docker system df |
Maintenance | Shows storage use of images, containers, volumes and BuildKit |
docker system prune -f |
Maintenance | Cleans stopped containers, unused networks and untagged images |
docker compose up -d |
Orchestration | Builds and starts every service defined in compose.yaml in the background |
docker compose down |
Orchestration | Stops and removes every container, network and internal link of the stack |
Further Resources
| Resource | Link | Purpose and content |
|---|---|---|
| Docker documentation | Docker Engine Docs | Official installation instructions, release notes and API reference |
| Docker Compose | Docker Compose Docs | Specification and documentation for multi-container orchestration |
| Ubuntu Server Guide | Ubuntu Server Guide | Official system documentation for Ubuntu 26.04 LTS (Resolute Raccoon) |
| Server hardening 2026 | Linux server hardening: SSH and CrowdSec | Baseline hardening, SSH with FIDO2 and nftables firewalls |
| LEMP stack Ubuntu 26.04 | Ubuntu 26.04 LEMP guide | Companion article for Nginx with HTTP/3, MariaDB and PHP 8.5 |
| Portainer CE web GUI | Install Portainer on Ubuntu | Graphical management of Docker containers, stacks and volumes through a web interface |
Conclusion
On Ubuntu 26.04 LTS, Docker provides a highly resource-efficient platform for server services through native cgroups v2 support and modern kernel overlays. Unlike a stock install, hardening the system — especially capping container logs through daemon.json, enabling live-restore, and knowing about the UFW bypass behaviour of iptables — produces stable production operation.
In day-to-day operation, administrators should watch two parameters above all: storage growth under /var/lib/docker through automated cleanup routines (docker system prune), and binding to localhost (127.0.0.1) so internal services are not exposed to the internet by accident. If you prefer to administer Docker containers, networks and multi-container stacks through a central web dashboard instead of the command line, the article Install Portainer on Ubuntu has the turnkey setup.