---
id: 2024-10-21-how-to-install-docker-and-docker-compose-on-almalinux
slug: how-to-install-docker-and-docker-compose-on-almalinux
title: "AlmaLinux: install and configure Docker and Docker Compose"
excerpt: "Install Docker CE and Docker Compose on AlmaLinux 9 and 10. Production-tested enterprise setup with DNF, SELinux volume contexts, firewalld and systemd."
date: "2024-10-21T09:00:00+02:00"
updated: "2026-09-08T08:44:00+02:00"
author:
  name: "Sebastian Palencsár"
  handle: "spalencsar"
category: "server-environments"
tags: ["almalinux", "docker", "docker-compose", "container", "linux", "rhel", "server-environments"]
reading_time: 30
toc: true
---

[AlmaLinux](https://almalinux.org/){.badge-link-text} is a firmly established binary-compatible enterprise distribution in the Red Hat ecosystem. Red Hat's default stack is its own container toolchain around [Podman](https://podman.io/){.badge-link-text}, Buildah and Skopeo, but existing development pipelines, third-party tools and multi-container environments still often require the original [Docker Engine (Docker CE)](https://www.docker.com/){.badge-link-text} plus modern [Docker Compose V2](https://docs.docker.com/compose/){.badge-link-text}.

Putting Docker Engine onto enterprise Linux is a different job than on Debian or Ubuntu. [SELinux](https://www.redhat.com/en/topics/linux/what-is-selinux){.badge-link-text} needs a precise understanding of volume labels (`:z` and `:Z`), packet filtering through `firewalld` needs clean port openings, and preinstalled Podman packages have to come off the system first so they do not fight Docker for the socket interface.

Docker CE and the Docker Compose CLI plugin go onto AlmaLinux 9 and current AlmaLinux 10 in a production-ready layout. The daemon is hardened through `daemon.json`, and network plus storage isolation is configured so the engine actually runs without the usual RHEL-family surprises.

## Architecture and enterprise specifics

On AlmaLinux a container engine that historically grew up around Debian and Ubuntu meets an uncompromising enterprise architecture. Three core pieces have to be understood before runtime problems show up later:

**Package conflicts with Red Hat tools:**

AlmaLinux ships Podman and the `crun` package from its default repositories. Both tools expose similar command structures and socket files. Leave those packages in place and the DNF transaction for the official Docker RPM packages fails on file collisions.

**SELinux as the default watchdog:**

SELinux on AlmaLinux runs in `Enforcing` mode by default. Containers run in a restrictive sandbox (`container_t`). When a container reaches host directories through a bind mount, the Linux kernel immediately blocks access with `EACCES` (Permission Denied) unless the filesystem target carries the SELinux type `container_file_t`.

**Network interaction with firewalld:**

Docker manages port forwarding and masquerading through direct iptables and nftables rules. A firewall restart or reload (`firewall-cmd --reload`) flushes those dynamic rules out of the kernel, and containers lose network access until the Docker service is restarted.

```markdown
┌─────────────────────────────────────────────────────────────┐
│   AlmaLinux Docker and SELinux enterprise architecture      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Client / CLI (docker and docker compose)                  │
│         │                                                   │
│         ▼ Unix socket (/var/run/docker.sock)                │
│   ┌─────────────────────────────────────────────────────┐   │
│   │  dockerd (container engine and REST API daemon)     │   │
│   └──────────────────────────┬──────────────────────────┘   │
│                              │                              │
│                              ▼ containerd / runc            │
│   ┌─────────────────────────────────────────────────────┐   │
│   │  Container instances (namespaces, cgroups v2)       │   │
│   └───┬─────────────────────────────────────────────┬───┘   │
│       │                                             │       │
│       ▼ firewalld (port/NAT)                        ▼       │
│   ┌─────────────┐                             ┌─────────┐   │
│   │ nftables    │                             │ SELinux │   │
│   │ filtering   │                             │ (:z/:Z) │   │
│   └─────────────┘                             └────┬────┘   │
│                                                    │        │
│                                                    ▼        │
│                                               Host storage  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

The Docker Engine on AlmaLinux uses `cgroups v2` and the `overlay2` storage driver by default. Both mechanisms are native in the enterprise kernel.

## Prerequisites and system preparation

You need a server running AlmaLinux 9 or AlmaLinux 10, SSH access, and a user account with administrative rights through `sudo`.

### Check kernel and distribution release

First verify the installed AlmaLinux version and the active Linux kernel:

```bash
cat /etc/os-release
uname -r
```

AlmaLinux 9 is based on Linux kernel 5.14, while AlmaLinux 10 runs modern 6.x kernels. Both variants meet every requirement for namespaces, control groups v2 and OverlayFS.

### Update the system

Bring every installed system package up to date through the package manager:

```bash
sudo dnf upgrade -y
```

If the update installed a new kernel, reboot the server once:

```bash
sudo reboot
```

### Clean up preinstalled container artefacts

Many AlmaLinux installation profiles already ship tools from the Podman ecosystem. Those packages can claim the same man pages, socket names and command aliases, so you remove competing packages completely:

```bash
sudo dnf remove -y podman buildah skopeo docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine
```

<blockquote class="infobox infobox--info">
💡 **Clean package base:** Removing Podman and old Docker packages keeps DNF from aborting later dependency resolution with cryptic error messages. Existing configuration files under `/etc/` stay untouched.
</blockquote>

## Configure the official Docker repository

Docker provides its own RPM repositories for enterprise Linux systems. On AlmaLinux and Rocky Linux you use the official **CentOS repository**.

<blockquote class="infobox infobox--warn">
⚠️ **CentOS vs. RHEL repository:** On AlmaLinux always use the repository URL `download.docker.com/linux/centos/docker-ce.repo`. The sibling repository `linux/rhel/` is historically meant for Red Hat subscription systems and regularly produces HTTP 404 package errors on AlmaLinux minor-point releases.
</blockquote>

### Add the repository on AlmaLinux 9 (DNF4)

AlmaLinux 9 uses DNF4. Install the `dnf-plugins-core` package first so the configuration manager is available:

```bash
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
```

### Add the repository on AlmaLinux 10 (DNF5)

AlmaLinux 10 uses the rewritten DNF5 package manager. Syntax for external repositories is simpler here and no longer needs a separate plugin package:

```bash
sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/centos/docker-ce.repo
```

### Import the GPG key and refresh the cache

Import Docker's official signing key so DNF can cryptographically verify the authenticity of every downloaded RPM package:

```bash
sudo rpm --import https://download.docker.com/linux/centos/gpg
sudo dnf makecache
```

Then check that the repository is listed as active:

```bash
dnf repolist | grep docker
```

Output: `docker-ce-stable   Docker CE Stable - ...`

## Install Docker Engine and Docker Compose V2

With the package source in place, install the full container toolchain. Outdated wrapper scripts and manual binary downloads from GitHub stay out of the picture; native RPM packages are the path.

### Install the core packages

Install the Docker Engine, the CLI, the containerd runtime, and the official CLI plugins for Buildx and Compose:

```bash
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

<span class="nb-accent">The role of each package:</span>

* **`containerd.io`**: The standardised container runtime that steers container lifecycle at kernel level.
* **`docker-ce`**: The actual Docker daemon (`dockerd`), which exposes the REST API and manages networks, volumes and images.
* **`docker-ce-cli`**: The `docker` command-line tool that controls the daemon.
* **`docker-buildx-plugin`**: Extends the CLI with modern BuildKit features for parallel and multi-architecture builds.
* **`docker-compose-plugin`**: Integrates Docker Compose V2 as a native subcommand (`docker compose`) directly into the Docker CLI.

### Verify package versions

After the transaction completes, check the installed software versions:

```bash
rpm -qa | grep -E 'docker|containerd'
```

Expected output (example):

```bash
containerd.io-1.7.25-3.1.el9.x86_64
docker-ce-27.3.1-1.el9.x86_64
docker-ce-cli-27.3.1-1.el9.x86_64
docker-compose-plugin-2.31.0-1.el9.x86_64
docker-buildx-plugin-0.19.1-1.el9.x86_64
```

## Service control and autostart through systemd

After package installation the Docker service on enterprise Linux is neither started nor enabled for boot by default.

### Start and enable the Docker service

Start the daemon and use the `--now` switch so the systemd unit comes up automatically on every server reboot:

```bash
sudo systemctl enable --now docker
```

Check the daemon state:

```bash
sudo systemctl status docker
```

The output must show `Active: active (running)`. Check the core parameters of the running engine with `docker info`:

```bash
sudo docker info | grep -E 'Server Version|Storage Driver|Cgroup Version'
```

Output:

```bash
 Server Version: 27.3.1
 Storage Driver: overlay2
 Cgroup Version: 2
```

### Set up non-root access

By default the Unix socket `/var/run/docker.sock` belongs to user `root` and group `docker`. To run Docker commands day to day without prefixing `sudo` every time, add your system user to the `docker` group:

```bash
sudo usermod -aG docker $USER
```

For the new group membership to take effect, either run `newgrp docker` or log out of SSH once and log back in.

<blockquote class="infobox infobox--warn">
⚠️ **Security risk of the docker group:** Membership in the `docker` group is effective root on the host. A simple bind mount of `/` into a privileged container lets you manipulate the host filesystem at will. Grant this permission only to trusted administrators, never to service accounts.
</blockquote>

Verify access without extra administrative rights:

```bash
docker run --rm hello-world
```

If `Hello from Docker!` appears, your unprivileged shell is talking to the daemon correctly.

## Enterprise hardening: SELinux and volume mounts

The interaction between Docker and SELinux on AlmaLinux is one of the most common failure modes for administrators who come from Ubuntu or Debian.

### The problem: EACCES on bind mounts

When you bind a directory from the host (for example a web root or configuration files) into a container, SELinux type checks kick in. A process inside the container has context `container_t`. The directory on the host typically has a context such as `unconfined_u:object_r:user_home_t:s0` or `var_t`.

Because `container_t` must not access ordinary user files, the Linux kernel blocks the access immediately:

```bash
# This mount fails inside the container with SELinux enforcing:
docker run -d -p 8080:80 -v /srv/web/html:/usr/share/nginx/html:ro nginx:alpine
# Result in the container log: "open() /usr/share/nginx/html/index.html failed (13: Permission denied)"
```

### The solution: the volume flags `:z` and `:Z`

Docker ships built-in switches that automatically adjust the SELinux context of the mounted host directory:

* **`:z` (shared):** Assigns the directory the context `container_file_t`. Several independent containers may read and write this directory at the same time.
* **`:Z` (private / exclusive):** Assigns the directory a uniquely labelled MCS label (`container_file_t:s0:c123,c456`). Only that one specific container has access. No other container on the system can reach this data.

```markdown
┌─────────────────────────────────────────────────────────────┐
│   SELinux volume isolation and mount flags (:z / :Z)        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Host filesystem (default context: unconfined_u:..._t)     │
│         │                                                   │
│         ├────────────────────────┬───────────────────┐      │
│         ▼ No flag                ▼ With flag :z      ▼ :Z   │
│   ┌────────────────────┐   ┌────────────────────────────┐   │
│   │ Access blocked!    │   │ container_file_t (shared)  │   │
│   │ (HTTP 403/EACCES)  │   │ Multiple containers access │   │
│   └────────────────────┘   └────────────────────────────┘   │
│                                    │                        │
│                                    ▼                        │
│                        Safe container access                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

🔧 **Practical example:**

Create a directory for static HTML files and bind it with the `:z` flag:

```bash
mkdir -p /srv/web/html
echo "<h1>AlmaLinux with Docker and SELinux</h1>" > /srv/web/html/index.html

# Start the Nginx container with the :z flag:
docker run -d --name webtest -p 8080:80 -v /srv/web/html:/usr/share/nginx/html:ro,z nginx:alpine
```

Check the SELinux context of the mounted folder on the host:

```bash
ls -Zd /srv/web/html
```

Output: `system_u:object_r:container_file_t:s0 /srv/web/html`

With `container_file_t` in place, Nginx inside the container reads the file without access errors. Never disable SELinux (`setenforce 0`) just to paper over permission problems — use the `:z` or `:Z` flags consistently instead.

## firewalld integration and network hardening

On AlmaLinux, `firewalld` owns packet filtering for the host. The interaction between Docker and firewalld has a technical quirk administrators need to know:

### iptables bypass and port openings

With networking enabled, Docker rewrites the kernel `iptables` and `nftables` tables on its own (`nat` table in the `PREROUTING` chain and `filter` table in the `DOCKER` chain). Publish a port with `-p 8080:80` and that rule hits in the Linux kernel *before* the regular ingress rules of the firewalld zones.

Explicit firewalld openings on enterprise systems are still mandatory:

```bash
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload
```

### The firewall reload conflict

<blockquote class="infobox infobox--warn">
⚠️ **Watch `firewall-cmd --reload`:** When you reload `firewalld` at runtime, the firewall daemon flushes every kernel filter table. The routing and masquerading rules Docker injected dynamically are wiped completely. Running containers can then neither reach the internet nor receive inbound packets.
</blockquote>

If a firewall change cuts networking, restart the Docker daemon so it rebuilds its filter chains in the kernel:

```bash
sudo systemctl restart docker
```

### Permanently secure the bridge interface

So that firewalld does not drop forwarded traffic over the virtual Docker bridge `docker0`, assign the interface to the `trusted` zone:

```bash
sudo firewall-cmd --permanent --zone=trusted --add-interface=docker0
sudo firewall-cmd --reload
sudo systemctl restart docker
```

## Production configuration: `/etc/docker/daemon.json`

In enterprise operation the Docker Engine defaults should not be taken as-is. Unbounded log files and container deaths during daemon upgrades are typical operational risks.

Create the central configuration file `/etc/docker/daemon.json`:

```bash
sudo nano /etc/docker/daemon.json
```

Put the following hardened, validated configuration in place:

```json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5"
  },
  "live-restore": true,
  "userland-proxy": false
}
```

<span class="nb-accent">What the settings do:</span>

* **`log-opts (max-size & max-file)`**: Caps each container's JSON log files at 20 MB and rotates across 5 files. That reliably stops noisy or broken containers from filling the root partition.
* **`live-restore: true`**: Lets the Docker Engine keep running containers alive when the `dockerd` process is restarted or updated through `dnf upgrade`. That cuts downtime during maintenance windows drastically.
* **`userland-proxy: false`**: Turns off the extra helper process `docker-proxy`. Traffic is then routed directly through kernel NAT (iptables/nftables), which is faster and cheaper on resources.

<span class="nb-accent">Important hardening note on no-new-privileges:</span>

A common recommendation is to enable `no-new-privileges` so privilege escalation through `setuid` or `setgid` binaries inside containers is blocked. That option does **not** exist as a global key in `daemon.json` in the Docker ecosystem (the daemon would refuse to start with an error). Declare it per container with `--security-opt=no-new-privileges:true` or in the Compose file instead.

Load the new configuration by restarting the daemon:

```bash
sudo systemctl restart docker
```

Verify the active settings:

```bash
docker info | grep -E 'Logging Driver|Live Restore'
```

## Practical use with Docker Compose V2

Docker Compose V2 is fully integrated into the Docker command set as a CLI plugin. The modern invocation is `docker compose` with a space (not the obsolete Python tool `docker-compose` with a hyphen).

### Create a multi-container project

Create a project directory for a typical web stack of an Nginx frontend and a Redis cache:

```bash
mkdir -p ~/webstack/html
cd ~/webstack

echo "<h1>Production stack on AlmaLinux</h1>" > html/index.html
```

Create the configuration file `docker-compose.yml`:

```bash
nano docker-compose.yml
```

Paste the following stack. Note the `:ro,z` flag on the host volume for SELinux:

```yaml
name: enterprise-stack

services:
  web:
    image: nginx:alpine
    container_name: web_frontend
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro,z
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1/ || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 3

  cache:
    image: redis:alpine
    container_name: redis_cache
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped

volumes:
  redis_data:
```

### Start and monitor the stack

Start the entire stack in the background:

```bash
docker compose up -d
```

Docker Compose pulls the images, creates the isolated network `enterprise-stack_default`, sets up the named volume `redis_data` and starts the containers.

Check the service state:

```bash
docker compose ps
```

**Expected output:**

```bash
NAME             IMAGE          COMMAND                  SERVICE   CREATED          STATUS                    PORTS
redis_cache      redis:alpine   "docker-entrypoint.s…"   cache     15 seconds ago   Up 14 seconds             6379/tcp
web_frontend     nginx:alpine   "/docker-entrypoint.…"   web       15 seconds ago   Up 14 seconds (healthy)   0.0.0.0:8080->80/tcp
```

**Check access to the web server from the command line:**

```bash
curl http://127.0.0.1:8080
```

Output: `<h1>Production stack on AlmaLinux</h1>`

**Stop the stack cleanly when you no longer need it:**

```bash
docker compose down
```

## Maintenance, updates and troubleshooting

Stable production operation needs standardised maintenance procedures and fast intervention when something breaks.

### Apply Docker Engine updates

Because the official package source is configured, updates for the Docker Engine and the Compose plugin arrive through the system package manager:

```bash
# On AlmaLinux 9:
sudo dnf check-update | grep docker
sudo dnf upgrade -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# On AlmaLinux 10:
sudo dnf upgrade -y 'docker-*' containerd.io
```

With `"live-restore": true` in `daemon.json`, existing containers keep running without interruption while the daemon updates.

### Analyse SELinux denials

If a container throws unexpected permission errors, check the audit log for SELinux AVCs:

```bash
sudo ausearch -m avc -ts recent
```

A `denied { read }` for a container there means the volume mount is missing the matching `:z` flag, or the directory needs an explicit context assignment. For a fast temporary adjustment, `chcon` is enough:

```bash
sudo chcon -Rt container_file_t /path/to/directory
```

To persist the SELinux type so a system relabel via `restorecon` does not overwrite the permission, use `semanage`:

```bash
sudo semanage fcontext -a -t container_file_t "/path/to/directory(/.*)?"
sudo restorecon -Rv /path/to/directory
```

### Clean up unused resources

Old container images, orphaned build caches and unused networks eat flash storage over time. Clean those artefacts in a controlled way:

```bash
docker system prune -a --volumes -f
```

## Command Reference (Cheatsheet)

The core commands for managing Docker and Compose on AlmaLinux at a glance:

| Command | Purpose | Context |
| :--- | :--- | :--- |
| `sudo dnf install -y docker-ce docker-compose-plugin` | Install Docker Engine and Compose V2 | Package manager |
| `sudo systemctl enable --now docker` | Start the Docker daemon and enable autostart | systemd |
| `sudo usermod -aG docker $USER` | Add the user to the docker group | User management |
| `docker info` | Show full system and storage-driver details | Diagnostics |
| `docker run --rm -v ./data:/app:z image` | Bind a host directory with the SELinux shared label | Container start |
| `sudo firewall-cmd --permanent --add-port=8080/tcp` | Open a container port in the host firewall | firewalld |
| `sudo firewall-cmd --reload && sudo systemctl restart docker` | Reload the firewall and regenerate Docker networking | Network maintenance |
| `docker compose up -d` | Start a multi-container stack in the background | Compose |
| `docker compose ps` | Check status of all services and healthchecks | Compose |
| `docker compose logs -f --tail=50` | Follow live logs of the active stack | Troubleshooting |
| `docker compose down` | Stop the stack cleanly and remove networks | Compose |
| `docker system prune -f` | Clean unused images and build caches | Disk maintenance |

## Further Resources

Official documentation and material for deeper study:

| Resource | Description | Type |
| :--- | :--- | :--- |
| [Docker Engine documentation](https://docs.docker.com/engine/){.badge-link-text} | Official handbook for installation, configuration and storage | Documentation |
| [Docker Compose specification](https://docs.docker.com/compose/compose-file/){.badge-link-text} | Reference for Compose YAML files and service attributes | Reference |
| [AlmaLinux Wiki](https://wiki.almalinux.org/){.badge-link-text} | Official administration guides and release documentation | Documentation |
| [Red Hat SELinux Guide](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/using_selinux/){.badge-link-text} | In-depth documentation of mandatory access control on RHEL | Reference |
| [firewalld documentation](https://firewalld.org/documentation/){.badge-link-text} | Zone, interface and policy management for Linux firewalls | Documentation |

## Conclusion

This deployment gives you a stable, low-maintenance, fully enterprise-capable Docker environment on AlmaLinux. The official CentOS repository delivers reliable updates, and the native Compose CLI plugin enables modern multi-container deployments without external dependencies.

Accounting for the enterprise specifics — especially setting the SELinux mount flags `:z` consistently and opening ports cleanly in `firewalld` — avoids the typical traps that unprepared setups hit on RHEL-based systems.

<blockquote class="infobox infobox--info">
💡 **Practical tip for production:** Configure `/etc/docker/daemon.json` before the first production container start. Parameters such as `log-opts` and `live-restore` protect the system permanently against filesystems filling up and allow uninterrupted daemon updates during regular maintenance windows.
</blockquote>
