CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
standalone-deploy.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.
| 95e33b0 | 1 | #!/usr/bin/env bash |
| 2 | # | |
| 3 | # Standalone single-box deploy for Gluecron on a DEDICATED VPS. | |
| 4 | # Brings up Gluecron + Postgres(pgvector) + Caddy(auto-HTTPS) with one command. | |
| 5 | # | |
| 6 | # Usage (as root on a fresh Ubuntu box): | |
| 9de2807 | 7 | # curl -fsSL https://raw.githubusercontent.com/ccantynz-alt/Gluecron.com/main/scripts/standalone-deploy.sh | bash |
| 95e33b0 | 8 | # or, after cloning: |
| 9 | # bash scripts/standalone-deploy.sh | |
| 10 | # | |
| 11 | # To migrate existing data: copy a dump to /root/gluecron.sql.gz BEFORE running | |
| 12 | # (e.g. `scp` it from the old box). The script restores it automatically. | |
| 13 | set -euo pipefail | |
| 14 | ||
| 15 | REPO_URL="https://github.com/ccantynz-alt/Gluecron.com.git" | |
| 9de2807 | 16 | REPO_BRANCH="main" |
| 95e33b0 | 17 | REPO_DIR="/opt/gluecron" |
| 18 | COMPOSE="docker compose -f docker-compose.standalone.yml" | |
| 19 | ||
| 20 | echo "== Gluecron standalone deploy ==" | |
| 21 | ||
| 22 | # 1. Docker | |
| 23 | if ! command -v docker >/dev/null 2>&1; then | |
| 24 | echo "-- installing Docker" | |
| 25 | curl -fsSL https://get.docker.com | sh | |
| 26 | fi | |
| 27 | ||
| 6b603a3 | 28 | # 2. Code (the standalone compose file lives on $REPO_BRANCH) |
| 95e33b0 | 29 | if [ ! -d "$REPO_DIR/.git" ]; then |
| 6b603a3 | 30 | echo "-- cloning $REPO_URL ($REPO_BRANCH)" |
| 31 | git clone -b "$REPO_BRANCH" "$REPO_URL" "$REPO_DIR" | |
| 95e33b0 | 32 | fi |
| 33 | cd "$REPO_DIR" | |
| 6b603a3 | 34 | git fetch origin "$REPO_BRANCH" 2>/dev/null || true |
| 35 | git checkout "$REPO_BRANCH" 2>/dev/null || true | |
| 36 | git pull --ff-only origin "$REPO_BRANCH" 2>/dev/null || true | |
| 95e33b0 | 37 | |
| 38 | # 3. Env (random Postgres password on first run) | |
| 39 | if [ ! -f .env ]; then | |
| 40 | echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .env | |
| 41 | echo "ANTHROPIC_API_KEY=" >> .env | |
| 42 | echo "-- generated .env (random Postgres password; add ANTHROPIC_API_KEY later if you want AI features)" | |
| 43 | fi | |
| 44 | ||
| 45 | # 4. Firewall (best-effort; only ports we need) | |
| 46 | if command -v ufw >/dev/null 2>&1; then | |
| 47 | ufw allow 22/tcp >/dev/null 2>&1 || true | |
| 48 | ufw allow 80/tcp >/dev/null 2>&1 || true | |
| 49 | ufw allow 443/tcp >/dev/null 2>&1 || true | |
| 50 | fi | |
| 51 | ||
| 52 | # 5. Database first | |
| 53 | echo "-- starting Postgres" | |
| 54 | $COMPOSE up -d postgres | |
| 55 | echo "-- waiting for Postgres to accept connections" | |
| 56 | until $COMPOSE exec -T postgres pg_isready -U gluecron -d gluecron >/dev/null 2>&1; do sleep 2; done | |
| 57 | ||
| 58 | # 6. Restore prior data if a dump is present | |
| 59 | if [ -f /root/gluecron.sql.gz ]; then | |
| 60 | echo "-- restoring data from /root/gluecron.sql.gz" | |
| 61 | gunzip -c /root/gluecron.sql.gz | $COMPOSE exec -T postgres psql -U gluecron -d gluecron >/dev/null | |
| 62 | elif [ -f /root/gluecron.sql ]; then | |
| 63 | echo "-- restoring data from /root/gluecron.sql" | |
| 64 | $COMPOSE exec -T postgres psql -U gluecron -d gluecron < /root/gluecron.sql >/dev/null | |
| 65 | else | |
| 66 | echo "-- no dump found at /root/gluecron.sql(.gz); starting with a fresh database" | |
| 67 | fi | |
| 68 | ||
| 69 | # 7. App + Caddy | |
| 70 | echo "-- building and starting Gluecron + Caddy" | |
| 71 | $COMPOSE up -d --build | |
| 72 | ||
| 73 | # 8. Migrations (idempotent — safe whether restored or fresh) | |
| 74 | echo "-- applying migrations" | |
| 75 | sleep 5 | |
| 76 | $COMPOSE exec -T gluecron bun run db:migrate || true | |
| 77 | ||
| f635e2f | 78 | # 9. Swap (protects a small box from OOM kills) |
| 79 | if [ "$(swapon --show --noheadings | wc -l)" -eq 0 ]; then | |
| 80 | echo "-- creating 1G swap file" | |
| 81 | fallocate -l 1G /swapfile && chmod 600 /swapfile && mkswap /swapfile >/dev/null && swapon /swapfile | |
| 82 | grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab | |
| 83 | fi | |
| 84 | ||
| 85 | # 10. Unattended security updates | |
| 86 | echo "-- enabling unattended-upgrades" | |
| 87 | DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades >/dev/null 2>&1 || true | |
| 88 | dpkg-reconfigure -f noninteractive unattended-upgrades >/dev/null 2>&1 || true | |
| 89 | ||
| 90 | # 11. Self-managing systemd timers: fast auto-deploy (~60s) + daily backup | |
| 91 | chmod +x scripts/auto-update.sh scripts/backup.sh | |
| 92 | ||
| 93 | cat > /etc/systemd/system/gluecron-update.service <<EOF | |
| 94 | [Unit] | |
| 95 | Description=Gluecron auto-deploy (pull + rebuild on new commits) | |
| 96 | After=docker.service | |
| 97 | Requires=docker.service | |
| 98 | [Service] | |
| 99 | Type=oneshot | |
| 100 | WorkingDirectory=$REPO_DIR | |
| 101 | EnvironmentFile=-$REPO_DIR/.env | |
| 102 | ExecStart=$REPO_DIR/scripts/auto-update.sh | |
| 103 | EOF | |
| 104 | ||
| 105 | cat > /etc/systemd/system/gluecron-update.timer <<EOF | |
| 106 | [Unit] | |
| 107 | Description=Run Gluecron auto-deploy every minute | |
| 108 | [Timer] | |
| 109 | OnBootSec=2min | |
| 110 | OnUnitActiveSec=60s | |
| 111 | [Install] | |
| 112 | WantedBy=timers.target | |
| 113 | EOF | |
| 114 | ||
| 115 | cat > /etc/systemd/system/gluecron-backup.service <<EOF | |
| 116 | [Unit] | |
| 117 | Description=Gluecron daily Postgres backup | |
| 118 | After=docker.service | |
| 119 | Requires=docker.service | |
| 120 | [Service] | |
| 121 | Type=oneshot | |
| 122 | WorkingDirectory=$REPO_DIR | |
| 123 | EnvironmentFile=-$REPO_DIR/.env | |
| 124 | ExecStart=$REPO_DIR/scripts/backup.sh | |
| 125 | EOF | |
| 126 | ||
| 127 | cat > /etc/systemd/system/gluecron-backup.timer <<EOF | |
| 128 | [Unit] | |
| 129 | Description=Run Gluecron backup daily | |
| 130 | [Timer] | |
| 131 | OnCalendar=daily | |
| 132 | Persistent=true | |
| 133 | [Install] | |
| 134 | WantedBy=timers.target | |
| 135 | EOF | |
| 136 | ||
| 137 | systemctl daemon-reload | |
| 138 | systemctl enable --now gluecron-update.timer gluecron-backup.timer >/dev/null 2>&1 || true | |
| 139 | ||
| 95e33b0 | 140 | echo |
| 141 | echo "== status ==" | |
| 142 | $COMPOSE ps | |
| f635e2f | 143 | echo "-- timers --" |
| 144 | systemctl list-timers 'gluecron-*' --no-pager 2>/dev/null || true | |
| 145 | echo | |
| 146 | echo "Self-healing active: container auto-restart, autoheal, log rotation," | |
| 147 | echo "1G swap, unattended security updates, daily DB backups (backups/), and" | |
| 148 | echo "auto-deploy (~60s) from the deploy branch." | |
| 95e33b0 | 149 | echo |
| 150 | echo "Done. Now point gluecron.com + www.gluecron.com DNS at THIS box's IP" | |
| 151 | echo "(Cloudflare, DNS-only / grey cloud). Caddy issues the cert automatically" | |
| 152 | echo "within ~1 minute of DNS resolving here. Verify with:" | |
| 153 | echo " curl -sI https://gluecron.com/healthz" |