Files
fleetforge/infrastructure/deploy/deploy.sh
T

207 lines
7.3 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
# ============================================================================
# Fleet Forge deployer
# Polls the git repo, rebuilds and redeploys the app containers on a single
# docker host (tested on Unraid). The deployer only orchectrates: the actual
# app containers (api, worker, web, nginx) run on the HOST docker daemon via
# the mounted docker socket.
#
# Required mounts:
# /var/run/docker.sock:/var/run/docker.sock
# <host>/src:/app/src persistent git checkout + state
# <host>/certs:/certs TLS certificates (rw, generated on demand)
# ff-nginx-conf:/cfg shared nginx config named volume (rw)
# create: docker volume create ff-nginx-conf
# ============================================================================
log() { echo "[deployer] $(date '+%F %T') $*"; }
die() { log "ERROR: $*"; exit 1; }
# --- Environment (with sane defaults) ---------------------------------------
GIT_REMOTE="${GIT_REMOTE:-https://git.pepe44.dev/Pepe44DEV/fleetforge.git}"
GIT_USER="${GIT_USER:-Pepe44DEV}"
GIT_TOKEN="${GIT_TOKEN:-}"
GIT_BRANCH="${GIT_BRANCH:-master}"
POLL_INTERVAL="${POLL_INTERVAL:-300}"
HOST_IP="${HOST_IP:-10.0.0.15}"
API_PORT="${API_PORT:-3005}"
HTTPS_PORT="${HTTPS_PORT:-5443}"
HTTP_PORT="${HTTP_PORT:-5080}"
NETWORK="${NETWORK:-fleetforge-net}"
DATABASE_URL="${DATABASE_URL:-}"
JWT_SECRET="${JWT_SECRET:-}"
SRC=/app/src
STATE_FILE="$SRC/.git/ff-deployed-commit"
NGINX_SRC=/etc/fleetforge/nginx.conf
NGINX_DEST=/cfg/fleetforge.conf
CERT_DIR=/certs
# Basic checks ----------------------------------------------------------------
[ -z "$GIT_TOKEN" ] && die "GIT_TOKEN ist nicht gesetzt"
[ -z "$DATABASE_URL" ] && die "DATABASE_URL ist nicht gesetzt"
[ -z "$JWT_SECRET" ] && die "JWT_SECRET ist nicht gesetzt"
command -v docker >/dev/null || die "docker CLI fehlt"
docker info >/dev/null 2>&1 || die "docker socket nicht erreichbar"
AUTH_URL=$(echo "$GIT_REMOTE" | sed "s#https://#https://$GIT_USER:$GIT_TOKEN@#")
CORS_ORIGINS="https://$HOST_IP:$HTTPS_PORT,http://$HOST_IP:$API_PORT"
# --- Git ---------------------------------------------------------------------
clone_or_pull() {
if [ ! -d "$SRC/.git" ]; then
log "initial clone: $GIT_REMOTE ($GIT_BRANCH)"
git clone --depth 1 --branch "$GIT_BRANCH" --single-branch "$AUTH_URL" "$SRC"
else
git -C "$SRC" fetch origin "$GIT_BRANCH"
git -C "$SRC" reset --hard "origin/$GIT_BRANCH"
git -C "$SRC" clean -fd
fi
git -C "$SRC" rev-parse "origin/$GIT_BRANCH"
}
# --- Shared docker networking / config ---------------------------------------
ensure_network() {
docker network inspect "$NETWORK" >/dev/null 2>&1 || docker network create "$NETWORK"
}
ensure_certs() {
if [ ! -f "$CERT_DIR/fleetforge.pem" ] || [ ! -f "$CERT_DIR/fleetforge-key.pem" ]; then
log "generating self-signed certificate for $HOST_IP"
mkdir -p "$CERT_DIR"
openssl req -x509 -nodes -newkey rsa:2048 -days 3650 -sha256 \
-keyout "$CERT_DIR/fleetforge-key.pem" \
-out "$CERT_DIR/fleetforge.pem" \
-subj "/CN=fleetforge.local" \
-addext "subjectAltName=IP:$HOST_IP,DNS:localhost,DNS:fleetforge.local,DNS:*.local"
fi
chmod 644 "$CERT_DIR/fleetforge.pem"
chmod 600 "$CERT_DIR/fleetforge-key.pem"
cp "$NGINX_SRC" "$NGINX_DEST"
}
# --- Build ---------------------------------------------------------------------
build_images() {
log "building images (api builder + migrations first)"
docker build -f "$SRC/infrastructure/docker/Dockerfile.api" --target builder \
-t fleetforge/api:builder "$SRC"
[ $? -eq 0 ] || die "api builder build failed"
log "running migrations + seed"
docker run --rm -e "DATABASE_URL=$DATABASE_URL" fleetforge/api:builder \
sh -c "cd /app/apps/api && pnpm migrate && pnpm seed"
[ $? -eq 0 ] || die "migrate/seed failed"
log "building api runner image"
docker build -f "$SRC/infrastructure/docker/Dockerfile.api" --target runner \
-t fleetforge/api:latest "$SRC"
[ $? -eq 0 ] || die "api build failed"
log "building worker image"
docker build -f "$SRC/infrastructure/docker/Dockerfile.worker" \
-t fleetforge/worker:latest "$SRC"
[ $? -eq 0 ] || die "worker build failed"
log "building web image (NEXT_PUBLIC_API_URL=$CORS_ORIGINS)"
docker build --build-arg "NEXT_PUBLIC_API_URL=https://$HOST_IP:$HTTPS_PORT/api" \
-f "$SRC/infrastructure/docker/Dockerfile.web" \
-t fleetforge/web:latest "$SRC"
[ $? -eq 0 ] || die "web build failed"
}
# --- Run -----------------------------------------------------------------------
run_containers() {
log "stopping old containers"
for c in ff-api ff-worker ff-web ff-nginx; do
docker rm -f "$c" >/dev/null 2>&1 || true
done
log "starting api (http $API_PORT for agents)"
docker run -d --name ff-api --restart unless-stopped --network "$NETWORK" \
-p "$API_PORT:3000" \
-e NODE_ENV=production \
-e "DATABASE_URL=$DATABASE_URL" \
-e "JWT_SECRET=$JWT_SECRET" \
-e "CORS_ORIGINS=$CORS_ORIGINS" \
-e "SOFTWARE_DIR=/app/infrastructure/software-releases" \
-e "AGENT_RELEASES_DIR=/app/infrastructure/agent-releases" \
-e "INSTALLER_DIR=/app/infrastructure/installer" \
fleetforge/api:latest
log "starting worker"
docker run -d --name ff-worker --restart unless-stopped --network "$NETWORK" \
-e NODE_ENV=production \
-e "DATABASE_URL=$DATABASE_URL" \
fleetforge/worker:latest
log "starting web"
docker run -d --name ff-web --restart unless-stopped --network "$NETWORK" \
-e NODE_ENV=production \
fleetforge/web:latest
log "starting nginx reverse proxy (https $HTTPS_PORT, http $HTTP_PORT)"
docker volume create ff-nginx-conf >/dev/null 2>&1 || true
cp "$NGINX_SRC" /cfg/fleetforge.conf
docker run -d --name ff-nginx --restart unless-stopped --network "$NETWORK" \
-p "$HTTPS_PORT:443" \
-p "$HTTP_PORT:80" \
-v "$CERT_DIR:/etc/nginx/certs:ro" \
-v "ff-nginx-conf:/etc/nginx/conf.d:ro" \
nginx:1.27-alpine
}
# --- Health ---------------------------------------------------------------------
wait_for_health() {
local tries=${1:-30}
for i in $(seq 1 "$tries"); do
if curl -fsS "http://$HOST_IP:$API_PORT/api/health" >/dev/null 2>&1; then
log "API healthy after ${i} attempts"
return 0
fi
sleep 5
done
log "WARNING: API health check failed after $tries attempts"
return 1
}
# --- Deploy ----------------------------------------------------------------------
deploy() {
build_images
ensure_network
ensure_certs
run_containers
wait_for_health || return 1
log "deployment complete: https://$HOST_IP:$HTTPS_PORT"
return 0
}
# ---------------------------------------------------------------------------------
log "Fleet Forge deployer started (poll every ${POLL_INTERVAL}s, branch $GIT_BRANCH)"
mkdir -p "$SRC" "$CERT_DIR" "/cfg"
while true; do
if ! HEAD=$(clone_or_pull 2>>/dev/stderr); then
log "git update failed, retrying in ${POLL_INTERVAL}s"
sleep "$POLL_INTERVAL"
continue
fi
LAST=""
[ -f "$STATE_FILE" ] && LAST=$(cat "$STATE_FILE")
if [ -z "$LAST" ] || [ "$HEAD" != "$LAST" ]; then
log "new commit detected: $HEAD (was: ${LAST:-none})"
if deploy; then
echo "$HEAD" > "$STATE_FILE"
else
log "deploy FAILED for $HEAD, will retry next poll"
fi
else
log "no changes (HEAD=$HEAD), sleeping ${POLL_INTERVAL}s"
fi
sleep "$POLL_INTERVAL"
done