#!/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 # /src:/app/src persistent git checkout + state # /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" \ 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