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.
flowchart TD
A[Linux]
A --> B[Debian / Ubuntu]
A --> C[RHEL / Rocky Linux]
B --> D[APT Repository]
C --> E[DNF Repository]
D --> F[Docker Packages]
E --> F
F --> G[Docker Engine]
F --> H[Docker CLI]
F --> I[containerd]
F --> J[Buildx]
F --> K[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:
flowchart LR
A[Docker CLI]
B[Docker Engine]
C[containerd]
D[Container]
A -->|Docker API| B
B --> C
C --> D
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.
flowchart TD
A[Docker Repository]
A --> B[Debian]
A --> C[Ubuntu]
A --> D[RHEL]
A --> E[Rocky Linux]
B --> B1[linux/debian]
C --> C1[linux/ubuntu]
D --> D1[linux/rhel]
E --> E1[this article uses linux/centos]
B1 --> F[APT]
C1 --> F
D1 --> G[DNF]
E1 --> G
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:
flowchart LR
A[Docker Client]
B[Docker Engine]
C[Docker Registry]
D[Image]
E[Container]
A -->|docker run| B
B -->|pull if not present locally| C
C --> D
D --> B
B -->|Create + Start| E
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.
flowchart LR
A[registry.example.com]
B[project/nginx]
C[1.0]
A -->|Registry| B
B -->|Repository| C
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.
flowchart LR
A[Developer / CI]
B[Private Registry]
C[Server A]
D[Server B]
E[Server C]
A -->|docker push| B
B -->|docker pull| C
B -->|docker pull| D
B -->|docker pull| E
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:
flowchart TD
A[Need to transfer a Docker image]
A --> B{Long-term, multi-machine use?}
B -->|No| C[docker save / load]
B -->|Yes| D[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:
flowchart TD
A[Docker Client]
B["Nginx<br/>registry.example.com:443"]
C["Docker Registry<br/>127.0.0.1:5000"]
D["/data/registry"]
A -->|HTTPS| B
B -->|HTTP| C
C -->|Persistent Data| D
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:
flowchart TD
A[Docker Client]
subgraph Docker Network
B[Nginx Container]
C["Registry Container<br/>registry:5000"]
end
D["/data/registry"]
A -->|HTTPS| B
B -->|HTTP| C
C --> D
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:
flowchart TD
A[Configure the Docker Repository]
A --> B[APT<br/>Debian / Ubuntu]
A --> C[DNF<br/>RHEL / Rocky Linux]
B --> D[Install Docker Packages]
C --> D
D --> E[Start Docker Engine]
E --> F[Configure User Permissions]
F --> G{How to obtain and distribute images?}
G -->|Single machine| H[Public Registry]
G -->|Occasional offline transfer| I[docker save / load]
G -->|Long-term, multi-server| J[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.