scripts/deploy.sh pushes the current branch to origin, ssh's into the prod box (neuronetz@cloud.neuronetz.ai:/home/neuronetz/docker-public/ neuro-psyc by default — overridable via env vars), clones-or-pulls, ensures the external 'backend' docker network exists, runs docker compose up -d --build (+ --profile gpu if PSYC_PROD_GPU=1), and then verifies the cockpit is healthy both on prod-internal :8767 and at the public URL — so the script ends knowing whether the page is up. Refuses to touch prod's .env (warns + copies .env.example if missing, so you can edit it manually). Never transfers data/ or adapters (gitignored; prod fetches its own corpus). Color output, idempotent, safe to re-run. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
148 lines
6.4 KiB
Bash
Executable File
148 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# deploy.sh — sync this branch to the prod box and verify the cockpit is serving.
|
|
#
|
|
# Usage: scripts/deploy.sh
|
|
#
|
|
# Env vars (all have defaults — override only if your setup differs):
|
|
# PSYC_PROD_HOST default: neuronetz@cloud.neuronetz.ai
|
|
# PSYC_PROD_PATH default: /home/neuronetz/docker-public/neuro-psyc
|
|
# PSYC_PROD_URL default: https://psyc.neuronetz.ai
|
|
# PSYC_PROD_GPU set to 1 to also bring up the inference (GPU) service
|
|
# PSYC_GIT_REMOTE default: origin
|
|
# PSYC_BRANCH default: the currently checked-out branch
|
|
#
|
|
# What it does (idempotent — safe to re-run):
|
|
# 1. push the current branch to origin
|
|
# 2. ssh into prod, clone the repo if missing, pull the branch
|
|
# 3. docker compose up -d --build (+ gpu profile if PSYC_PROD_GPU=1)
|
|
# 4. probe :8767/healthz on the prod box + the public URL; report state
|
|
#
|
|
# What it does NOT do:
|
|
# • touch .env on the prod box (set keys there once, manually — gitignored)
|
|
# • transfer data/ or model artifacts (gitignored; prod fetches its own)
|
|
# • configure DNS or TLS (that's the reverse-proxy + acme-companion side)
|
|
|
|
set -euo pipefail
|
|
|
|
HOST="${PSYC_PROD_HOST:-neuronetz@cloud.neuronetz.ai}"
|
|
REMOTE_PATH="${PSYC_PROD_PATH:-/home/neuronetz/docker-public/neuro-psyc}"
|
|
PUBLIC_URL="${PSYC_PROD_URL:-https://psyc.neuronetz.ai}"
|
|
GIT_REMOTE="${PSYC_GIT_REMOTE:-origin}"
|
|
BRANCH="${PSYC_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}"
|
|
WITH_GPU="${PSYC_PROD_GPU:-}"
|
|
|
|
# ── tty styling ─────────────────────────────────────────────────────────
|
|
if [[ -t 1 ]]; then
|
|
B=$'\e[1m'; D=$'\e[2m'; R=$'\e[31m'; G=$'\e[32m'; Y=$'\e[33m'; C=$'\e[36m'; Z=$'\e[0m'
|
|
else B=; D=; R=; G=; Y=; C=; Z=; fi
|
|
say() { printf "%s[deploy]%s %s\n" "$C" "$Z" "$*"; }
|
|
ok() { printf "%s[deploy]%s %s%s%s\n" "$G" "$Z" "$G" "$*" "$Z"; }
|
|
warn() { printf "%s[deploy]%s %s%s%s\n" "$Y" "$Z" "$Y" "$*" "$Z"; }
|
|
fail() { printf "%s[deploy]%s %s%s%s\n" "$R" "$Z" "$R" "$*" "$Z" >&2; exit 1; }
|
|
|
|
# ── 0. preflight ────────────────────────────────────────────────────────
|
|
command -v ssh >/dev/null || fail "ssh not installed locally"
|
|
command -v git >/dev/null || fail "git not installed locally"
|
|
command -v curl >/dev/null || fail "curl not installed locally"
|
|
[[ -d .git ]] || fail "run from the psyc repo root (no .git here)"
|
|
|
|
if ! git diff --quiet HEAD -- 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
|
|
warn "local working tree has uncommitted changes — they won't be deployed (git push only sends commits)."
|
|
fi
|
|
|
|
GIT_URL=$(git config --get "remote.${GIT_REMOTE}.url") \
|
|
|| fail "no remote '${GIT_REMOTE}' configured locally"
|
|
|
|
# ── 1. local push ───────────────────────────────────────────────────────
|
|
say "pushing ${B}${BRANCH}${Z} to ${B}${GIT_REMOTE}${Z}…"
|
|
git push "${GIT_REMOTE}" "${BRANCH}" || fail "git push failed — fix and retry"
|
|
LOCAL_REV=$(git rev-parse --short HEAD)
|
|
ok "pushed ${BRANCH} @ ${LOCAL_REV}"
|
|
|
|
# ── 2. remote bring-up ──────────────────────────────────────────────────
|
|
say "deploying to ${B}${HOST}:${REMOTE_PATH}${Z}…"
|
|
|
|
COMPOSE_PROFILES=""
|
|
[[ -n "$WITH_GPU" ]] && COMPOSE_PROFILES="--profile gpu"
|
|
|
|
# heredoc runs on the prod box. Local vars are interpolated by THIS shell;
|
|
# remote vars start with \$ so they're set on the remote side.
|
|
ssh -o StrictHostKeyChecking=accept-new -T "${HOST}" bash -s <<REMOTE
|
|
set -euo pipefail
|
|
HOST_PATH="${REMOTE_PATH}"
|
|
BRANCH="${BRANCH}"
|
|
GIT_URL="${GIT_URL}"
|
|
COMPOSE_PROFILES="${COMPOSE_PROFILES}"
|
|
|
|
prn() { printf ' · %s\n' "\$*"; }
|
|
|
|
# 2a. ensure dir + working tree
|
|
if [[ ! -d "\$HOST_PATH/.git" ]]; then
|
|
prn "no working tree at \$HOST_PATH — cloning"
|
|
mkdir -p "\$(dirname "\$HOST_PATH")"
|
|
git clone "\$GIT_URL" "\$HOST_PATH"
|
|
fi
|
|
cd "\$HOST_PATH"
|
|
|
|
# 2b. fetch + checkout + pull
|
|
prn "git fetch origin"
|
|
git fetch --quiet origin
|
|
prn "git checkout \$BRANCH"
|
|
git checkout --quiet "\$BRANCH" 2>/dev/null || git checkout --quiet -b "\$BRANCH" "origin/\$BRANCH"
|
|
prn "git pull --ff-only origin \$BRANCH"
|
|
git pull --quiet --ff-only origin "\$BRANCH"
|
|
REMOTE_REV=\$(git rev-parse --short HEAD)
|
|
prn "now at \$REMOTE_REV"
|
|
|
|
# 2c. .env sanity
|
|
if [[ ! -f .env ]]; then
|
|
prn "WARNING: .env missing — copying .env.example. Edit it before psyc fetch-all will work."
|
|
cp .env.example .env
|
|
fi
|
|
|
|
# 2d. external 'backend' network for nginx-proxy
|
|
if ! docker network ls --format '{{.Name}}' | grep -qx backend; then
|
|
prn "creating external docker network 'backend'"
|
|
docker network create backend
|
|
fi
|
|
|
|
# 2e. compose up
|
|
prn "docker compose up -d --build \$COMPOSE_PROFILES"
|
|
docker compose up -d --build \$COMPOSE_PROFILES
|
|
|
|
prn "container status:"
|
|
docker compose ps --format "table {{.Name}}\t{{.Status}}" | sed 's/^/ /'
|
|
REMOTE
|
|
|
|
ok "remote bring-up complete"
|
|
|
|
# ── 3. internal health probe (on the prod box localhost) ───────────────
|
|
say "probing ${B}127.0.0.1:8767/healthz${Z} on prod (up to 90s)…"
|
|
REMOTE_HEALTH=$(ssh -o StrictHostKeyChecking=accept-new "${HOST}" '
|
|
for i in $(seq 1 45); do
|
|
if curl -fs http://127.0.0.1:8767/healthz >/dev/null 2>&1; then echo OK; exit 0; fi
|
|
sleep 2
|
|
done
|
|
echo TIMEOUT')
|
|
|
|
if [[ "${REMOTE_HEALTH}" != *OK* ]]; then
|
|
fail "cockpit unhealthy on prod after 90s — ssh ${HOST}, cd ${REMOTE_PATH}, run 'docker compose logs cockpit' to debug"
|
|
fi
|
|
ok "cockpit healthy on prod"
|
|
|
|
# ── 4. external probe via the public URL ────────────────────────────────
|
|
say "probing ${B}${PUBLIC_URL}/healthz${Z} from here…"
|
|
if curl --max-time 8 -fs "${PUBLIC_URL}/healthz" >/dev/null 2>&1; then
|
|
INF=$(curl --max-time 5 -s "${PUBLIC_URL}/api/inference-status" || printf '%s' '{}')
|
|
ok "${PUBLIC_URL} is LIVE"
|
|
printf " inference: %s\n" "${INF}"
|
|
else
|
|
warn "public URL not reachable from here — most likely DNS or TLS isn't finished"
|
|
warn " diag:"
|
|
warn " dig +short psyc.neuronetz.ai → expect A record to prod IP"
|
|
warn " on the prod-host: docker logs acme-companion --tail 30"
|
|
warn " cockpit IS healthy on prod-internal :8767 — the app is fine, the front isn't there yet"
|
|
fi
|
|
|
|
ok "done — deployed ${BRANCH} @ ${LOCAL_REV}"
|