#!/usr/bin/env bash
set -euo pipefail
PUBKEY="${1:-}"
PORT="${PORT:-3010}"
SITE_DOMAIN="${SITE_DOMAIN:-gluecron.com}"
DB_USER="gluecron"
DB_NAME="gluecron"
DB_PASS="$(openssl rand -hex 16)"
say() { echo ""; echo "==> $*"; }
ok() { echo " ✓ $*"; }
warn() { echo " ⚠ $*"; }
say "[1/11] Authorising SSH key on root@"
mkdir -p /root/.ssh
chmod 700 /root/.ssh
touch /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
if [ -n "$PUBKEY" ]; then
if grep -qF "$PUBKEY" /root/.ssh/authorized_keys 2>/dev/null; then
ok "SSH key already authorised"
else
echo "$PUBKEY" >> /root/.ssh/authorized_keys
ok "SSH key added"
fi
else
warn "No PUBKEY argument — skipping SSH key install"
fi
say "[2/11] Configuring sshd (allow both key + password as fallback)"
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
mkdir -p /etc/ssh/sshd_config.d
cat > /etc/ssh/sshd_config.d/99-gluecron-auth.conf <<EOF
PasswordAuthentication yes
PubkeyAuthentication yes
PermitRootLogin yes
EOF
systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null || true
ok "sshd reloaded — both auth methods enabled"
say "[3/11] Ensuring git, curl, postgresql, caddy, bun are installed"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
for pkg in git curl ca-certificates gnupg debian-keyring debian-archive-keyring apt-transport-https openssl unzip xz-utils tar; do
dpkg -l | grep -qw "$pkg" || apt-get install -y -qq "$pkg"
done
ok "base packages installed"
if ! command -v psql >/dev/null 2>&1; then
apt-get install -y -qq postgresql postgresql-contrib
systemctl enable --now postgresql
ok "postgres installed + started"
else
ok "postgres already installed"
fi
if ! command -v caddy >/dev/null 2>&1; then
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt-get update -qq
apt-get install -y -qq caddy
systemctl enable --now caddy
ok "caddy installed + started"
else
ok "caddy already installed (likely from crontech)"
fi
if [ ! -x /root/.bun/bin/bun ]; then
curl -fsSL https://bun.sh/install | bash >/dev/null
ok "bun installed"
else
ok "bun already installed"
fi
export PATH="/root/.bun/bin:$PATH"
ok "bun version: $(/root/.bun/bin/bun --version 2>/dev/null || echo unknown)"
say "[4/11] Creating local Postgres DB + user (idempotent)"
if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -q 1; then
ok "postgres user '${DB_USER}' already exists"
else
sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';"
ok "postgres user '${DB_USER}' created"
fi
if sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then
ok "postgres database '${DB_NAME}' already exists"
else
sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};"
ok "postgres database '${DB_NAME}' created"
fi
EXISTING_DB_URL=""
if [ -f /etc/gluecron.env ] && grep -q '^DATABASE_URL=' /etc/gluecron.env; then
EXISTING_DB_URL=$(grep '^DATABASE_URL=' /etc/gluecron.env | cut -d= -f2- | tr -d '"')
ok "reusing existing DATABASE_URL from /etc/gluecron.env"
fi
DATABASE_URL="${EXISTING_DB_URL:-postgresql://${DB_USER}:${DB_PASS}@127.0.0.1:5432/${DB_NAME}}"
if [ -z "$EXISTING_DB_URL" ]; then
sudo -u postgres psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}';"
fi
say "[5/11] Setting up /opt/gluecron source tree"
mkdir -p /opt
if [ ! -d /opt/gluecron/.git ]; then
git clone https://github.com/ccantynz-alt/Gluecron.com.git /opt/gluecron
ok "cloned gluecron"
else
cd /opt/gluecron
git fetch --prune origin main
git reset --hard origin/main
ok "pulled latest main"
fi
cd /opt/gluecron
say "[6/11] Writing /etc/gluecron.env"
mkdir -p /data/repos
chmod 755 /data/repos
umask 077
cat > /etc/gluecron.env <<EOF
DATABASE_URL=${DATABASE_URL}
APP_BASE_URL=https://${SITE_DOMAIN}
SITE_ADMIN_USERNAME=${SITE_ADMIN_USERNAME:-ccantynz}
GIT_REPOS_PATH=/data/repos
PORT=${PORT}
DEMO_SEED_ON_BOOT=1
EMAIL_PROVIDER=log
EMAIL_FROM="gluecron <no-reply@${SITE_DOMAIN}>"
NODE_ENV=production
EOF
chmod 600 /etc/gluecron.env
umask 022
ok "/etc/gluecron.env written (chmod 600)"
say "[7/11] bun install + db:migrate"
/root/.bun/bin/bun install --frozen-lockfile
set -a; source /etc/gluecron.env; set +a
/root/.bun/bin/bun run src/db/migrate.ts || warn "migrations command failed — may already be applied"
ok "deps + migrations done"
say "[8/11] Writing /etc/systemd/system/gluecron.service"
BUN_BIN=/root/.bun/bin/bun
cat > /etc/systemd/system/gluecron.service <<EOF
[Unit]
Description=Gluecron — AI-native code intelligence platform
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/gluecron
EnvironmentFile=/etc/gluecron.env
ExecStart=${BUN_BIN} run src/index.ts
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=gluecron
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable gluecron >/dev/null
systemctl restart gluecron
ok "gluecron systemd unit installed + started"
say "[9/11] Caddy reverse-proxy config for ${SITE_DOMAIN}"
CADDYFILE=/etc/caddy/Caddyfile
touch "$CADDYFILE"
if grep -qE "^${SITE_DOMAIN}\s*\{" "$CADDYFILE"; then
ok "${SITE_DOMAIN} block already in Caddyfile — leaving as-is"
else
cat >> "$CADDYFILE" <<EOF
${SITE_DOMAIN} {
encode zstd gzip
reverse_proxy localhost:${PORT}
}
www.${SITE_DOMAIN} {
redir https://${SITE_DOMAIN}{uri} permanent
}
EOF
ok "appended ${SITE_DOMAIN} block to Caddyfile"
fi
if caddy validate --config "$CADDYFILE" --adapter caddyfile 2>&1 | grep -qi "valid"; then
systemctl reload caddy || systemctl restart caddy
ok "caddy reloaded"
else
warn "caddy validate warned — check: caddy validate --config $CADDYFILE"
fi
say "[10/11] Waiting for /healthz to come up (60s)"
for i in $(seq 1 12); do
code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/healthz" || echo 000)
if [ "$code" = "200" ]; then
ok "gluecron is live on http://localhost:${PORT} (attempt $i)"
break
fi
sleep 5
done
if [ "$code" != "200" ]; then
warn "healthz did not respond 200 after 60s. Logs: journalctl -u gluecron -n 50 --no-pager"
fi
say "[11/11] DONE"
echo ""
echo "============================================================"
echo " GLUECRON BOOTSTRAP COMPLETE"
echo "============================================================"
echo " systemd unit: gluecron.service"
echo " port: ${PORT}"
echo " env file: /etc/gluecron.env (chmod 600)"
echo " bare repos: /data/repos"
echo " source: /opt/gluecron"
echo ""
echo " Test from your laptop:"
echo " ssh root@\$(hostname -I | awk '{print \$1}') 'systemctl status gluecron --no-pager | head -5'"
echo ""
echo " Public URL (once DNS is correct + Caddy issued cert):"
echo " https://${SITE_DOMAIN}"
echo " https://${SITE_DOMAIN}/api/version"
echo ""
echo " Useful commands:"
echo " systemctl status gluecron"
echo " journalctl -u gluecron -f"
echo " bash /opt/gluecron/scripts/bootstrap-hetzner.sh # safe to re-run"
echo "============================================================"
|