johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
#!/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