From 3975b5c51ff960b92e62dc82e0086f3d09d8248c Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 14 Feb 2026 16:35:26 +0100 Subject: [PATCH] Fix CRLF line endings in runtime/deploy scripts and enforce LF --- .gitattributes | 12 +++ docker/Dockerfile | 146 +++++++++++++------------- docker/docker-compose.yml | 168 +++++++++++++++--------------- docker/docker-entrypoint.sh | 74 ++++++------- scripts/deploy.sh | 200 ++++++++++++++++++------------------ scripts/update.sh | 90 ++++++++-------- 6 files changed, 351 insertions(+), 339 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4885a46 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +* text=auto + +# Deployment/runtime scripts must stay LF for Linux containers/shells. +*.sh text eol=lf +Dockerfile text eol=lf +**/Dockerfile text eol=lf + +# Keep YAML and env-ish config files LF across platforms. +*.yml text eol=lf +*.yaml text eol=lf +*.env text eol=lf +*.sql text eol=lf diff --git a/docker/Dockerfile b/docker/Dockerfile index 6d6d1d0..357ae6f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,73 +1,73 @@ -# ============================================================================= -# MOPC Platform - Production Dockerfile -# ============================================================================= -# Multi-stage build for optimized production image - -FROM node:22-alpine AS base - -# Install dependencies only when needed -FROM base AS deps -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# Copy package files -COPY package.json package-lock.json* ./ -RUN npm ci - -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -# Generate Prisma client -RUN npx prisma generate - -# Build Next.js -ENV NEXT_TELEMETRY_DISABLED=1 -RUN npm run build - -# Production image, copy all the files and run next -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -# Create non-root user for security -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -# Install runtime dependencies for migrations and seeding -RUN apk add --no-cache libc6-compat - -# Copy built Next.js standalone output -COPY --from=builder /app/public ./public -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static - -# Copy full node_modules for prisma migrations and seeding -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/prisma ./prisma -COPY --from=builder /app/package.json ./package.json - -# Copy files needed for seeding (tsx needs tsconfig for path resolution) -COPY --from=builder /app/docs/Candidatures2026.csv ./docs/Candidatures2026.csv -COPY --from=builder /app/tsconfig.json ./tsconfig.json - -# Copy entrypoint script -COPY docker/docker-entrypoint.sh /app/docker-entrypoint.sh -RUN chmod +x /app/docker-entrypoint.sh - -# Set correct permissions -RUN chown -R nextjs:nodejs /app - -USER nextjs - -EXPOSE 7600 - -ENV PORT=7600 -ENV HOSTNAME="0.0.0.0" - -# Run via entrypoint (migrate then start) -CMD ["/app/docker-entrypoint.sh"] +# ============================================================================= +# MOPC Platform - Production Dockerfile +# ============================================================================= +# Multi-stage build for optimized production image + +FROM node:22-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy package files +COPY package.json package-lock.json* ./ +RUN npm ci + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Generate Prisma client +RUN npx prisma generate + +# Build Next.js +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +# Create non-root user for security +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Install runtime dependencies for migrations and seeding +RUN apk add --no-cache libc6-compat + +# Copy built Next.js standalone output +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +# Copy full node_modules for prisma migrations and seeding +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/package.json ./package.json + +# Copy files needed for seeding (tsx needs tsconfig for path resolution) +COPY --from=builder /app/docs/Candidatures2026.csv ./docs/Candidatures2026.csv +COPY --from=builder /app/tsconfig.json ./tsconfig.json + +# Copy entrypoint script +COPY docker/docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh + +# Set correct permissions +RUN chown -R nextjs:nodejs /app + +USER nextjs + +EXPOSE 7600 + +ENV PORT=7600 +ENV HOSTNAME="0.0.0.0" + +# Run via entrypoint (migrate then start) +CMD ["/app/docker-entrypoint.sh"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c5a4da6..0a90e7d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,84 +1,84 @@ -# ============================================================================= -# MOPC Platform - Production Docker Compose -# ============================================================================= -# This stack contains only the Next.js app and PostgreSQL. -# MinIO and Poste.io are external services connected via environment variables. -# -# The app image is built by Gitea CI and pushed to the container registry. -# `pull_policy: always` ensures `docker compose up -d` checks for newer app images. -# The app entrypoint runs `prisma migrate deploy` before starting Next.js. - -services: - app: - image: ${REGISTRY_URL}/mopc-app:latest - pull_policy: always - container_name: mopc-app - restart: unless-stopped - dns: - - 8.8.8.8 - - 8.8.4.4 - ports: - - "127.0.0.1:7600:7600" - env_file: - - .env - environment: - - NODE_ENV=production - - DATABASE_URL=postgresql://mopc:${DB_PASSWORD}@postgres:5432/mopc - - NEXTAUTH_URL=${NEXTAUTH_URL} - - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - - AUTH_SECRET=${NEXTAUTH_SECRET} - - AUTH_TRUST_HOST=true - - MINIO_ENDPOINT=${MINIO_ENDPOINT} - - MINIO_PUBLIC_ENDPOINT=${MINIO_PUBLIC_ENDPOINT:-} - - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} - - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} - - MINIO_BUCKET=${MINIO_BUCKET} - - SMTP_HOST=${SMTP_HOST} - - SMTP_PORT=${SMTP_PORT} - - SMTP_USER=${SMTP_USER} - - SMTP_PASS=${SMTP_PASS} - - EMAIL_FROM=${EMAIL_FROM} - - POSTE_API_URL=${POSTE_API_URL:-https://mail.monaco-opc.com} - - POSTE_ADMIN_EMAIL=${POSTE_ADMIN_EMAIL} - - POSTE_ADMIN_PASSWORD=${POSTE_ADMIN_PASSWORD} - - POSTE_MAIL_DOMAIN=${POSTE_MAIL_DOMAIN:-monaco-opc.com} - - OPENAI_API_KEY=${OPENAI_API_KEY:-} - - OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o} - - MAX_FILE_SIZE=${MAX_FILE_SIZE:-524288000} - depends_on: - postgres: - condition: service_healthy - networks: - - mopc-network - healthcheck: - test: ["CMD", "node", "-e", "fetch('http://localhost:7600/api/health').then(r=>{if(!r.ok)throw r;process.exit(0)}).catch(()=>process.exit(1))"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 60s - - postgres: - image: postgres:16-alpine - container_name: mopc-postgres - restart: unless-stopped - environment: - - POSTGRES_USER=mopc - - POSTGRES_PASSWORD=${DB_PASSWORD} - - POSTGRES_DB=mopc - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U mopc"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - mopc-network - -volumes: - postgres_data: - driver: local - -networks: - mopc-network: - driver: bridge +# ============================================================================= +# MOPC Platform - Production Docker Compose +# ============================================================================= +# This stack contains only the Next.js app and PostgreSQL. +# MinIO and Poste.io are external services connected via environment variables. +# +# The app image is built by Gitea CI and pushed to the container registry. +# `pull_policy: always` ensures `docker compose up -d` checks for newer app images. +# The app entrypoint runs `prisma migrate deploy` before starting Next.js. + +services: + app: + image: ${REGISTRY_URL}/mopc-app:latest + pull_policy: always + container_name: mopc-app + restart: unless-stopped + dns: + - 8.8.8.8 + - 8.8.4.4 + ports: + - "127.0.0.1:7600:7600" + env_file: + - .env + environment: + - NODE_ENV=production + - DATABASE_URL=postgresql://mopc:${DB_PASSWORD}@postgres:5432/mopc + - NEXTAUTH_URL=${NEXTAUTH_URL} + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - AUTH_SECRET=${NEXTAUTH_SECRET} + - AUTH_TRUST_HOST=true + - MINIO_ENDPOINT=${MINIO_ENDPOINT} + - MINIO_PUBLIC_ENDPOINT=${MINIO_PUBLIC_ENDPOINT:-} + - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} + - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} + - MINIO_BUCKET=${MINIO_BUCKET} + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_USER=${SMTP_USER} + - SMTP_PASS=${SMTP_PASS} + - EMAIL_FROM=${EMAIL_FROM} + - POSTE_API_URL=${POSTE_API_URL:-https://mail.monaco-opc.com} + - POSTE_ADMIN_EMAIL=${POSTE_ADMIN_EMAIL} + - POSTE_ADMIN_PASSWORD=${POSTE_ADMIN_PASSWORD} + - POSTE_MAIL_DOMAIN=${POSTE_MAIL_DOMAIN:-monaco-opc.com} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + - OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o} + - MAX_FILE_SIZE=${MAX_FILE_SIZE:-524288000} + depends_on: + postgres: + condition: service_healthy + networks: + - mopc-network + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:7600/api/health').then(r=>{if(!r.ok)throw r;process.exit(0)}).catch(()=>process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + postgres: + image: postgres:16-alpine + container_name: mopc-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=mopc + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=mopc + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mopc"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - mopc-network + +volumes: + postgres_data: + driver: local + +networks: + mopc-network: + driver: bridge diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index e9982bb..5767050 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -1,37 +1,37 @@ -#!/bin/sh -set -eu - -MAX_MIGRATION_RETRIES="${MIGRATION_MAX_RETRIES:-30}" -MIGRATION_RETRY_DELAY_SECONDS="${MIGRATION_RETRY_DELAY_SECONDS:-2}" -ATTEMPT=1 - -echo "==> Running database migrations (with retry)..." -until npx prisma migrate deploy; do - if [ "$ATTEMPT" -ge "$MAX_MIGRATION_RETRIES" ]; then - echo "ERROR: Migration failed after ${MAX_MIGRATION_RETRIES} attempts." - exit 1 - fi - echo "Migration attempt ${ATTEMPT} failed. Retrying in ${MIGRATION_RETRY_DELAY_SECONDS}s..." - ATTEMPT=$((ATTEMPT + 1)) - sleep "$MIGRATION_RETRY_DELAY_SECONDS" -done - -echo "==> Generating Prisma client..." -npx prisma generate - -# Auto-seed on first startup: check if Users table is empty -USER_COUNT=$(node -e " - const { PrismaClient } = require('@prisma/client'); - const p = new PrismaClient(); - p.user.count().then(c => { console.log(c); p.\$disconnect(); }).catch(() => { console.log('0'); p.\$disconnect(); }); -" 2>/dev/null || echo "0") - -if [ "$USER_COUNT" = "0" ]; then - echo "==> Empty database detected — running seed..." - npx prisma db seed || echo "WARNING: Seed script failed." -else - echo "==> Database already seeded ($USER_COUNT users found), skipping seed." -fi - -echo "==> Starting application..." -exec node server.js +#!/bin/sh +set -eu + +MAX_MIGRATION_RETRIES="${MIGRATION_MAX_RETRIES:-30}" +MIGRATION_RETRY_DELAY_SECONDS="${MIGRATION_RETRY_DELAY_SECONDS:-2}" +ATTEMPT=1 + +echo "==> Running database migrations (with retry)..." +until npx prisma migrate deploy; do + if [ "$ATTEMPT" -ge "$MAX_MIGRATION_RETRIES" ]; then + echo "ERROR: Migration failed after ${MAX_MIGRATION_RETRIES} attempts." + exit 1 + fi + echo "Migration attempt ${ATTEMPT} failed. Retrying in ${MIGRATION_RETRY_DELAY_SECONDS}s..." + ATTEMPT=$((ATTEMPT + 1)) + sleep "$MIGRATION_RETRY_DELAY_SECONDS" +done + +echo "==> Generating Prisma client..." +npx prisma generate + +# Auto-seed on first startup: check if Users table is empty +USER_COUNT=$(node -e " + const { PrismaClient } = require('@prisma/client'); + const p = new PrismaClient(); + p.user.count().then(c => { console.log(c); p.\$disconnect(); }).catch(() => { console.log('0'); p.\$disconnect(); }); +" 2>/dev/null || echo "0") + +if [ "$USER_COUNT" = "0" ]; then + echo "==> Empty database detected — running seed..." + npx prisma db seed || echo "WARNING: Seed script failed." +else + echo "==> Database already seeded ($USER_COUNT users found), skipping seed." +fi + +echo "==> Starting application..." +exec node server.js diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 61562f8..b4d2c53 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,100 +1,100 @@ -#!/bin/bash -# ============================================================================= -# MOPC Platform - First-Time Deployment Script -# ============================================================================= -# Usage: ./scripts/deploy.sh -# Run this once on the Linux VPS to set up the platform. -# The Docker image is built by Gitea CI and pulled from the registry. - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -DOCKER_DIR="$PROJECT_DIR/docker" - -echo "============================================" -echo " MOPC Platform - Deployment" -echo "============================================" -echo "" - -# 1. Check Docker is available -if ! command -v docker &> /dev/null; then - echo "ERROR: Docker is not installed." - exit 1 -fi - -if ! docker compose version &> /dev/null; then - echo "ERROR: Docker Compose v2 is not available." - exit 1 -fi - -# 2. Check environment file -if [ ! -f "$DOCKER_DIR/.env" ]; then - echo "No .env file found in docker/." - echo "Copying template..." - cp "$DOCKER_DIR/.env.production" "$DOCKER_DIR/.env" - echo "" - echo "IMPORTANT: Edit docker/.env with your production values before continuing." - echo " nano $DOCKER_DIR/.env" - echo "" - exit 1 -fi - -# 3. Load registry URL from env -source "$DOCKER_DIR/.env" -if [ -z "$REGISTRY_URL" ] || [ "$REGISTRY_URL" = "CHANGE_ME" ]; then - echo "ERROR: REGISTRY_URL is not set in docker/.env" - echo "Set it to your Gitea registry URL (e.g. gitea.example.com/your-org)" - exit 1 -fi - -# 4. Log in to container registry -echo "==> Logging in to container registry ($REGISTRY_URL)..." -docker login "$REGISTRY_URL" - -# 5. Create data directories -echo "==> Creating data directories..." -sudo mkdir -p /data/mopc/postgres -sudo chown -R 1000:1000 /data/mopc - -# 6. Pull and start -echo "==> Pulling latest images and starting services..." -cd "$DOCKER_DIR" -docker compose up -d --pull always - -# 7. Wait for health check -echo "==> Waiting for application to start..." -MAX_WAIT=120 -WAITED=0 -while [ $WAITED -lt $MAX_WAIT ]; do - if curl -sf http://localhost:7600/api/health > /dev/null 2>&1; then - echo "" - echo "============================================" - echo " Application is running!" - echo "============================================" - echo "" - echo " URL: http://localhost:7600" - echo " Health: http://localhost:7600/api/health" - echo "" - echo " NEXT STEPS:" - echo " 1. Run the one-time database seed:" - echo " ./scripts/seed.sh" - echo "" - echo " 2. Set up Nginx reverse proxy:" - echo " sudo ln -s $DOCKER_DIR/nginx/mopc-platform.conf /etc/nginx/sites-enabled/" - echo " sudo nginx -t && sudo systemctl reload nginx" - echo "" - echo " 3. Set up SSL:" - echo " sudo certbot --nginx -d portal.monaco-opc.com" - echo "" - exit 0 - fi - sleep 2 - WAITED=$((WAITED + 2)) - printf "." -done - -echo "" -echo "WARNING: Application did not become healthy within ${MAX_WAIT}s." -echo "Check logs: cd $DOCKER_DIR && docker compose logs -f app" -exit 1 +#!/bin/bash +# ============================================================================= +# MOPC Platform - First-Time Deployment Script +# ============================================================================= +# Usage: ./scripts/deploy.sh +# Run this once on the Linux VPS to set up the platform. +# The Docker image is built by Gitea CI and pulled from the registry. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +DOCKER_DIR="$PROJECT_DIR/docker" + +echo "============================================" +echo " MOPC Platform - Deployment" +echo "============================================" +echo "" + +# 1. Check Docker is available +if ! command -v docker &> /dev/null; then + echo "ERROR: Docker is not installed." + exit 1 +fi + +if ! docker compose version &> /dev/null; then + echo "ERROR: Docker Compose v2 is not available." + exit 1 +fi + +# 2. Check environment file +if [ ! -f "$DOCKER_DIR/.env" ]; then + echo "No .env file found in docker/." + echo "Copying template..." + cp "$DOCKER_DIR/.env.production" "$DOCKER_DIR/.env" + echo "" + echo "IMPORTANT: Edit docker/.env with your production values before continuing." + echo " nano $DOCKER_DIR/.env" + echo "" + exit 1 +fi + +# 3. Load registry URL from env +source "$DOCKER_DIR/.env" +if [ -z "$REGISTRY_URL" ] || [ "$REGISTRY_URL" = "CHANGE_ME" ]; then + echo "ERROR: REGISTRY_URL is not set in docker/.env" + echo "Set it to your Gitea registry URL (e.g. gitea.example.com/your-org)" + exit 1 +fi + +# 4. Log in to container registry +echo "==> Logging in to container registry ($REGISTRY_URL)..." +docker login "$REGISTRY_URL" + +# 5. Create data directories +echo "==> Creating data directories..." +sudo mkdir -p /data/mopc/postgres +sudo chown -R 1000:1000 /data/mopc + +# 6. Pull and start +echo "==> Pulling latest images and starting services..." +cd "$DOCKER_DIR" +docker compose up -d --pull always + +# 7. Wait for health check +echo "==> Waiting for application to start..." +MAX_WAIT=120 +WAITED=0 +while [ $WAITED -lt $MAX_WAIT ]; do + if curl -sf http://localhost:7600/api/health > /dev/null 2>&1; then + echo "" + echo "============================================" + echo " Application is running!" + echo "============================================" + echo "" + echo " URL: http://localhost:7600" + echo " Health: http://localhost:7600/api/health" + echo "" + echo " NEXT STEPS:" + echo " 1. Run the one-time database seed:" + echo " ./scripts/seed.sh" + echo "" + echo " 2. Set up Nginx reverse proxy:" + echo " sudo ln -s $DOCKER_DIR/nginx/mopc-platform.conf /etc/nginx/sites-enabled/" + echo " sudo nginx -t && sudo systemctl reload nginx" + echo "" + echo " 3. Set up SSL:" + echo " sudo certbot --nginx -d portal.monaco-opc.com" + echo "" + exit 0 + fi + sleep 2 + WAITED=$((WAITED + 2)) + printf "." +done + +echo "" +echo "WARNING: Application did not become healthy within ${MAX_WAIT}s." +echo "Check logs: cd $DOCKER_DIR && docker compose logs -f app" +exit 1 diff --git a/scripts/update.sh b/scripts/update.sh index ea27820..09bc751 100644 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -1,45 +1,45 @@ -#!/bin/bash -# ============================================================================= -# MOPC Platform - Update / Redeploy Script -# ============================================================================= -# Usage: ./scripts/update.sh -# Pulls the latest image from the registry and restarts the app. -# PostgreSQL is NOT restarted. - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -DOCKER_DIR="$PROJECT_DIR/docker" - -echo "============================================" -echo " MOPC Platform - Update" -echo "============================================" -echo "" - -# 1. Pull and recreate app only (postgres stays running) -echo "==> Pulling latest image and recreating app..." -cd "$DOCKER_DIR" -docker compose up -d --pull always --force-recreate app - -# 2. Wait for health check -echo "==> Waiting for application to start..." -MAX_WAIT=120 -WAITED=0 -while [ $WAITED -lt $MAX_WAIT ]; do - if curl -sf http://localhost:7600/api/health > /dev/null 2>&1; then - echo "" - echo "============================================" - echo " Update complete! App is healthy." - echo "============================================" - exit 0 - fi - sleep 2 - WAITED=$((WAITED + 2)) - printf "." -done - -echo "" -echo "WARNING: Application did not become healthy within ${MAX_WAIT}s." -echo "Check logs: cd $DOCKER_DIR && docker compose logs -f app" -exit 1 +#!/bin/bash +# ============================================================================= +# MOPC Platform - Update / Redeploy Script +# ============================================================================= +# Usage: ./scripts/update.sh +# Pulls the latest image from the registry and restarts the app. +# PostgreSQL is NOT restarted. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +DOCKER_DIR="$PROJECT_DIR/docker" + +echo "============================================" +echo " MOPC Platform - Update" +echo "============================================" +echo "" + +# 1. Pull and recreate app only (postgres stays running) +echo "==> Pulling latest image and recreating app..." +cd "$DOCKER_DIR" +docker compose up -d --pull always --force-recreate app + +# 2. Wait for health check +echo "==> Waiting for application to start..." +MAX_WAIT=120 +WAITED=0 +while [ $WAITED -lt $MAX_WAIT ]; do + if curl -sf http://localhost:7600/api/health > /dev/null 2>&1; then + echo "" + echo "============================================" + echo " Update complete! App is healthy." + echo "============================================" + exit 0 + fi + sleep 2 + WAITED=$((WAITED + 2)) + printf "." +done + +echo "" +echo "WARNING: Application did not become healthy within ${MAX_WAIT}s." +echo "Check logs: cd $DOCKER_DIR && docker compose logs -f app" +exit 1