Server administrationWritten by GLWS14 min readUbuntu 24.04 LTS / AlmaLinux 9
Commands run on your server unless a section says otherwise. Replace example usernames, addresses and paths with your own values. Keep a working session open when changing remote access.

Before you start

Use a fresh or well-understood Ubuntu 24.04 LTS or AlmaLinux 9 VPS with a non-root sudo account and a working firewall. Docker needs a 64-bit system and runs comfortably on 2 GB of memory for a few small containers; images and volumes use disk, so check free space first.

Install Docker from Docker's own repository rather than the distribution package. The distribution packages are older and use different names, which makes official instructions harder to follow. Remove any conflicting packages only if you installed them earlier and have no containers you need.

On the VPS: check the system, free disk space and any existing Docker
cat /etc/os-release
df -h /
docker --version 2>/dev/null || echo "Docker is not installed"

Ubuntu: add the repository and install

Add Docker's signing key and package source, then install the engine, command-line tool, container runtime, Buildx and the Compose plugin. The source line uses your release codename, so it works for the installed Ubuntu version.

Ubuntu only
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
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
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

AlmaLinux: add the repository and install

AlmaLinux is binary-compatible with RHEL, and Docker's CentOS repository is commonly used on it. Add the repository, install the same packages and start the service. Accept the Docker signing key only if the fingerprint shown matches the one in Docker's documentation.

AlmaLinux only
sudo dnf install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
Ubuntu starts and enables the Docker service automatically after installation. On AlmaLinux the last command does it for you.

Test the installation

Run the small hello-world image with sudo. It downloads, starts, prints a message and exits. Then confirm Compose is available. Docker Compose v2 is a plugin, so the command is docker compose with a space, not docker-compose.

Both systems
sudo docker run --rm hello-world
sudo docker compose version
sudo systemctl is-enabled docker

Decide whether to add your user to the docker group

Members of the docker group can control Docker without sudo, but that is equivalent to root access on the server, because a container can mount any part of the filesystem. Add your user only if that matches who uses this account. Otherwise keep using sudo.

Group membership applies after you log out and in again.

Optional
sudo usermod -aG docker "$USER"
# log out, log in, then:
docker run --rm hello-world

Run a first Compose project

Create a project directory with a compose.yaml file. This example runs the Nginx web server and publishes it on port 8080 of the loopback address only. The 127.0.0.1 prefix matters: it keeps the port private to the server, and you reach it through a reverse proxy or an SSH tunnel.

Create the project
mkdir -p ~/demo && cd ~/demo
cat > compose.yaml <<'EOF'
services:
  web:
    image: nginx:stable
    ports:
      - "127.0.0.1:8080:80"
    restart: unless-stopped
EOF
sudo docker compose up -d
curl -I http://127.0.0.1:8080
Stop and remove it
sudo docker compose down
Recreate the application container during an update. Named volumes hold persistent files and database data. A volume is storage, not a backup; preserve a separate recoverable copy.
Recreate the application container during an update. Named volumes hold persistent files and database data. A volume is storage, not a backup; preserve a separate recoverable copy.

The published-port firewall trap

Docker adds its own firewall rules for published ports. A port published as "8080:80" is reachable from the internet even if UFW or firewalld says it is closed, because Docker's rules are evaluated before them. Publish services as 127.0.0.1:PORT:PORT unless you intend them to be public, and put a reverse proxy in front of anything that should be reachable.

Check what is actually listening after every change. Anything bound to 0.0.0.0 or [::] is reachable from outside unless something else blocks it.

List listeners and running containers
sudo ss -lntp
sudo docker ps --format "table {{.Names}}\t{{.Ports}}"
Images and volumes grow quietly. Check usage with docker system df, and remove unused images with docker image prune once you know what you can rebuild.

Official documentation

These upstream references provide more detail on the commands and workflows in this guide.