Installing Docker itself is no longer difficult these days. Whether you check the official documentation or just have an AI generate the install commands for your current Linux environment, deployment can be finished quickly.

What’s actually worth understanding is which package repository Docker gets installed from, what differs between distributions, and — once Docker is installed — where images should come from and how they should be distributed.

The process for installing Docker on Linux is actually quite unified. Ubuntu and Debian use APT, RHEL and Rocky Linux use DNF, but in the end all of them install the same set of components: Docker Engine, the CLI, containerd, Buildx, and Compose.

Linux

Debian / Ubuntu

RHEL / Rocky Linux

APT Repository

DNF Repository

Docker Packages

Docker Engine

Docker CLI

containerd

Buildx

Docker Compose

Linux

Debian / Ubuntu

RHEL / Rocky Linux

APT Repository

DNF Repository

Docker Packages

Docker Engine

Docker CLI

containerd

Buildx

Docker Compose

This article covers the Docker installation process by following this shared flow, rather than repeating several nearly identical tutorials for each distribution.

1. What Gets Installed When You “Install Docker”

What we casually call “installing Docker” actually installs several components:

docker-ce
docker-ce-cli
containerd.io
docker-buildx-plugin
docker-compose-plugin

Among these, docker-ce provides the Docker Engine — the dockerd process that runs long-term in the background. docker-ce-cli provides the docker command you use day to day, with the CLI communicating with the daemon over the Docker API. containerd handles lower-level container lifecycle management, Buildx provides modern image-building capabilities, and the Compose plugin is used to manage services made up of multiple containers.

The relationship between them can be understood simply as:

Docker API

Docker CLI

Docker Engine

containerd

Container

Docker API

Docker CLI

Docker Engine

containerd

Container

Docker Compose is now installed as a Docker CLI plugin, so you should use docker compose rather than the docker-compose command commonly seen in older tutorials, and there’s no longer any need to install Compose via pip install docker-compose.

If a machine previously had Docker installed from the system repositories, an old Docker version, or another compatible implementation, you can clean up conflicting packages as appropriate. But if the machine already has an actively-used Podman workload on it, you shouldn’t blindly remove Podman just to install Docker.

2. Setting Up the Docker Package Repository

The biggest difference when installing Docker across Linux distributions really just comes down to the package manager and repository.

Docker Repository

Debian

Ubuntu

RHEL

Rocky Linux

linux/debian

linux/ubuntu

linux/rhel

this article uses linux/centos

APT

DNF

Docker Repository

Debian

Ubuntu

RHEL

Rocky Linux

linux/debian

linux/ubuntu

linux/rhel

this article uses linux/centos

APT

DNF

Debian and Ubuntu use APT. First install the base tools and set up the keyring:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings

You can get the current distribution from /etc/os-release:

. /etc/os-release

DISTRO="$ID"
CODENAME="${UBUNTU_CODENAME:-$VERSION_CODENAME}"

For the systems covered in this article:

Ubuntu  -> DISTRO=ubuntu
Debian  -> DISTRO=debian

Download the corresponding Docker GPG key:

sudo curl -fsSL \
    "https://download.docker.com/linux/${DISTRO}/gpg" \
    -o /etc/apt/keyrings/docker.asc

sudo chmod a+r /etc/apt/keyrings/docker.asc

Then create the Docker APT source:

sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/${DISTRO}
Suites: ${CODENAME}
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

Finally, update the package index:

sudo apt update

RHEL and Rocky Linux use DNF. First install the repository management tool:

sudo dnf install -y dnf-plugins-core

RHEL can use Docker’s official RHEL repository directly:

sudo dnf config-manager --add-repo \
    https://download.docker.com/linux/rhel/docker-ce.repo

Rocky Linux needs special attention here.

Don’t construct a URL yourself based on the distribution name, like this:

https://download.docker.com/linux/rocky/

Docker doesn’t actually provide a standalone repository like that.

This article uses Docker’s CentOS repository on Rocky Linux instead:

sudo dnf config-manager --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

Rocky Linux, RHEL, and CentOS all belong to the Enterprise Linux ecosystem, and their corresponding packages typically show up tagged as:

.el9
.el10

For example, Rocky Linux 9 uses EL9 packages, and Rocky Linux 10 uses EL10 packages.

Once set up, you can check whether the repository is working correctly:

dnf repolist | grep docker
dnf list docker-ce --showduplicates

If you’d previously misconfigured it as:

download.docker.com/linux/rocky/

you can delete the corresponding /etc/yum.repos.d/docker-ce.repo file first, then set it up again.

3. Installation and Basic Configuration

Once the repository is set up, there’s almost no difference between distributions from this point on.

Debian and Ubuntu use:

sudo apt install -y \
    docker-ce \
    docker-ce-cli \
    containerd.io \
    docker-buildx-plugin \
    docker-compose-plugin

RHEL and Rocky Linux use:

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

Once installation finishes, start Docker:

sudo systemctl enable --now docker

Check the version:

docker --version
docker compose version

Then run a test container:

sudo docker run --rm hello-world

This one command already runs through the simplest possible Docker workflow:

docker run

pull if not present locally

Create + Start

Docker Client

Docker Engine

Docker Registry

Image

Container

docker run

pull if not present locally

Create + Start

Docker Client

Docker Engine

Docker Registry

Image

Container

The Docker daemon requires root privileges by default. If you want regular users to run docker directly:

sudo usermod -aG docker "$USER"

Then, after logging back into the shell:

docker ps

Keep in mind that a user who can access the Docker daemon effectively has very high system privileges, so don’t casually add untrusted users to the docker group.

The Docker daemon’s main configuration file is:

/etc/docker/daemon.json

The default data directory is:

/var/lib/docker

If the system disk is small, or the machine has a separate data disk, you can change it:

{
  "data-root": "/data/docker"
}

Then:

sudo systemctl restart docker

If the default disk space is sufficient, there’s no need to change Docker’s data directory just for the sake of “optimizing” something.

4. Docker Registry and Image Distribution

Once Docker is installed, the next question that comes up often is:

Where do Docker images actually come from?

For example:

docker pull nginx

is actually downloading an image from a Registry.

A complete image reference can be written as:

registry.example.com/project/nginx:1.0

which contains a Registry, a repository, and a tag.

Registry

Repository

registry.example.com

project/nginx

1.0

Registry

Repository

registry.example.com

project/nginx

1.0

Docker Hub itself is a public Registry.

If you only have one server, using the public Registry directly is usually enough. But if you have multiple servers, CI/CD, an intranet environment, or unstable access to the public Registry, it’s worth deploying your own Registry.

docker push

docker pull

docker pull

docker pull

Developer / CI

Private Registry

Server A

Server B

Server C

docker push

docker pull

docker pull

docker pull

Developer / CI

Private Registry

Server A

Server B

Server C

The simplest way to test this is:

docker run -d \
    --name registry \
    --restart always \
    -p 5000:5000 \
    registry:3

Then you can test pushing:

docker pull nginx:latest

docker tag nginx:latest \
    localhost:5000/nginx:latest

docker push \
    localhost:5000/nginx:latest

Here, docker tag doesn’t re-copy the image — it adds a new reference to the existing image. The Registry hostname in a reference determines where docker push should upload the image to.

For long-term operation, the Registry’s data should be persisted:

services:
  registry:
    image: registry:3
    container_name: registry
    restart: unless-stopped
    ports:
      - "127.0.0.1:5000:5000"
    volumes:
      - /data/registry:/var/lib/registry

Then:

docker compose up -d

This deliberately only listens on:

127.0.0.1:5000

because a Registry without TLS and authentication shouldn’t be exposed directly to the public internet.

If you’re only occasionally transferring one image to another machine, there’s no need to deploy a Registry for that. You can just do:

docker save -o nginx.tar nginx:latest
rsync -avP nginx.tar server:/tmp/
docker load -i /tmp/nginx.tar

The difference between the two approaches can be understood simply as:

No

Yes

Need to transfer a Docker image

Long-term, multi-machine use?

docker save / load

Private Registry

No

Yes

Need to transfer a Docker image

Long-term, multi-machine use?

docker save / load

Private Registry

5. Using Nginx to Serve the Registry over HTTPS

If the Registry needs to be accessed long-term by other servers, it should use HTTPS, with access control configured.

A structure that’s relatively easy to maintain is:

HTTPS

HTTP

Persistent Data

Docker Client

Nginx

registry.example.com:443

Docker Registry

127.0.0.1:5000

/data/registry

HTTPS

HTTP

Persistent Data

Docker Client

Nginx

registry.example.com:443

Docker Registry

127.0.0.1:5000

/data/registry

The Registry itself only listens on localhost:

127.0.0.1:5000

and the domain name, TLS certificate, and public-facing entry point are all handled by Nginx.

For example:

server {
    listen 443 ssl;
    server_name registry.example.com;

    ssl_certificate \
        /etc/letsencrypt/live/registry.example.com/fullchain.pem;
    ssl_certificate_key \
        /etc/letsencrypt/live/registry.example.com/privkey.pem;

    client_max_body_size 0;

    location / {
        proxy_pass http://127.0.0.1:5000;

        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_read_timeout 900;
        proxy_request_buffering off;
    }
}

client_max_body_size 0 avoids Docker image layers failing due to a regular web upload size limit.

If Nginx itself is also running inside Docker, you can no longer use:

proxy_pass http://127.0.0.1:5000;

because 127.0.0.1 inside a container points to the Nginx container itself.

In that case, Nginx and the Registry should be placed on the same Docker network and accessed by service name instead:

proxy_pass http://registry:5000;

which turns the structure into:

Docker Network

HTTPS

HTTP

Docker Client

Nginx Container

Registry Container

registry:5000

/data/registry

Docker Network

HTTPS

HTTP

Docker Client

Nginx Container

Registry Container

registry:5000

/data/registry

HTTPS alone isn’t enough. If the Registry can be reached from the public internet, authentication should also be configured.

You can add Basic Authentication on Nginx:

location / {
    auth_basic "Docker Registry";
    auth_basic_user_file /etc/nginx/registry.htpasswd;

    proxy_pass http://127.0.0.1:5000;

    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_read_timeout 900;
    proxy_request_buffering off;
}

Log in from the Docker client:

docker login registry.example.com

After that, you can:

docker tag nginx:latest \
    registry.example.com/nginx:latest

docker push \
    registry.example.com/nginx:latest

On another server:

docker login registry.example.com

docker pull \
    registry.example.com/nginx:latest

For a Registry exposed to the public internet, it’s not recommended to use HTTP with insecure-registry for convenience. Normal cross-machine usage should prioritize setting up HTTPS.

6. Summary

Installing Docker on Linux doesn’t fundamentally involve several completely different processes.

Ubuntu and Debian use APT, RHEL and Rocky Linux use DNF, and the real differences are mainly in the repository and package manager:

Single machine

Offline transfer

Long-term, multi-server

Configure the Docker Repository

APT

Debian / Ubuntu

DNF

RHEL / Rocky Linux

Install Docker Packages

Start Docker Engine

Configure User Permissions

Image distribution method?

Public Registry

docker save / load

Private Registry

Single machine

Offline transfer

Long-term, multi-server

Configure the Docker Repository

APT

Debian / Ubuntu

DNF

RHEL / Rocky Linux

Install Docker Packages

Start Docker Engine

Configure User Permissions

Image distribution method?

Public Registry

docker save / load

Private Registry

Debian and Ubuntu can each use their own corresponding Docker APT repository directly. RHEL can use Docker’s RHEL repository. Rocky Linux has no standalone linux/rocky Docker repository, so this article uses Docker’s CentOS repository as a compatible installation option.

Once Docker is installed, if you’re just running an application on a single machine, the public Registry is usually enough; if you’re only occasionally transferring images offline, docker save/load is simpler; if there’s CI/CD, multi-server deployment, or long-term image distribution needs, it’s worth deploying your own Registry and serving it over HTTPS with authentication through Nginx.

Up to this point, we’ve covered the infrastructure-level questions around Docker:

How to install Docker, and how Docker images are obtained, stored, and distributed.

As for how images, containers, volumes, bind mounts, networks, port mapping, and Docker Compose actually work, that’s better suited to a separate article of its own.