Install and configure Docker on Ubuntu 26.04 LTS

Install Docker on Ubuntu 26.04 LTS: from the official APT repository through daemon tuning and log rotation to UFW firewall integration.

Reading time: 25 min

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.sock has 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 pid namespace makes a process inside the container see its own PID list (starting at PID 1). The net namespace assigns the container its own virtual network interfaces and routing tables. mnt isolates mount points, ipc shared memory regions and user user 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 upgrade installed a new Linux kernel, a reboot (sudo reboot) is advisable before the Docker installation. Only then do kernel namespaces and cgroups v2 run 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 binary docker-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 docker group 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 with docker logs.
  • max-size: 10m and max-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 prune does not delete Docker volumes by default, to prevent accidental data loss (for example database data). Only the explicit extra --volumes removes 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.

Share & export

Export as Markdown

Related posts