MHOSTMHOST

Docker and Docker Compose

Docker runs applications in isolated containers — no manual dependency installs, no version conflicts on the host. This article covers installing Docker Engine and the Docker Compose plugin on an mHost VPS, running your first container, and walking through a docker-compose.yml example with two services.

Docker is containers, not virtualization

Docker containerizes applications at the OS level: every container on a server shares the same host Linux kernel and is isolated from the others by ordinary kernel mechanisms (namespaces, cgroups) — no hypervisor, no guest operating systems inside. That's exactly why containers are lightweight and start fast: a container isn't a "tiny virtual machine," it's a regular process isolated on the same kernel.

Important: mHost virtual servers have nested virtualization disabled — you can't run your own hypervisor, a nested VM, or a software emulator (KVM and the like) inside the VPS. Docker is not nested virtualization and doesn't use hardware virtualization extensions — it works through kernel-level containers, so it installs and runs just fine on any mHost VPS with no extra configuration needed.

What you'll need

  • An mHost VPS running Ubuntu 22.04/24.04, Debian 11/12, or AlmaLinux/Rocky Linux 8/9.
  • SSH access as root or a user with sudo rights.
  • The server's public IP — you'll need it to check the container from a browser.

Step 1. Install Docker Engine and the Docker Compose plugin

There's no ready-made VMmanager 6 recipe for Docker (unlike, say, LEMP or Django — see the full list in "Ready-made VMmanager 6 recipes") — you install it manually, using either method below.

We'll install docker-ce — Docker's own official build — rather than docker.io from the distro's repository (which is usually an older version). The Compose plugin (docker-compose-plugin) is installed with the same package manager and adds the docker compose command (two words, no hyphen) — the modern replacement for the old standalone docker-compose binary (with a hyphen), which is no longer developed.

Official repository: Ubuntu and Debian

If Docker was already installed on this server from the distro's own repository, remove the old packages first:

bash
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1)
Tip: if none of these packages are installed, the command just prints a harmless error — that's expected on a clean server, carry on.

Set up Docker's official repository and install the packages:

bash
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

sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Tip: the commands are the same on Debian — just replace ubuntu with debian in two places: the GPG key URL (.../linux/ubuntu/gpg) and the URIs: line of the repository file.

Official repository: AlmaLinux and Rocky Linux

Both distros are RHEL-compatible, so Docker's official RHEL repository works:

bash
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker

dnf may ask you to confirm the repository's GPG key — check the fingerprint it shows against the one listed on the official docs.docker.com/engine/install/rhel/ page before accepting.

Verify: if dnf can't find packages via the .../linux/rhel/docker-ce.repo repository on your AlmaLinux/Rocky version, try the CentOS path instead — https://download.docker.com/linux/centos/docker-ce.repo (also official, for RHEL-compatible systems).

Quick method: the get.docker.com script

For testing or development, you can install Docker with a single command — a universal script that detects your distro automatically:

bash
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
Important: the get.docker.com script isn't recommended for production — it only installs the latest stable release with no way to pin a version, it isn't designed to upgrade an existing Docker install, and it doesn't account for custom environment setups. Use the official repository above for a production server. It's worth reviewing scripts from the internet before running them — you can run sudo sh get-docker.sh --dry-run first to see what it would do without actually doing it.

Step 2. Verify the installation

bash
sudo systemctl status docker
sudo docker run hello-world

If everything is working, docker run hello-world reports that the Docker client successfully contacted the daemon, pulled the test image, and ran it.

Done: Docker Engine and Docker Compose are installed and running.

Running without sudo (optional)

By default, only root (or a user going through sudo) can manage Docker — the daemon runs as root, and the /var/run/docker.sock socket is only accessible to root and the docker group. To run docker commands as a regular user without sudo:

bash
sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
docker run hello-world

(if the docker group already exists — which is usually the case after installing docker-ce via apt/dnf — groupadd will just say the group already exists; that's not an error)

Important: the docker group is effectively equivalent to root access — any user in it can mount the host's root filesystem inside a container. Only add people you'd trust with admin access to the server.

For consistency, the rest of this article shows docker/docker compose commands with sudo — if you set up passwordless access as above, you can drop it.

Step 3. Run your first container

Start Nginx in the background with port 80 published:

bash
sudo docker run -d --name web --restart unless-stopped -p 80:80 nginx:alpine
  • -d — run in the background (detached).
  • --name web — name the container so you don't have to remember a random ID.
  • --restart unless-stopped — restart the container automatically, including after a server reboot, until you stop it yourself.
  • -p 80:80 — publish host port 80 to port 80 inside the container.

Check that the container is running, then open the server's IP in a browser:

bash
sudo docker ps
curl -I http://localhost
Important: port 80 needs to be open in the firewall, or the server won't be reachable from outside. Check it and open the port if needed — see "Setting up the firewall: ufw".

Once you're done with this container, stop and remove it — otherwise it will hold port 80, which you'll need in the next step:

bash
sudo docker rm -f web

Step 4. docker-compose.yml in practice

Compose describes several containers in a single file, instead of typing out docker run with a dozen flags for each service by hand.

Create a project directory and a file for the static page:

bash
mkdir -p ~/myapp/html && cd ~/myapp
echo "<h1>Hello from Docker Compose</h1>" > html/index.html

Create the docker-compose.yml file:

bash
nano docker-compose.yml
yaml
services:
  web:
    image: nginx:alpine
    container_name: web
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    depends_on:
      - db

  db:
    image: postgres:16
    container_name: db
    restart: unless-stopped
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: change_me
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

What's going on here:

  • services — the project's containers: web (Nginx) and db (PostgreSQL).
  • image — which image to use; restart — the restart policy, same as in docker run.
  • ports — host:container port publishing, same as the -p flag.
  • volumes — for web, this is a bind mount of the ./html directory from the host into the container, read-only (:ro); for db, it's the named volume db_data, where PostgreSQL keeps its data independent of the container's lifecycle.
  • environment — environment variables inside the container.
  • depends_on — Compose starts db before web.
Important: change POSTGRES_PASSWORD: change_me to your own password before using this file on a real server.

Start the project:

bash
sudo docker compose up -d
sudo docker compose ps
Important: if the web container from step 3 is still running, port 80 is already taken — remove it (sudo docker rm -f web) before starting Compose. And make sure port 80 is open in the firewall — see "Setting up the firewall: ufw".

View the logs and stop the project:

bash
sudo docker compose logs -f
sudo docker compose down
Tip: docker compose down stops and removes the containers and network, but the named volume db_data stays — the PostgreSQL data won't be lost by the next docker compose up -d. To remove the data too, use docker compose down -v — that's irreversible.

Common commands

Docker

  • docker pull <image> — download an image without running it.
  • docker images — list downloaded images.
  • docker build -t <name> . — build an image from a Dockerfile in the current directory.
  • docker ps — list running containers.
  • docker ps -a — list all containers, including stopped ones.
  • docker logs -f <container> — follow a container's logs in real time.
  • docker exec -it <container> sh — open a shell inside a running container (on Debian/Ubuntu-based images, bash is usually available instead of sh).
  • docker stop <container> / docker start <container> / docker restart <container> — stop / start / restart.
  • docker rm <container> — remove a stopped container (-f forces it, even if running).
  • docker rmi <image> — remove an image.
  • docker system prune — remove unused containers, images, and networks, freeing up disk space.

Docker Compose

  • docker compose up -d — create and start all the project's services in the background.
  • docker compose ps — status of the current project's services.
  • docker compose logs -f — follow all the project's service logs in real time.
  • docker compose stop / docker compose restart — stop / restart services without removing the containers.
  • docker compose down — stop and remove the project's containers and network (volumes stay).
  • docker compose exec <service> sh — shell inside a specific service's container.
  • docker compose pull — update the services' images to their current versions.

What's next

If you're not set on Docker specifically and just need a ready-made web stack (LAMP/LEMP) or a specific app (Zabbix, Tomcat, and others) — there may already be a one-click recipe for it in VMmanager 6: see "Ready-made VMmanager 6 recipes". If your containers are exposed to the outside, don't forget the firewall: "Setting up the firewall: ufw".