Docker and Docker Compose
Docker Engine with the Compose plugin from the official repository, a Compose example, and why Docker bypasses your firewall.
5 min read
On this page
Applies to Debian 12/13, Ubuntu 22.04/24.04, AlmaLinux 9 and Rocky Linux 9. We install Docker from Docker's official repository, not the older docker.io package from the distribution.
Debian
apt remove -y docker.io docker-compose podman-docker containerd runc 2>/dev/null
apt install -y ca-certificates curl
install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
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/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginUbuntu
Same as Debian, with ubuntu in the URLs:
apt remove -y docker.io docker-compose podman-docker containerd runc 2>/dev/null
apt install -y ca-certificates curl
install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
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" > /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginAlmaLinux and Rocky Linux
dnf remove -y podman buildah runc 2>/dev/null
dnf install -y dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now dockerdnf asks about Docker's GPG key. The fingerprint must be 060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35.
On Debian and Ubuntu, Docker starts on its own after installation.
Check that it worked
docker version
docker compose version
docker run --rm hello-worldhello-world pulls a tiny image and prints Hello from Docker!.
A Compose example
A small web app with a database. Create a folder:
mkdir -p /opt/app && cd /opt/app
nano compose.yamlservices:
web:
image: nginx:stable
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- db-data:/var/lib/postgresql/data
secrets:
db_password:
file: ./db_password.txt
volumes:
db-data:mkdir -p html && echo "Hello" > html/index.html
openssl rand -base64 24 > db_password.txt && chmod 600 db_password.txt
docker compose up -dThe important points:
127.0.0.1:8080:80publishes the web server on the server itself only. Nobody can reach it from outside. A reverse proxy such as Caddy or nginx on the host then forwardshttps://your-domainto it.- The database has no
ports:at all. The web container still reaches it by the namedb, because Compose puts both into a shared network. restart: unless-stoppedbrings the containers back after a server reboot.
Useful commands in the folder:
docker compose ps # what's running
docker compose logs -f web # live logs
docker compose pull # fetch new images
docker compose up -d # restart with new images
docker compose down # stop and remove (volumes stay)Test on the server:
curl http://127.0.0.1:8080Docker and the firewall
Docker writes its own rules into the kernel's packet filter. Connections to published ports go past the rules of ufw and firewalld. So ufw deny 8080 doesn't help when a container runs with -p 8080:80.
How to stay safe:
-
Publish on 127.0.0.1 only, as in the example above:
"127.0.0.1:8080:80". Whatever should be reachable from outside (usually just 80 and 443) goes through a reverse proxy on the host or a dedicated proxy container. -
Never publish databases. Containers in the same Compose network don't need
ports:. -
If a port must be public, but only for certain addresses: rules in the
DOCKER-USERchain. Docker evaluates them before its own rules. Example, port 5432 only for198.51.100.7(look up the interface name withip -br link):bashiptables -I DOCKER-USER -i eth0 -p tcp --dport 5432 ! -s 198.51.100.7 -j DROPSuch rules are gone after a reboot unless you persist them, and they only cover IPv4. That's why option 1 is almost always the better way.
Common errors
| Message | Cause and fix |
|---|---|
permission denied while trying to connect to the Docker daemon socket | Not root and not in the docker group. Use sudo docker … or add the group and log in again. |
docker: 'compose' is not a docker command | docker-compose-plugin is missing. The old hyphenated docker-compose command is outdated. |
Bind for 0.0.0.0:80 failed: port is already allocated | Something else uses the port. ss -tlnp shows what. |
Conflict with podman or runc on AlmaLinux/Rocky | Run dnf remove podman buildah runc first, as above. |
no space left on device | Old images and build leftovers: docker system df, clean up with docker system prune. |
What next
- Set up a firewall for SSH and your reverse proxy's ports.
- Add swap if many containers fill up the RAM.
- Rather not run your own server? CitexNode Deploy builds and runs web apps from Git.
- Help: ticket.