Setup recipe: a Docker host

A ready-made first-boot script that installs Docker Engine and Compose from Docker's repository, caps container logs, and lets your user run docker.

This is a setup script that turns a new server into a Docker host on its first boot. It works on Ubuntu, Debian, AlmaLinux, Rocky Linux and Fedora.

It does three things:

  • Installs Docker Engine with the Compose and Buildx plugins from Docker's own repository, the same way as the Docker one-click app. On a release Docker hasn't published packages for yet, Ubuntu and Debian get the distribution's docker.io and Fedora gets moby-engine; on AlmaLinux and Rocky Linux it stops with a message instead.
  • Caps each container's log at three files of 10 MB, in /etc/docker/daemon.json, so a chatty container can't fill the disk. If that file already exists, it's left alone.
  • Adds the user deploy to the docker group, if that user exists. The secure baseline recipe creates it; run that one first, or change DOCKER_USER.

Being in the docker group is the same as having root on the server: only add people you'd give root to.

Deploy it

Deploy with VPSNine: this opens the order form with the script in the setup script field (you log in first). Edit the settings at the top if you need to, then order.

A setup script field holds one script. To use this together with the secure baseline, paste this script's lines after the baseline's in the same field, or run one of them by hand after the first login. With the CLI:

bash
vps9 servers create --plan standard --image ubuntu-24.04 --hostname docker-1 --key key_… --setup-script ./docker-host.sh

The script

bash
#!/bin/bash
# VPSNine setup recipe: Docker host
# https://vpsnine.com/help/docker-host-setup-script
# Docker Engine with the Compose and Buildx plugins from Docker's own repository, container logs
# capped in size, and a user who can run docker without sudo.
set -euo pipefail
umask 022

# ---- settings: change these before you run it ----
DOCKER_USER=deploy   # added to the docker group if this user exists (the secure baseline makes it)

# Which family of distributions this is: Debian and Ubuntu, or AlmaLinux, Rocky Linux and Fedora.
. /etc/os-release
case " ${ID:-} ${ID_LIKE:-} " in
  *" debian "*|*" ubuntu "*) family=debian ;;
  *" rhel "*|*" fedora "*|*" centos "*) family=rhel ;;
  *) echo "This recipe supports Ubuntu, Debian, AlmaLinux, Rocky Linux and Fedora, not ${PRETTY_NAME:-this system}" >&2; exit 1 ;;
esac

echo "== Installing Docker"
if [ "$family" = debian ]; then
export DEBIAN_FRONTEND=noninteractive
apt-get -o DPkg::Lock::Timeout=300 update
apt-get -o DPkg::Lock::Timeout=300 install -y ca-certificates curl
. /etc/os-release
if ! command -v docker >/dev/null 2>&1; then
  if curl -fsSL -o /dev/null "https://download.docker.com/linux/${ID}/dists/${VERSION_CODENAME}/Release"; then
    install -m 0755 -d /etc/apt/keyrings
    curl -fsSL "https://download.docker.com/linux/${ID}/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/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
    apt-get -o DPkg::Lock::Timeout=300 update
    apt-get -o DPkg::Lock::Timeout=300 install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  else
    echo "Docker's repository has no ${VERSION_CODENAME} yet: installing the distribution's docker.io"
    apt-get -o DPkg::Lock::Timeout=300 update
    apt-get -o DPkg::Lock::Timeout=300 install -y docker.io
    apt-get -o DPkg::Lock::Timeout=300 install -y docker-compose-v2 || apt-get -o DPkg::Lock::Timeout=300 install -y docker-compose || true
  fi
fi
systemctl enable --now docker
else
. /etc/os-release
if ! command -v docker >/dev/null 2>&1; then
  # Docker publishes a Fedora repository and a CentOS one, which AlmaLinux and Rocky Linux use (checked 2026-10-16:
  # centos/10 exists, so AlmaLinux 10 works). A release it doesn't carry yet: Fedora's own moby-engine, else stop.
  if [ "$ID" = fedora ]; then repo=fedora; rel=${VERSION_ID}; else repo=centos; rel=${VERSION_ID%%.*}; fi
  if curl -fsSL -o /dev/null "https://download.docker.com/linux/${repo}/${rel}/$(uname -m)/stable/repodata/repomd.xml"; then
    curl -fsSL "https://download.docker.com/linux/${repo}/docker-ce.repo" -o /etc/yum.repos.d/docker-ce.repo
    dnf -y remove podman-docker buildah runc >/dev/null 2>&1 || true
    dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  elif [ "$ID" = fedora ]; then
    echo "Docker's repository has no Fedora ${rel} yet: installing Fedora's moby-engine"
    dnf -y install moby-engine docker-compose
  else
    echo "Docker's repository has no ${PRETTY_NAME} yet" >&2
    exit 1
  fi
fi
systemctl enable --now docker
fi

echo "== Capping container logs at 3 files of 10 MB each"
# Only when there is no daemon.json yet, so a rerun never overwrites your own settings.
if [ ! -f /etc/docker/daemon.json ]; then
  install -d -m 0755 /etc/docker
  cat > /etc/docker/daemon.json <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
EOF
  systemctl restart docker
fi

if id "$DOCKER_USER" >/dev/null 2>&1; then
  echo "== Adding $DOCKER_USER to the docker group (log in again for it to apply)"
  usermod -aG docker "$DOCKER_USER"
else
  echo "== No user $DOCKER_USER: skipped the docker group. Run usermod -aG docker <user> later."
fi
docker version
echo "== Done"

Check the result

bash
tail -n 20 /var/log/vps9-setup.log
cat /var/lib/vps9-setup.exit
docker run --rm hello-world
docker compose version

deploy needs to log in again before the group applies. After that, docker ps works without sudo.

Ports and the firewall

Docker writes its own firewall rules for published ports, and those go around ufw. A container started with -p 8080:80 is reachable from the internet even when ufw says port 8080 is closed. Publish on localhost (-p 127.0.0.1:8080:80) and put a reverse proxy such as the Caddy recipe in front, or block the port in the server firewall, which filters before traffic reaches the server. Install Docker has more on running containers.

Be first when orders open

Join the waitlist and we'll send you one email when VPSNine launches. Pick a plan if you already know which one you want, and we'll size the first servers around it.

  • One email, on launch day
  • Monthly or yearly, no lock-in
  • 14-day money-back guarantee