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

install-self-host.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.

install-self-host.shBlame346 lines · 1 contributor
c4e643cClaude1#!/usr/bin/env bash
2# =============================================================================
3# Gluecron self-host installer.
4#
5# curl -fsSL https://gluecron.com/install-server | bash
6#
7# Goal: a working self-hosted Gluecron in 60 seconds. Microsoft GHE is a
8# 50GB blob and a week of professional services; we ship a ~200MB binary
9# and a curl-bash.
10#
11# What this script does (every step is idempotent + verbose):
12# 1. Detect OS + arch (linux/darwin × x64/arm64)
13# 2. Resolve host (default https://gluecron.com, env override)
14# 3. Fetch SHA256SUMS, then the binary, verify the hash
15# 4. Install to /opt/gluecron/bin (sudo) or ~/.gluecron/bin (no sudo)
16# 5. Provision Postgres: detect existing OR offer docker postgres
17# 6. Write /etc/gluecron.env (or ~/.gluecron/gluecron.env) from .env.example
18# and prompt for the few required keys
19# 7. Run migrations
20# 8. Create a systemd unit (linux) OR launchd plist (darwin) + start it
21# 9. Print the "Your Gluecron is live at http://localhost:3010" banner
22#
23# Hard rules:
24# - Never `set +e` — failures must bubble up.
25# - Never assume jq / docker / sudo are present — feature-flag everything.
26# - Re-runnable: a second run upgrades the binary in place.
27# =============================================================================
28
29set -Eeuo pipefail
30
31# ── pretty printers (matched to scripts/install.sh) ────────────────────────
32say() { printf "\n\033[1;34m> %s\033[0m\n" "$*"; }
33ok() { printf " \033[32mv\033[0m %s\n" "$*"; }
34warn() { printf " \033[33m!\033[0m %s\n" "$*"; }
35fail() { printf " \033[31mx\033[0m %s\n" "$*"; exit 1; }
36
37HOST="${GLUECRON_HOST:-https://gluecron.com}"
38HOST="${HOST%/}"
39PORT="${GLUECRON_PORT:-3010}"
40
41# ── 1. Detect platform ─────────────────────────────────────────────────────
42say "[1/9] Detecting platform"
43UNAME_S="$(uname -s 2>/dev/null || echo unknown)"
44UNAME_M="$(uname -m 2>/dev/null || echo unknown)"
45
46case "$UNAME_S" in
47 Linux*) PLAT=linux ;;
48 Darwin*) PLAT=darwin ;;
49 *) fail "Unsupported OS: $UNAME_S (linux / darwin only)" ;;
50esac
51
52case "$UNAME_M" in
53 x86_64|amd64) ARCH=x64 ;;
54 aarch64|arm64) ARCH=arm64 ;;
55 *) fail "Unsupported arch: $UNAME_M (x64 / arm64 only)" ;;
56esac
57
58TARGET="$PLAT-$ARCH"
59BIN_NAME="gluecron-server-$TARGET"
60ok "platform: $TARGET"
61
62# ── 2. Choose install root (sudo vs user) ──────────────────────────────────
63say "[2/9] Resolving install location"
64USE_SUDO=0
65INSTALL_PREFIX=""
66ENV_FILE=""
67SERVICE_USER=""
68
69if [ "$(id -u)" = "0" ]; then
70 INSTALL_PREFIX="/opt/gluecron"
71 ENV_FILE="/etc/gluecron.env"
72 SERVICE_USER="gluecron"
73 ok "running as root — installing to $INSTALL_PREFIX"
74elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
75 USE_SUDO=1
76 INSTALL_PREFIX="/opt/gluecron"
77 ENV_FILE="/etc/gluecron.env"
78 SERVICE_USER="${SUDO_USER:-$(whoami)}"
79 ok "passwordless sudo available — installing to $INSTALL_PREFIX"
80else
81 INSTALL_PREFIX="$HOME/.gluecron"
82 ENV_FILE="$HOME/.gluecron/gluecron.env"
83 SERVICE_USER="$(whoami)"
84 ok "no sudo — installing to $INSTALL_PREFIX (user-scope)"
85fi
86
87run_priv() {
88 if [ "$USE_SUDO" = "1" ]; then
89 sudo "$@"
90 else
91 "$@"
92 fi
93}
94
95run_priv mkdir -p "$INSTALL_PREFIX/bin" "$INSTALL_PREFIX/repos" "$INSTALL_PREFIX/data"
96
97# ── 3. Download + verify the binary ────────────────────────────────────────
98say "[3/9] Fetching binary from $HOST"
99TMP="$(mktemp -d -t gluecron-install.XXXXXX)"
100trap 'rm -rf "$TMP"' EXIT
101
102SUMS_URL="$HOST/dist/SHA256SUMS"
103BIN_URL="$HOST/dist/$BIN_NAME"
104
105curl -fsSL --max-time 30 "$SUMS_URL" -o "$TMP/SHA256SUMS" \
106 || fail "Couldn't fetch $SUMS_URL — is the host serving binaries?"
107ok "checksums fetched"
108
109EXPECTED_HASH=$(awk -v f="$BIN_NAME" '$2==f {print $1}' "$TMP/SHA256SUMS")
110if [ -z "$EXPECTED_HASH" ]; then
111 fail "No checksum for $BIN_NAME in SHA256SUMS — build the bundle for your platform."
112fi
113
114curl -fsSL --max-time 600 "$BIN_URL" -o "$TMP/$BIN_NAME" \
115 || fail "Couldn't download $BIN_URL"
116ACTUAL_HASH=$(sha256sum "$TMP/$BIN_NAME" | awk '{print $1}')
117if [ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]; then
118 fail "SHA-256 mismatch: expected $EXPECTED_HASH, got $ACTUAL_HASH"
119fi
120ok "verified sha256: ${ACTUAL_HASH:0:16}…"
121
122chmod +x "$TMP/$BIN_NAME"
123run_priv mv -f "$TMP/$BIN_NAME" "$INSTALL_PREFIX/bin/gluecron-server"
124ok "installed: $INSTALL_PREFIX/bin/gluecron-server"
125
126# Optional: fetch the env.example as a starter template
127if curl -fsSL --max-time 15 "$HOST/dist/env.example" -o "$TMP/env.example" 2>/dev/null; then
128 ok "env template fetched"
129else
130 warn "env.example not on server — using fallback minimal template"
131 cat >"$TMP/env.example" <<'ENV_FALLBACK'
132DATABASE_URL=postgresql://gluecron:gluecron@localhost:5432/gluecron
133GIT_REPOS_PATH=/opt/gluecron/repos
134PORT=3010
135ENV_FALLBACK
136fi
137
138# ── 4. Postgres — detect or provision ──────────────────────────────────────
139say "[4/9] Postgres"
140DETECTED_DB=""
141if command -v psql >/dev/null 2>&1; then
142 if psql -h localhost -U postgres -c 'SELECT 1' >/dev/null 2>&1; then
143 DETECTED_DB="postgresql://postgres@localhost:5432/postgres"
144 ok "found local Postgres reachable as postgres@localhost"
145 fi
146fi
147if [ -z "$DETECTED_DB" ] && [ -n "${DATABASE_URL:-}" ]; then
148 DETECTED_DB="$DATABASE_URL"
149 ok "using DATABASE_URL from env"
150fi
151
152DB_URL=""
153if [ -n "$DETECTED_DB" ]; then
154 DB_URL="$DETECTED_DB"
155else
156 warn "no existing Postgres detected"
157 if command -v docker >/dev/null 2>&1; then
158 if [ -t 0 ]; then
159 printf " Start a Postgres 14 container via docker? [Y/n] "
160 read -r ANSWER
161 else
162 ANSWER="${GLUECRON_USE_DOCKER_PG:-y}"
163 fi
164 case "$ANSWER" in
165 n|N|no|NO)
166 warn "skipping docker — you must set DATABASE_URL manually before first start"
167 ;;
168 *)
169 if docker ps --format '{{.Names}}' | grep -q '^gluecron-postgres$'; then
170 ok "gluecron-postgres container already running"
171 else
172 docker run -d --name gluecron-postgres \
173 -e POSTGRES_USER=gluecron \
174 -e POSTGRES_PASSWORD=gluecron \
175 -e POSTGRES_DB=gluecron \
176 -p 5432:5432 \
177 -v gluecron-pgdata:/var/lib/postgresql/data \
178 postgres:14 >/dev/null \
179 || warn "docker run failed — see docker logs gluecron-postgres"
180 ok "started gluecron-postgres container"
181 # Give Postgres a few seconds to accept connections.
182 sleep 4
183 fi
184 DB_URL="postgresql://gluecron:gluecron@localhost:5432/gluecron"
185 ;;
186 esac
187 else
188 warn "docker not on PATH either — please set DATABASE_URL in $ENV_FILE before starting"
189 fi
190fi
191
192# ── 5. Write env file ──────────────────────────────────────────────────────
193say "[5/9] Writing $ENV_FILE"
194TMP_ENV="$(mktemp -t gluecron-env.XXXXXX)"
195# Start from the template, then overwrite the keys we know.
196cp "$TMP/env.example" "$TMP_ENV"
197patch_env() {
198 local KEY="$1" VALUE="$2"
199 if grep -qE "^${KEY}=" "$TMP_ENV"; then
200 # macOS sed needs the empty -i arg; gnu sed accepts -i alone.
201 sed -i.bak -e "s|^${KEY}=.*$|${KEY}=${VALUE}|" "$TMP_ENV" && rm -f "$TMP_ENV.bak"
202 else
203 echo "${KEY}=${VALUE}" >>"$TMP_ENV"
204 fi
205}
206
207if [ -n "$DB_URL" ]; then patch_env DATABASE_URL "$DB_URL"; fi
208patch_env GIT_REPOS_PATH "$INSTALL_PREFIX/repos"
209patch_env PORT "$PORT"
210
211# Prompt only when stdin is a TTY — non-interactive runs (CI, docker build)
212# just inherit existing env vars and ship.
213if [ -t 0 ] && [ -z "${GLUECRON_SKIP_PROMPTS:-}" ]; then
214 if ! grep -qE '^APP_BASE_URL=' "$TMP_ENV"; then
215 printf " Public URL of this instance [http://localhost:%s]: " "$PORT"
216 read -r APP_BASE_URL
217 APP_BASE_URL="${APP_BASE_URL:-http://localhost:$PORT}"
218 patch_env APP_BASE_URL "$APP_BASE_URL"
219 fi
220fi
221
222run_priv mkdir -p "$(dirname "$ENV_FILE")"
223run_priv install -m 0640 "$TMP_ENV" "$ENV_FILE"
224ok "wrote $ENV_FILE"
225
226# ── 6. Migrations ──────────────────────────────────────────────────────────
227say "[6/9] Running database migrations"
228# The compiled binary embeds the migration runner — invoke it via the
229# GLUECRON_RUN_MIGRATIONS env flag (handled in src/index.ts at boot when
230# the corresponding sub-command lands). For now we drive `psql` if the
231# migrations tarball is available.
232if curl -fsSL --max-time 30 "$HOST/dist/migrations.tar.gz" -o "$TMP/migrations.tar.gz" 2>/dev/null; then
233 mkdir -p "$TMP/migrations"
234 tar -xzf "$TMP/migrations.tar.gz" -C "$TMP/migrations"
235 ok "migrations bundle fetched"
236fi
237
238# Source the env so DATABASE_URL is visible to the binary.
239# shellcheck disable=SC1090
240set -a; . "$ENV_FILE"; set +a
241if [ -n "${DATABASE_URL:-}" ]; then
242 if "$INSTALL_PREFIX/bin/gluecron-server" --migrate 2>/dev/null; then
243 ok "migrations applied via gluecron-server --migrate"
244 else
245 warn "binary --migrate failed (older build?) — re-run after first start; the boot path also self-migrates."
246 fi
247else
248 warn "DATABASE_URL not set — skipped migrations. Set it in $ENV_FILE and re-run."
249fi
250
251# ── 7. Systemd unit (linux) / launchd plist (darwin) ──────────────────────
252say "[7/9] Creating service"
253if [ "$PLAT" = "linux" ] && command -v systemctl >/dev/null 2>&1; then
254 UNIT_PATH="/etc/systemd/system/gluecron.service"
255 TMP_UNIT="$(mktemp -t gluecron-unit.XXXXXX)"
256 cat >"$TMP_UNIT" <<UNIT
257[Unit]
258Description=Gluecron self-host server
259After=network.target
260
261[Service]
262Type=simple
263User=$SERVICE_USER
264EnvironmentFile=$ENV_FILE
265ExecStart=$INSTALL_PREFIX/bin/gluecron-server
266Restart=on-failure
267RestartSec=3
268WorkingDirectory=$INSTALL_PREFIX
269
270[Install]
271WantedBy=multi-user.target
272UNIT
273 run_priv install -m 0644 "$TMP_UNIT" "$UNIT_PATH"
274 run_priv systemctl daemon-reload
275 run_priv systemctl enable --now gluecron.service \
276 || warn "systemctl enable failed — start manually with: systemctl start gluecron"
277 ok "systemd unit installed and started"
278
279elif [ "$PLAT" = "darwin" ]; then
280 PLIST_PATH="$HOME/Library/LaunchAgents/com.gluecron.server.plist"
281 mkdir -p "$(dirname "$PLIST_PATH")"
282 cat >"$PLIST_PATH" <<PLIST
283<?xml version="1.0" encoding="UTF-8"?>
284<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
285<plist version="1.0">
286<dict>
287 <key>Label</key><string>com.gluecron.server</string>
288 <key>ProgramArguments</key>
289 <array>
290 <string>$INSTALL_PREFIX/bin/gluecron-server</string>
291 </array>
292 <key>EnvironmentVariables</key><dict>
293 <key>GLUECRON_ENV_FILE</key><string>$ENV_FILE</string>
294 </dict>
295 <key>WorkingDirectory</key><string>$INSTALL_PREFIX</string>
296 <key>RunAtLoad</key><true/>
297 <key>KeepAlive</key><true/>
298 <key>StandardOutPath</key><string>$INSTALL_PREFIX/data/gluecron.out.log</string>
299 <key>StandardErrorPath</key><string>$INSTALL_PREFIX/data/gluecron.err.log</string>
300</dict>
301</plist>
302PLIST
303 launchctl unload "$PLIST_PATH" 2>/dev/null || true
304 launchctl load "$PLIST_PATH" \
305 || warn "launchctl load failed — start manually with: launchctl load $PLIST_PATH"
306 ok "launchd plist installed: $PLIST_PATH"
307
308else
309 warn "no service manager detected — start manually with:"
310 warn " $INSTALL_PREFIX/bin/gluecron-server"
311fi
312
313# ── 8. Wait for health ─────────────────────────────────────────────────────
314say "[8/9] Waiting for the server to come up"
315HEALTH="http://localhost:$PORT/healthz"
316GREEN=0
317for i in 1 2 3 4 5 6 7 8 9 10; do
318 CODE=$(curl -fsS -o /dev/null -w "%{http_code}" --max-time 2 "$HEALTH" || echo 000)
319 if [ "$CODE" = "200" ]; then GREEN=1; break; fi
320 sleep 1
321done
322if [ "$GREEN" = "1" ]; then
323 ok "/healthz green"
324else
325 warn "server didn't return 200 within 10s — check logs (journalctl -u gluecron) or run:"
326 warn " $INSTALL_PREFIX/bin/gluecron-server"
327fi
328
329# ── 9. Done ────────────────────────────────────────────────────────────────
330say "[9/9] All set"
331printf "\n"
332printf "\033[1;32m============================================================\033[0m\n"
333printf "\033[1;32m GLUECRON SELF-HOST INSTALL COMPLETE\033[0m\n"
334printf "\033[1;32m============================================================\033[0m\n"
335printf " Binary: %s/bin/gluecron-server\n" "$INSTALL_PREFIX"
336printf " Env: %s\n" "$ENV_FILE"
337printf " Repos: %s/repos\n" "$INSTALL_PREFIX"
338printf " Port: %s\n" "$PORT"
339printf "\n"
340printf " Your Gluecron is live at \033[1mhttp://localhost:%s\033[0m — visit\n" "$PORT"
341printf " to create your first admin user.\n"
342printf "\n"
343printf " Upgrade: curl -fsSL %s/install-server | bash\n" "$HOST"
344printf " Logs: journalctl -u gluecron -f (linux)\n"
345printf " Logs: tail -f %s/data/gluecron.err.log (darwin)\n" "$INSTALL_PREFIX"
346printf "\n"