#!/usr/bin/env bash # Makes a fresh Ubuntu 24.04 host knotless's own: serve, pages, the merge # queue's runner and the triager, which deploy.sh puts on it, behind a # Caddy of its own for the domain. # # deploy/setup.sh # KNOTLESS_HOST=root@host KNOTLESS_DOMAIN=git.example.com deploy/setup.sh # # or with those in deploy/local.env. Safe to run again: each step leaves # what is already done as it is. # # What it sets up: # # - docker, from Docker's own apt repository; # - the network the proxy reaches serve over, knotless # (KNOTLESS_PROXY_NETWORK in docker-compose.yml); # - Caddy, in /etc/knotless-proxy, with a certificate for the domain from # HTTP-01 once its records point here, and www redirected to it; # - with KNOTLESS_PAGES_DOMAIN (knotless.page), repositories' sites there # too: Caddy built with the caddy-dns/cloudflare module, holding # certificates for the domain, every name one label under it, and every # private site's one label under private., from the DNS # challenge, with the zone's own token, KNOTLESS_PAGES_DNS_TOKEN # (Zone:Read and DNS:Edit on that zone alone). The token is given once, # kept in /etc/knotless-proxy/cloudflare.env: a run without it keeps the # one there; # - a firewall letting in SSH, HTTP and HTTPS alone; # - unattended security upgrades, and a daily prune of images unused for # a week and of build cache past 20 GB; # - a 4 GB swap file, used only under pressure (swappiness 10), so a spike # slows the host rather than stalling it as it drops and rereads its # programs from disk; # - KNOTLESS_DATA (/mnt/data/knotless) and the runner's state # (/var/lib/knotless-run). # # Without a host of its own, deploy.sh can join another's proxy instead: # that proxy's network named knotless, and the domain's block in its # Caddyfile, as in README.md. set -euo pipefail cd "$(dirname "$0")/.." [ -f deploy/local.env ] && { set -a; . deploy/local.env; set +a; } DEST="${1:-${KNOTLESS_HOST:-}}" DOMAIN="${2:-${KNOTLESS_DOMAIN:-}}" [ -n "$DEST" ] && [ -n "$DOMAIN" ] || { sed -n '6,7p' "$0" >&2; exit 2; } DATA="${KNOTLESS_DATA:-/mnt/data/knotless}" PAGES="${KNOTLESS_PAGES_DOMAIN:-}" # The zone's token, over stdin rather than the command line, which the # host's process list would show: written, as it is, whenever given. if [ -n "$PAGES" ] && [ -n "${KNOTLESS_PAGES_DNS_TOKEN:-}" ]; then printf 'CLOUDFLARE_API_TOKEN=%s\n' "$KNOTLESS_PAGES_DNS_TOKEN" | ssh -o StrictHostKeyChecking=accept-new "$DEST" \ 'install -d -m 0755 /etc/knotless-proxy && umask 077 && cat >/etc/knotless-proxy/cloudflare.env' fi ssh -o StrictHostKeyChecking=accept-new "$DEST" \ DOMAIN="$DOMAIN" DATA="$DATA" PAGES="$PAGES" bash -s <<'HOST' set -euo pipefail export DEBIAN_FRONTEND=noninteractive echo "==> packages" apt-get update -q apt-get install -yq ca-certificates curl gnupg ufw unattended-upgrades >/dev/null install -d -m 0755 /etc/apt/keyrings echo "==> docker" if ! command -v docker >/dev/null; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc . /etc/os-release echo "deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $VERSION_CODENAME stable" \ >/etc/apt/sources.list.d/docker.list apt-get update -q apt-get install -yq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null fi docker network inspect knotless >/dev/null 2>&1 || docker network create knotless >/dev/null echo "==> proxy" install -d -m 0755 /etc/knotless-proxy cat >/etc/knotless-proxy/Caddyfile <&2 exit 2 } cat >>/etc/knotless-proxy/Caddyfile </etc/knotless-proxy/Dockerfile <<'EOF' FROM caddy:2-builder AS build RUN xcaddy build --with github.com/caddy-dns/cloudflare FROM caddy:2 COPY --from=build /usr/bin/caddy /usr/bin/caddy EOF CADDY="build: . image: knotless-proxy-caddy env_file: [cloudflare.env]" else rm -f /etc/knotless-proxy/Dockerfile fi cat >/etc/knotless-proxy/docker-compose.yml <&1 | tail -1 docker compose -f /etc/knotless-proxy/docker-compose.yml exec -T caddy \ caddy reload --config /etc/caddy/Caddyfile /dev/null 2>&1 || true echo "==> firewall" ufw default deny incoming >/dev/null ufw default allow outgoing >/dev/null ufw allow OpenSSH >/dev/null ufw allow 80,443/tcp >/dev/null ufw allow 443/udp >/dev/null ufw --force enable >/dev/null echo "==> logs kept 30 days, as the privacy policy says" install -d /etc/systemd/journald.conf.d cat >/etc/systemd/journald.conf.d/knotless.conf <<'EOF' [Journal] MaxRetentionSec=30day EOF systemctl restart systemd-journald echo "==> swap" if ! swapon --show=NAME --noheadings | grep -qx /swapfile; then [ -f /swapfile ] || { fallocate -l 4G /swapfile; chmod 600 /swapfile; mkswap -q /swapfile; } swapon /swapfile fi grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >>/etc/fstab echo vm.swappiness=10 >/etc/sysctl.d/90-knotless-swap.conf sysctl -q -p /etc/sysctl.d/90-knotless-swap.conf echo "==> upgrades and pruning" dpkg-reconfigure -f noninteractive unattended-upgrades cat >/etc/systemd/system/knotless-prune.service <<'EOF' [Unit] Description=Removes images no container has used for a week, and build cache past 20 GB [Service] Type=oneshot ExecStart=/usr/bin/docker image prune -af --filter until=168h ExecStart=/usr/bin/docker builder prune -f --max-used-space 20GB EOF cat >/etc/systemd/system/knotless-prune.timer <<'EOF' [Unit] Description=Daily image prune [Timer] OnCalendar=daily RandomizedDelaySec=1h Persistent=true [Install] WantedBy=timers.target EOF systemctl daemon-reload systemctl enable --now knotless-prune.timer >/dev/null echo "==> data" install -d -m 0700 /etc/knotless /var/lib/knotless-run install -d -m 0755 "$DATA" echo "done: point $DOMAIN here, then deploy.sh" HOST