web/public/cloud-init.shblame
View source
4a006da1#!/bin/bash
966d71f2# Grove cloud-init — bootstraps a Grove instance on any VPS.
2a9592c3# Sets up Mononoke + Grove Bridge + Grove API + Grove Web + Grove Collab + Caddy.
4a006da4#
966d71f5# Placeholders replaced by the deploy page before use:
966d71f6# __INSTANCE_ID__ — hub instance record ID
966d71f7# __HUB_DOMAIN__ — hub domain (e.g., grove.host)
966d71f8# __INSTANCE_DOMAIN__ — instance domain (optional, empty for IP-only)
966d71f9# __HUB_TOKEN__ — user's JWT for the readiness callback
4a006da10
4a006da11set -euo pipefail
4a006da12
4a006da13GROVE_DIR="/opt/grove"
966d71f14DATA_DIR="/data/grove"
966d71f15HUB_DOMAIN="__HUB_DOMAIN__"
966d71f16INSTANCE_DOMAIN="__INSTANCE_DOMAIN__"
966d71f17INSTANCE_ID="__INSTANCE_ID__"
966d71f18HUB_TOKEN="__HUB_TOKEN__"
4a006da19
966d71f20# ── Install Docker if missing ────────────────────────────────────────
4a006da21
966d71f22if ! command -v docker &>/dev/null; then
966d71f23 echo "Installing Docker..."
966d71f24 curl -fsSL https://get.docker.com | sh
966d71f25fi
4a006da26
966d71f27# ── Discover public IP (provider-agnostic) ───────────────────────────
966d71f28
966d71f29DROPLET_IP=$(curl -sf https://ifconfig.me || curl -sf https://api.ipify.org || hostname -I | awk '{print $1}')
966d71f30echo "Public IP: ${DROPLET_IP}"
966d71f31
966d71f32# ── Generate secrets ─────────────────────────────────────────────────
966d71f33
966d71f34JWT_SECRET=$(openssl rand -hex 32)
966d71f35
966d71f36# ── Generate self-signed TLS certs for Mononoke SLAPI ───────────────
966d71f37
966d71f38mkdir -p "${DATA_DIR}/tls"
966d71f39
966d71f40# CA
966d71f41openssl genrsa -out "${DATA_DIR}/tls/ca.key" 4096 2>/dev/null
966d71f42openssl req -new -x509 -key "${DATA_DIR}/tls/ca.key" \
966d71f43 -out "${DATA_DIR}/tls/ca.crt" -days 3650 \
966d71f44 -subj "/CN=Grove Instance CA" 2>/dev/null
966d71f45
966d71f46# Server cert
966d71f47openssl genrsa -out "${DATA_DIR}/tls/server.key" 4096 2>/dev/null
966d71f48openssl req -new -key "${DATA_DIR}/tls/server.key" \
966d71f49 -out "${DATA_DIR}/tls/server.csr" \
966d71f50 -subj "/CN=${INSTANCE_DOMAIN:-${DROPLET_IP}}" 2>/dev/null
966d71f51
966d71f52cat > "${DATA_DIR}/tls/ext.cnf" <<EOF
966d71f53authorityKeyIdentifier=keyid,issuer
966d71f54basicConstraints=CA:FALSE
966d71f55keyUsage=digitalSignature,keyEncipherment
966d71f56extendedKeyUsage=serverAuth,clientAuth
966d71f57subjectAltName=@alt_names
966d71f58
966d71f59[alt_names]
966d71f60IP.1 = ${DROPLET_IP}
966d71f61DNS.1 = ${INSTANCE_DOMAIN:-localhost}
966d71f62EOF
966d71f63
966d71f64openssl x509 -req -in "${DATA_DIR}/tls/server.csr" \
966d71f65 -CA "${DATA_DIR}/tls/ca.crt" -CAkey "${DATA_DIR}/tls/ca.key" \
966d71f66 -CAcreateserial -out "${DATA_DIR}/tls/server.crt" \
966d71f67 -days 3650 -extfile "${DATA_DIR}/tls/ext.cnf" 2>/dev/null
966d71f68
966d71f69rm -f "${DATA_DIR}/tls/server.csr" "${DATA_DIR}/tls/ext.cnf" "${DATA_DIR}/tls/ca.srl"
966d71f70
966d71f71echo "TLS certificates generated."
966d71f72
966d71f73# ── Create directory structure ───────────────────────────────────────
966d71f74
966d71f75mkdir -p "${DATA_DIR}/mononoke-config/common"
966d71f76mkdir -p "${DATA_DIR}/mononoke-config/repo_definitions"
966d71f77mkdir -p "${DATA_DIR}/mononoke-config/repos"
966d71f78mkdir -p "${DATA_DIR}/configerator"
4a006da79mkdir -p "${DATA_DIR}/mononoke"
80fafdf80mkdir -p "${DATA_DIR}/api"
966d71f81mkdir -p "${GROVE_DIR}"
4a006da82
4a006da83# ── Write Mononoke config ───────────────────────────────────────────
4a006da84
966d71f85cat > "${DATA_DIR}/mononoke-config/common/common.toml" <<'TOML'
4a006da86enable_http_control_api = true
4a006da87
4a006da88[internal_identity]
4a006da89identity_type = "SERVICE"
4a006da90identity_data = "grove"
4a006da91TOML
4a006da92
966d71f93cat > "${DATA_DIR}/mononoke-config/common/storage.toml" <<'TOML'
4a006da94[default]
4a006da95
4a006da96[default.metadata]
4a006da97[default.metadata.local]
966d71f98local_db_path = "/data/grove/mononoke/metadata"
4a006da99
4a006da100[default.blobstore]
4a006da101[default.blobstore.blob_files]
966d71f102path = "/data/grove/mononoke/blobs"
4a006da103
4a006da104[default.mutable_blobstore]
4a006da105[default.mutable_blobstore.blob_files]
966d71f106path = "/data/grove/mononoke/mutable_blobs"
4a006da107TOML
4a006da108
966d71f109# ── Write justknobs.json (required by Mononoke) ─────────────────────
966d71f110
966d71f111cat > "${DATA_DIR}/justknobs.json" <<'JSON'
966d71f112{}
966d71f113JSON
966d71f114
966d71f115# ── Write .env ───────────────────────────────────────────────────────
966d71f116
966d71f117cat > "${GROVE_DIR}/.env" <<EOF
966d71f118JWT_SECRET=${JWT_SECRET}
966d71f119DOMAIN=${INSTANCE_DOMAIN:-${DROPLET_IP}}
966d71f120HUB_DOMAIN=${HUB_DOMAIN}
966d71f121EOF
966d71f122
966d71f123# ── Write Caddyfile ──────────────────────────────────────────────────
966d71f124
966d71f125if [ -n "${INSTANCE_DOMAIN}" ]; then
966d71f126 CADDY_ADDR="${INSTANCE_DOMAIN}"
966d71f127else
966d71f128 CADDY_ADDR=":80"
966d71f129fi
966d71f130
966d71f131cat > "${GROVE_DIR}/Caddyfile" <<CADDYEOF
966d71f132${CADDY_ADDR} {
966d71f133 handle /api/auth/* {
966d71f134 reverse_proxy https://${HUB_DOMAIN} {
966d71f135 header_up Host ${HUB_DOMAIN}
966d71f136 }
966d71f137 }
966d71f138
966d71f139 handle /api/instances/* {
966d71f140 reverse_proxy https://${HUB_DOMAIN} {
966d71f141 header_up Host ${HUB_DOMAIN}
966d71f142 }
966d71f143 }
966d71f144
966d71f145 handle /v2/* {
966d71f146 reverse_proxy registry:5000
966d71f147 }
966d71f148
0b4b582149 handle /collab/socket.io/* {
2a9592c150 uri strip_prefix /collab
0b4b582151 reverse_proxy grove-web:3334
2a9592c152 }
2a9592c153
966d71f154 handle /api/repos {
966d71f155 reverse_proxy grove-api:4000
966d71f156 }
966d71f157
966d71f158 handle /api/repos/* {
966d71f159 reverse_proxy grove-api:4000
966d71f160 }
966d71f161
966d71f162 handle {
966d71f163 reverse_proxy grove-web:3000
966d71f164 }
966d71f165
966d71f166 header {
966d71f167 X-Content-Type-Options nosniff
966d71f168 X-Frame-Options DENY
966d71f169 }
966d71f170}
966d71f171CADDYEOF
4a006da172
4a006da173# ── Write docker-compose.yml ────────────────────────────────────────
4a006da174
4a006da175cat > "${GROVE_DIR}/docker-compose.yml" <<'COMPOSE'
4a006da176services:
5f0fbcf177 registry:
5f0fbcf178 image: registry:2
5f0fbcf179 ports:
5f0fbcf180 - "127.0.0.1:5000:5000"
5f0fbcf181 volumes:
5f0fbcf182 - registry-data:/var/lib/registry
5f0fbcf183 environment:
5f0fbcf184 - REGISTRY_STORAGE_DELETE_ENABLED=true
5f0fbcf185 restart: unless-stopped
5f0fbcf186
4a006da187 mononoke-slapi:
966d71f188 image: ${HUB_DOMAIN}/grove-mononoke:latest
4a006da189 command:
4a006da190 - --listening-host-port
4a006da191 - "0.0.0.0:8443"
4a006da192 - --config-path
966d71f193 - /data/grove/mononoke-config
966d71f194 - --local-configerator-path
966d71f195 - /data/grove/configerator
966d71f196 - --cache-mode
966d71f197 - disabled
966d71f198 - --just-knobs-config-path
966d71f199 - /data/grove/justknobs.json
966d71f200 - --tls-certificate
966d71f201 - /data/grove/tls/server.crt
966d71f202 - --tls-private-key
966d71f203 - /data/grove/tls/server.key
966d71f204 - --tls-ca
966d71f205 - /data/grove/tls/ca.crt
966d71f206 ports:
966d71f207 - "8443:8443"
4a006da208 volumes:
966d71f209 - /data/grove:/data/grove
4a006da210 restart: unless-stopped
4a006da211
4a006da212 mononoke-git:
966d71f213 image: ${HUB_DOMAIN}/grove-mononoke:latest
4a006da214 entrypoint: ["/usr/local/bin/git_server"]
4a006da215 command:
7422c65216 - --listen-host
7422c65217 - "0.0.0.0"
7422c65218 - --listen-port
7422c65219 - "8080"
4a006da220 - --config-path
966d71f221 - /data/grove/mononoke-config
966d71f222 - --local-configerator-path
966d71f223 - /data/grove/configerator
966d71f224 - --cache-mode
966d71f225 - disabled
966d71f226 - --just-knobs-config-path
966d71f227 - /data/grove/justknobs.json
966d71f228 - --tls-certificate
966d71f229 - /data/grove/tls/server.crt
966d71f230 - --tls-private-key
966d71f231 - /data/grove/tls/server.key
966d71f232 - --tls-ca
966d71f233 - /data/grove/tls/ca.crt
4a006da234 ports:
4a006da235 - "8080:8080"
4a006da236 volumes:
966d71f237 - /data/grove:/data/grove
4a006da238 depends_on:
4a006da239 mononoke-slapi:
966d71f240 condition: service_started
4a006da241 restart: unless-stopped
4a006da242
4a006da243 grove-bridge:
966d71f244 image: ${HUB_DOMAIN}/grove-mononoke:latest
4a006da245 entrypoint: ["/usr/local/bin/grove_bridge"]
4a006da246 command:
4a006da247 - --listening-host-port
4a006da248 - "0.0.0.0:3100"
4a006da249 - --config-path
966d71f250 - /data/grove/mononoke-config
966d71f251 - --local-configerator-path
966d71f252 - /data/grove/configerator
966d71f253 - --cache-mode
966d71f254 - disabled
966d71f255 - --just-knobs-config-path
966d71f256 - /data/grove/justknobs.json
4a006da257 volumes:
966d71f258 - /data/grove:/data/grove
4a006da259 depends_on:
4a006da260 mononoke-slapi:
966d71f261 condition: service_started
4a006da262 restart: unless-stopped
4a006da263
80fafdf264 grove-api:
966d71f265 image: ${HUB_DOMAIN}/grove-api:latest
80fafdf266 environment:
966d71f267 - PORT=4000
80fafdf268 - DATABASE_PATH=/data/grove.db
966d71f269 - JWT_SECRET=${JWT_SECRET}
80fafdf270 - GROVE_BRIDGE_URL=http://grove-bridge:3100
966d71f271 - MONONOKE_CONFIG_PATH=/data/grove/mononoke-config
80fafdf272 - CANOPY_ENABLED=true
80fafdf273 - CANOPY_WORKSPACE_DIR=/canopy/workspaces
80fafdf274 - NODE_ENV=production
80fafdf275 volumes:
966d71f276 - /data/grove/api:/data
80fafdf277 - /var/run/docker.sock:/var/run/docker.sock
966d71f278 - /data/grove:/data/grove
80fafdf279 - canopy-workspaces:/canopy/workspaces
80fafdf280 depends_on:
80fafdf281 grove-bridge:
80fafdf282 condition: service_started
80fafdf283 restart: unless-stopped
80fafdf284
966d71f285 grove-web:
966d71f286 image: ${HUB_DOMAIN}/grove-web:latest
2a9592c287 volumes:
0b4b582288 - collab-data:/data/collab
2a9592c289 environment:
2a9592c290 - GROVE_API_URL=http://grove-api:4000
0b4b582291 - DATA_DIR=/data/collab
0b4b582292 - COLLAB_SOCKET_PORT=3334
0b4b582293 - JWT_SECRET=${JWT_SECRET}
2a9592c294 - NODE_ENV=production
0b4b582295 depends_on:
0b4b582296 - grove-api
2a9592c297 restart: unless-stopped
2a9592c298
966d71f299 caddy:
966d71f300 image: caddy:2-alpine
966d71f301 ports:
966d71f302 - "80:80"
966d71f303 - "443:443"
966d71f304 volumes:
966d71f305 - ./Caddyfile:/etc/caddy/Caddyfile:ro
966d71f306 - caddy-data:/data
966d71f307 - caddy-config:/config
966d71f308 depends_on:
966d71f309 - grove-web
966d71f310 - grove-api
966d71f311 restart: unless-stopped
966d71f312
4a006da313volumes:
5f0fbcf314 registry-data:
80fafdf315 canopy-workspaces:
2a9592c316 collab-data:
966d71f317 caddy-data:
966d71f318 caddy-config:
4a006da319COMPOSE
4a006da320
966d71f321# ── Pull images from hub registry ───────────────────────────────────
5f0fbcf322
966d71f323echo "Pulling images from ${HUB_DOMAIN}..."
966d71f324docker pull "${HUB_DOMAIN}/grove-mononoke:latest"
966d71f325docker pull "${HUB_DOMAIN}/grove-api:latest"
966d71f326docker pull "${HUB_DOMAIN}/grove-web:latest"
5f0fbcf327
966d71f328# ── Start ────────────────────────────────────────────────────────────
4a006da329
4a006da330cd "${GROVE_DIR}"
4a006da331docker compose up -d
4a006da332
4a006da333# ── Wait for health ──────────────────────────────────────────────────
4a006da334
4a006da335echo "Waiting for Grove Bridge to be healthy..."
4a006da336for i in $(seq 1 60); do
4a006da337 if curl -sf http://localhost:3100/health > /dev/null 2>&1; then
4a006da338 echo "Grove Bridge is healthy."
4a006da339 break
4a006da340 fi
4a006da341 sleep 5
4a006da342done
4a006da343
4a006da344# ── Notify hub that instance is ready ────────────────────────────────
4a006da345
966d71f346curl -sf -X POST "https://${HUB_DOMAIN}/api/instances/${INSTANCE_ID}/ready" \
4a006da347 -H "Content-Type: application/json" \
966d71f348 -d "{\"ip\": \"${DROPLET_IP}\", \"jwt_secret\": \"${JWT_SECRET}\"}" || true
4a006da349
4a006da350# ── Done ─────────────────────────────────────────────────────────────
4a006da351
4a006da352touch "${GROVE_DIR}/.setup-complete"
966d71f353echo ""
966d71f354echo "================================================"
966d71f355echo " Grove instance deployment complete."
966d71f356echo " IP: ${DROPLET_IP}"
966d71f357echo " URL: ${INSTANCE_DOMAIN:+https://${INSTANCE_DOMAIN}}${INSTANCE_DOMAIN:-http://${DROPLET_IP}}"
966d71f358echo ""
966d71f359echo " Mononoke SLAPI: :8443 (TLS)"
966d71f360echo " CA cert: ${DATA_DIR}/tls/ca.crt"
966d71f361echo "================================================"