Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

bootstrap-hetzner.sh

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

bootstrap-hetzner.shBlame297 lines · 1 contributor
fc1e337Claude1#!/usr/bin/env bash
2# =============================================================================
3# Gluecron one-shot Hetzner bootstrap.
4#
5# Run ONCE on a fresh (or partially-set-up) Hetzner box, via the web console,
6# as root. Idempotent: safe to re-run. Won't touch existing crontech systemd
7# units, will append gluecron's entries instead.
8#
9# Usage (paste this ONE line in Hetzner Console as root):
10# curl -sSL https://raw.githubusercontent.com/ccantynz-alt/Gluecron.com/main/scripts/bootstrap-hetzner.sh \
11# | bash -s -- "ssh-ed25519 AAAA... your@laptop"
12#
13# What it does (every step is idempotent + verbose):
14# 1. Authorise the supplied SSH pubkey on root@ (so you can SSH from laptop)
15# 2. Re-enable PasswordAuthentication as a safety fallback
16# 3. Install Bun, Postgres, Caddy, git if missing
17# 4. Create local Postgres DB + user 'gluecron' if missing
18# 5. Clone /opt/gluecron from main if missing, or git pull if present
19# 6. Write /etc/gluecron.env with DATABASE_URL pointing at local Postgres
20# 7. Bun install + run migrations
21# 8. Write systemd unit for gluecron on port 3010
22# 9. Append gluecron + www.gluecron blocks to Caddyfile (if missing)
23# 10. Reload Caddy + start gluecron
24# 11. Smoke test /healthz
25# =============================================================================
26
27set -euo pipefail
28PUBKEY="${1:-}"
29PORT="${PORT:-3010}"
30SITE_DOMAIN="${SITE_DOMAIN:-gluecron.com}"
31DB_USER="gluecron"
32DB_NAME="gluecron"
33DB_PASS="$(openssl rand -hex 16)"
34
35say() { echo ""; echo "==> $*"; }
36ok() { echo " ✓ $*"; }
37warn() { echo " ⚠ $*"; }
38
39# ────────────────────────────────────────────────────────────────────────────
40# 1. Authorise SSH pubkey
41# ────────────────────────────────────────────────────────────────────────────
42say "[1/11] Authorising SSH key on root@"
43mkdir -p /root/.ssh
44chmod 700 /root/.ssh
45touch /root/.ssh/authorized_keys
46chmod 600 /root/.ssh/authorized_keys
47if [ -n "$PUBKEY" ]; then
48 if grep -qF "$PUBKEY" /root/.ssh/authorized_keys 2>/dev/null; then
49 ok "SSH key already authorised"
50 else
51 echo "$PUBKEY" >> /root/.ssh/authorized_keys
52 ok "SSH key added"
53 fi
54else
55 warn "No PUBKEY argument — skipping SSH key install"
56fi
57
58# ────────────────────────────────────────────────────────────────────────────
59# 2. Re-enable PasswordAuthentication (safety fallback)
60# ────────────────────────────────────────────────────────────────────────────
61say "[2/11] Configuring sshd (allow both key + password as fallback)"
62sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
63sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
64sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
65mkdir -p /etc/ssh/sshd_config.d
66cat > /etc/ssh/sshd_config.d/99-gluecron-auth.conf <<EOF
67PasswordAuthentication yes
68PubkeyAuthentication yes
69PermitRootLogin yes
70EOF
71systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null || true
72ok "sshd reloaded — both auth methods enabled"
73
74# ────────────────────────────────────────────────────────────────────────────
75# 3. Install dependencies if missing
76# ────────────────────────────────────────────────────────────────────────────
77say "[3/11] Ensuring git, curl, postgresql, caddy, bun are installed"
78export DEBIAN_FRONTEND=noninteractive
79apt-get update -qq
80
ee80a15Claude81for pkg in git curl ca-certificates gnupg debian-keyring debian-archive-keyring apt-transport-https openssl unzip xz-utils tar; do
fc1e337Claude82 dpkg -l | grep -qw "$pkg" || apt-get install -y -qq "$pkg"
83done
84ok "base packages installed"
85
86# Postgres
87if ! command -v psql >/dev/null 2>&1; then
88 apt-get install -y -qq postgresql postgresql-contrib
89 systemctl enable --now postgresql
90 ok "postgres installed + started"
91else
92 ok "postgres already installed"
93fi
94
95# Caddy (skip install if already there — likely from crontech setup)
96if ! command -v caddy >/dev/null 2>&1; then
97 curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
98 curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
99 apt-get update -qq
100 apt-get install -y -qq caddy
101 systemctl enable --now caddy
102 ok "caddy installed + started"
103else
104 ok "caddy already installed (likely from crontech)"
105fi
106
107# Bun
108if [ ! -x /root/.bun/bin/bun ]; then
109 curl -fsSL https://bun.sh/install | bash >/dev/null
110 ok "bun installed"
111else
112 ok "bun already installed"
113fi
114export PATH="/root/.bun/bin:$PATH"
115ok "bun version: $(/root/.bun/bin/bun --version 2>/dev/null || echo unknown)"
116
117# ────────────────────────────────────────────────────────────────────────────
118# 4. Local Postgres DB + user
119# ────────────────────────────────────────────────────────────────────────────
120say "[4/11] Creating local Postgres DB + user (idempotent)"
121if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -q 1; then
122 ok "postgres user '${DB_USER}' already exists"
123else
124 sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';"
125 ok "postgres user '${DB_USER}' created"
126fi
127if sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then
128 ok "postgres database '${DB_NAME}' already exists"
129else
130 sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};"
131 ok "postgres database '${DB_NAME}' created"
132fi
133
134# Read existing DB password if env file already has one (don't overwrite)
135EXISTING_DB_URL=""
136if [ -f /etc/gluecron.env ] && grep -q '^DATABASE_URL=' /etc/gluecron.env; then
137 EXISTING_DB_URL=$(grep '^DATABASE_URL=' /etc/gluecron.env | cut -d= -f2- | tr -d '"')
138 ok "reusing existing DATABASE_URL from /etc/gluecron.env"
139fi
140DATABASE_URL="${EXISTING_DB_URL:-postgresql://${DB_USER}:${DB_PASS}@127.0.0.1:5432/${DB_NAME}}"
141
142# If we're using a NEW password we just generated, sync it to the DB user
143if [ -z "$EXISTING_DB_URL" ]; then
144 sudo -u postgres psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}';"
145fi
146
147# ────────────────────────────────────────────────────────────────────────────
148# 5. Clone or update /opt/gluecron
149# ────────────────────────────────────────────────────────────────────────────
150say "[5/11] Setting up /opt/gluecron source tree"
151mkdir -p /opt
152if [ ! -d /opt/gluecron/.git ]; then
153 git clone https://github.com/ccantynz-alt/Gluecron.com.git /opt/gluecron
154 ok "cloned gluecron"
155else
156 cd /opt/gluecron
157 git fetch --prune origin main
158 git reset --hard origin/main
159 ok "pulled latest main"
160fi
161cd /opt/gluecron
162
163# ────────────────────────────────────────────────────────────────────────────
164# 6. Write /etc/gluecron.env (preserve existing where present)
165# ────────────────────────────────────────────────────────────────────────────
166say "[6/11] Writing /etc/gluecron.env"
167mkdir -p /data/repos
168chmod 755 /data/repos
169umask 077
170cat > /etc/gluecron.env <<EOF
171DATABASE_URL=${DATABASE_URL}
172APP_BASE_URL=https://${SITE_DOMAIN}
173SITE_ADMIN_USERNAME=${SITE_ADMIN_USERNAME:-ccantynz}
174GIT_REPOS_PATH=/data/repos
175PORT=${PORT}
176DEMO_SEED_ON_BOOT=1
177EMAIL_PROVIDER=log
178EMAIL_FROM="gluecron <no-reply@${SITE_DOMAIN}>"
179NODE_ENV=production
180EOF
181chmod 600 /etc/gluecron.env
182umask 022
183ok "/etc/gluecron.env written (chmod 600)"
184
185# ────────────────────────────────────────────────────────────────────────────
186# 7. Bun install + run migrations
187# ────────────────────────────────────────────────────────────────────────────
188say "[7/11] bun install + db:migrate"
189/root/.bun/bin/bun install --frozen-lockfile
190set -a; source /etc/gluecron.env; set +a
191/root/.bun/bin/bun run src/db/migrate.ts || warn "migrations command failed — may already be applied"
192ok "deps + migrations done"
193
194# ────────────────────────────────────────────────────────────────────────────
195# 8. systemd unit for gluecron
196# ────────────────────────────────────────────────────────────────────────────
197say "[8/11] Writing /etc/systemd/system/gluecron.service"
198BUN_BIN=/root/.bun/bin/bun
199cat > /etc/systemd/system/gluecron.service <<EOF
200[Unit]
201Description=Gluecron — AI-native code intelligence platform
202After=network-online.target postgresql.service
203Wants=network-online.target
204
205[Service]
206Type=simple
207User=root
208WorkingDirectory=/opt/gluecron
209EnvironmentFile=/etc/gluecron.env
210ExecStart=${BUN_BIN} run src/index.ts
211Restart=always
212RestartSec=5
213StandardOutput=journal
214StandardError=journal
215SyslogIdentifier=gluecron
216LimitNOFILE=65536
217
218[Install]
219WantedBy=multi-user.target
220EOF
221systemctl daemon-reload
222systemctl enable gluecron >/dev/null
223systemctl restart gluecron
224ok "gluecron systemd unit installed + started"
225
226# ────────────────────────────────────────────────────────────────────────────
227# 9. Append gluecron + www.gluecron to Caddyfile if missing
228# ────────────────────────────────────────────────────────────────────────────
229say "[9/11] Caddy reverse-proxy config for ${SITE_DOMAIN}"
230CADDYFILE=/etc/caddy/Caddyfile
231touch "$CADDYFILE"
232if grep -qE "^${SITE_DOMAIN}\s*\{" "$CADDYFILE"; then
233 ok "${SITE_DOMAIN} block already in Caddyfile — leaving as-is"
234else
235 cat >> "$CADDYFILE" <<EOF
236
237${SITE_DOMAIN} {
238 encode zstd gzip
239 reverse_proxy localhost:${PORT}
240}
241
242www.${SITE_DOMAIN} {
243 redir https://${SITE_DOMAIN}{uri} permanent
244}
245EOF
246 ok "appended ${SITE_DOMAIN} block to Caddyfile"
247fi
248
249if caddy validate --config "$CADDYFILE" --adapter caddyfile 2>&1 | grep -qi "valid"; then
250 systemctl reload caddy || systemctl restart caddy
251 ok "caddy reloaded"
252else
253 warn "caddy validate warned — check: caddy validate --config $CADDYFILE"
254fi
255
256# ────────────────────────────────────────────────────────────────────────────
257# 10. Wait + smoke test
258# ────────────────────────────────────────────────────────────────────────────
259say "[10/11] Waiting for /healthz to come up (60s)"
260for i in $(seq 1 12); do
261 code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/healthz" || echo 000)
262 if [ "$code" = "200" ]; then
263 ok "gluecron is live on http://localhost:${PORT} (attempt $i)"
264 break
265 fi
266 sleep 5
267done
268if [ "$code" != "200" ]; then
269 warn "healthz did not respond 200 after 60s. Logs: journalctl -u gluecron -n 50 --no-pager"
270fi
271
272# ────────────────────────────────────────────────────────────────────────────
273# 11. Summary
274# ────────────────────────────────────────────────────────────────────────────
275say "[11/11] DONE"
276echo ""
277echo "============================================================"
278echo " GLUECRON BOOTSTRAP COMPLETE"
279echo "============================================================"
280echo " systemd unit: gluecron.service"
281echo " port: ${PORT}"
282echo " env file: /etc/gluecron.env (chmod 600)"
283echo " bare repos: /data/repos"
284echo " source: /opt/gluecron"
285echo ""
286echo " Test from your laptop:"
287echo " ssh root@\$(hostname -I | awk '{print \$1}') 'systemctl status gluecron --no-pager | head -5'"
288echo ""
289echo " Public URL (once DNS is correct + Caddy issued cert):"
290echo " https://${SITE_DOMAIN}"
291echo " https://${SITE_DOMAIN}/api/version"
292echo ""
293echo " Useful commands:"
294echo " systemctl status gluecron"
295echo " journalctl -u gluecron -f"
296echo " bash /opt/gluecron/scripts/bootstrap-hetzner.sh # safe to re-run"
297echo "============================================================"