feat: automatic deploy system (poll+deploy docker stack on Unraid)
- deployer container polls git, builds api/worker/web, runs migrations+seed, starts ff-api/ff-worker/ff-web + nginx TLS reverse proxy against external Postgres (10.0.0.15) - Dockerfile.web: drop missing packages/ui package, add NEXT_PUBLIC_API_URL arg - add root .dockerignore (build context cleanup)
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/.next
|
||||||
|
**/*.tsbuildinfo
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
infrastructure/certs
|
||||||
|
infrastructure/agent-releases
|
||||||
|
infrastructure/software-releases
|
||||||
|
agents/forge-agent/forge-agent
|
||||||
|
apps/web/.env*
|
||||||
|
.turbo
|
||||||
|
pnpm-debug.log*
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Fleet Forge deployer environment
|
||||||
|
# Werte beim docker run als -e übergeben bzw. bei Unraid im Template setzen.
|
||||||
|
|
||||||
|
# --- Git / Polling ----------------------------------------------------------
|
||||||
|
GIT_REMOTE=https://git.pepe44.dev/Pepe44DEV/fleetforge.git
|
||||||
|
GIT_USER=Pepe44DEV
|
||||||
|
GIT_TOKEN=<gitea-api-token>
|
||||||
|
GIT_BRANCH=master
|
||||||
|
POLL_INTERVAL=300
|
||||||
|
|
||||||
|
# --- Host / Ports -----------------------------------------------------------
|
||||||
|
HOST_IP=10.0.0.15
|
||||||
|
API_PORT=3005 # HTTP-Port, den die Agenten als server_url nutzen
|
||||||
|
HTTPS_PORT=5443 # Web/UI via https://10.0.0.15:5443
|
||||||
|
HTTP_PORT=5080
|
||||||
|
|
||||||
|
# --- App secrets ------------------------------------------------------------
|
||||||
|
# Wird gegen den externen Postgres auf 10.0.0.15 verwendet (DB "FleetForge").
|
||||||
|
DATABASE_URL=postgresql://admin:Start01!@10.0.0.15:5432/FleetForge?schema=public
|
||||||
|
JWT_SECRET=<starkes-secret>
|
||||||
Executable
+201
@@ -0,0 +1,201 @@
|
|||||||
|
#!/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)
|
||||||
|
# <host>/nginx:/cfg rendered nginx config (rw)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
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 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 "$NGINX_DEST:/etc/nginx/conf.d/fleetforge.conf: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
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
docker-cli \
|
||||||
|
git \
|
||||||
|
bash \
|
||||||
|
openssl \
|
||||||
|
curl \
|
||||||
|
coreutils \
|
||||||
|
findutils \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata
|
||||||
|
|
||||||
|
COPY deploy.sh /usr/local/bin/deploy.sh
|
||||||
|
COPY nginx.conf /etc/fleetforge/nginx.conf
|
||||||
|
RUN chmod +x /usr/local/bin/deploy.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/deploy.sh"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/fleetforge.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/fleetforge-key.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
|
client_max_body_size 200m;
|
||||||
|
proxy_read_timeout 1800s;
|
||||||
|
proxy_send_timeout 1800s;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://ff-api:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://ff-web:3001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,14 +6,14 @@ FROM base AS deps
|
|||||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
|
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
|
||||||
COPY apps/web/package.json ./apps/web/
|
COPY apps/web/package.json ./apps/web/
|
||||||
COPY packages/shared/package.json ./packages/shared/
|
COPY packages/shared/package.json ./packages/shared/
|
||||||
COPY packages/ui/package.json ./packages/ui/
|
|
||||||
RUN pnpm install --frozen-lockfile || pnpm install
|
RUN pnpm install --frozen-lockfile || pnpm install
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
|
ARG NEXT_PUBLIC_API_URL=http://localhost:3000/api
|
||||||
|
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
|
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
|
||||||
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
|
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
|
||||||
COPY --from=deps /app/packages/ui/node_modules ./packages/ui/node_modules
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pnpm --filter @fleetforge/shared build
|
RUN pnpm --filter @fleetforge/shared build
|
||||||
RUN pnpm --filter @fleetforge/web build
|
RUN pnpm --filter @fleetforge/web build
|
||||||
|
|||||||
Reference in New Issue
Block a user