~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
217 lines · 7.3 KB Raw History
#!/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 <ssh destination> <domain>
#   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.<domain>, 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 <<EOF
$DOMAIN {
	# Pushes through the gateway carry packs.
	request_body {
		max_size 100MB
	}

	# Holds a request through a rollout's second or two with no serve.
	reverse_proxy knotless-serve:2590 {
		lb_try_duration 20s
		lb_try_interval 250ms
	}

	encode gzip zstd
}

www.$DOMAIN {
	redir https://$DOMAIN{uri} permanent
}
EOF
# Repositories' sites: the domain and each owner's label under it, on one
# certificate from the DNS challenge, as a wildcard's must be, by a Caddy
# built with the module that answers it. Without them, stock Caddy, and
# nothing of theirs left but the token.
CADDY="image: caddy:2"
if [ -n "$PAGES" ]; then
  [ -f /etc/knotless-proxy/cloudflare.env ] || {
    echo "$PAGES needs KNOTLESS_PAGES_DNS_TOKEN once, for its certificate" >&2
    exit 2
  }
  cat >>/etc/knotless-proxy/Caddyfile <<EOF

$PAGES, *.$PAGES, *.private.$PAGES {
	tls {
		dns cloudflare {env.CLOUDFLARE_API_TOKEN}
	}

	reverse_proxy knotless-serve:2590 {
		lb_try_duration 20s
		lb_try_interval 250ms
	}

	encode gzip zstd
}
EOF
  cat >/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 <<EOF
name: knotless-proxy
services:
  caddy:
    $CADDY
    restart: always
    ports: ["80:80", "443:443", "443:443/udp"]
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks: [knotless]
volumes:
  caddy-data:
  caddy-config:
networks:
  knotless:
    external: true
EOF
# Each from /dev/null: this script is their stdin, which they would read.
# --build makes Caddy with the module once, and after from its cache, so
# a run that changes nothing recreates nothing.
docker compose -f /etc/knotless-proxy/docker-compose.yml up -d --build --quiet-pull </dev/null 2>&1 | tail -1
docker compose -f /etc/knotless-proxy/docker-compose.yml exec -T caddy \
  caddy reload --config /etc/caddy/Caddyfile </dev/null >/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