#!/bin/sh # Joins the box it runs on to the fleet. A person runs it from the line `./fleet invite ` prints, # a cloud box from cloud-init at first boot. Its arguments are the box's machine name in fleet-config.yaml # and a Tailscale key that works once and dies within the hour. Every box joins the tailnet under its # name with MagicDNS off, keeps root trusting the fleet's root keys (fleet-root-keys.timer), and gets # Docker with live-restore and Coolify's network on its fixed subnet. # `--control` adds Tailscale SSH, the fleet network, the firewall admitting only that network to the # metadata address, and Coolify at COOLIFY_VERSION, from that release's own files with auto-update off. # `--sync-root-keys` alone sets up the root keys on a box joined before they were synced. Every step is # idempotent. POSIX sh, since the pasted line runs it as `sh` on a box with nothing installed yet. set -eu COOLIFY_VERSION=4.3.23 # Coolify's installer and upgrade script download compose files from Coolify's CDN, which serves only # the newest release. Both run here against the pinned release's files, mirrored in the CDN's layout. COOLIFY_RELEASE_URL=https://raw.githubusercontent.com/coollabsio/coolify/v$COOLIFY_VERSION COOLIFY_RELEASE_MIRROR=/data/coolify/release-mirror COOLIFY_CDN_LINE='CDN="https://cdn.coollabs.io/coolify"' FLEET_NETWORK=fleet FLEET_SUBNET=10.255.0.0/24 # Coolify's network, the one its Traefik publishes ports on, made here before Coolify would make it from # the pool: cloudflared on the host reaches Traefik through docker-proxy from this network's gateway, # the one address Traefik trusts forwarded headers from (fleetcli/apply/proxy.py). deepdish's, as found. ROOT_KEYS_SYNC=/usr/local/sbin/fleet-root-keys COOLIFY_NETWORK=coolify COOLIFY_SUBNET=10.0.1.0/24 COOLIFY_GATEWAY=10.0.1.1 METADATA_ADDRESS=169.254.169.254 # Where Docker takes a new network's subnet from: outside the fleet VPC (10.20.0.0/16), FLEET_SUBNET, # COOLIFY_SUBNET and the tailnet (100.64.0.0/10). Coolify's installer keeps a daemon.json that names a pool. DOCKER_ADDRESS_POOL=172.16.0.0/12 DOCKER_PACKAGES="docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin" fail() { echo "join.sh: $1" >&2 exit 1 } apt_install() { for package in "$@"; do if ! dpkg -s "$package" >/dev/null 2>&1; then apt-get update -q apt-get install -y -q "$@" return fi done } write_daemon_json() { mkdir -p /etc/docker [ -f /etc/docker/daemon.json ] || echo '{}' >/etc/docker/daemon.json jq --arg pool "$DOCKER_ADDRESS_POOL" '{ "log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "3"}, "default-address-pools": [{"base": $pool, "size": 24}] } * . | .["live-restore"] = true' /etc/docker/daemon.json >/etc/docker/daemon.json.join mv /etc/docker/daemon.json.join /etc/docker/daemon.json } add_docker_repository() { os=$(sed -n 's/^ID=//p' /etc/os-release) codename=$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release) case $os in ubuntu | debian) ;; *) fail "Docker's apt repository covers Ubuntu and Debian; this box is $os" ;; esac install -m 0755 -d /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/$os/gpg" -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc cat >/etc/apt/sources.list.d/docker.sources <"$ROOT_KEYS_SYNC" <<'EOF' #!/bin/sh # Makes root's fleet-root lines in authorized_keys the keys at ROOT_KEYS_URL, which ./fleet root-key writes: # the current key and, once it has rotated, the one before it. Each may log in from the tailnet and the box # itself alone, so a key that leaks opens nothing from the internet. A fetch that fails, or a list that is # not one or two of the fleet's ed25519 keys, changes nothing. Written by join.sh; fleet-root-keys.timer # runs it at boot and every five minutes. set -eu ROOT_KEYS_URL=https://fleet-root-keys-databob-labs.s3.us-east-2.amazonaws.com/fleet-root-keys.pub FROM=100.64.0.0/10,127.0.0.1,::1 AUTHORIZED=/root/.ssh/authorized_keys refuse() { echo "fleet-root-keys: $1; root's keys are unchanged" >&2 exit 1 } keys=$(curl -fsSL --max-time 30 "$ROOT_KEYS_URL") || refuse "cannot fetch $ROOT_KEYS_URL" count=$(printf '%s\n' "$keys" | grep -c . || true) if [ "$count" -lt 1 ] || [ "$count" -gt 2 ]; then refuse "$ROOT_KEYS_URL holds $count keys, not one or two" fi if printf '%s\n' "$keys" | grep -Evxq 'ssh-ed25519 [A-Za-z0-9+/]+={0,2} fleet-root'; then refuse "$ROOT_KEYS_URL holds a line that is not an ssh-ed25519 key named fleet-root" fi install -d -m 700 /root/.ssh touch "$AUTHORIZED" { grep -v ' fleet-root$' "$AUTHORIZED" || true printf '%s\n' "$keys" | sed "s|^|from=\"$FROM\" |" } >"$AUTHORIZED.fleet" chmod 600 "$AUTHORIZED.fleet" if cmp -s "$AUTHORIZED.fleet" "$AUTHORIZED"; then rm "$AUTHORIZED.fleet" else mv "$AUTHORIZED.fleet" "$AUTHORIZED" fi EOF chmod 755 "$ROOT_KEYS_SYNC" cat >/etc/systemd/system/fleet-root-keys.service </etc/systemd/system/fleet-root-keys.timer </dev/null 2>&1; then return fi docker network create "$name" "$@" } install_firewall() { apt_install nftables mkdir -p /etc/fleet cat >/etc/fleet/firewall.nft </etc/systemd/system/fleet-firewall.service </dev/null; then curl -fsSL https://tailscale.com/install.sh | sh fi tailscale up --auth-key="$key" --hostname="$box" --accept-dns=false --ssh="$control" install_root_key_sync apt_install ca-certificates curl jq write_daemon_json if command -v docker >/dev/null; then systemctl reload docker else add_docker_repository # shellcheck disable=SC2086 apt_install $DOCKER_PACKAGES fi create_network "$COOLIFY_NETWORK" --attachable --subnet "$COOLIFY_SUBNET" --gateway "$COOLIFY_GATEWAY" if [ "$control" = true ]; then create_network "$FLEET_NETWORK" --subnet "$FLEET_SUBNET" install_firewall install_coolify fi tailscale status --self --peers=false