diff --git a/Dockerfile b/Dockerfile index 50afd78..7f9f55e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# ── Build stage ────────────────────────────────────────────────────── +# ── Build stage: Python deps ──────────────────────────────────────────── FROM python:3.12-slim AS builder WORKDIR /build @@ -7,48 +7,61 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ gcc libpq-dev curl \ && rm -rf /var/lib/apt/lists/* -# Copy pyproject.toml for both api and scanner COPY apps/api/pyproject.toml ./api/pyproject.toml COPY apps/scanner/pyproject.toml ./scanner/pyproject.toml -# Install API dependencies RUN pip install --no-cache-dir --prefix=/install api/. - -# Install Scanner dependencies (Playwright + Chromium) -# PYTHONPATH needed because --prefix=/install doesn't auto-set site-packages path RUN pip install --no-cache-dir --prefix=/install scanner/. \ && PYTHONPATH=/install/lib/python3.12/site-packages \ /install/bin/playwright install chromium --with-deps -# ── Runtime stage ──────────────────────────────────────────────────── +# ── Build stage: banner bundle ───────────────────────────────────────── +FROM node:20-slim AS banner-builder +WORKDIR /build/banner +COPY apps/banner/package.json apps/banner/package-lock.json ./ +RUN npm ci +COPY apps/banner/ . +RUN npm run build + +# ── Build stage: admin UI ────────────────────────────────────────────── +FROM node:20-slim AS admin-builder +WORKDIR /build/admin +COPY apps/admin-ui/package.json apps/admin-ui/package-lock.json ./ +RUN npm ci +COPY apps/admin-ui/ . +COPY --from=banner-builder /build/banner/dist/ ./public/ +RUN npx vite build + +# ── Runtime stage ────────────────────────────────────────────────────── FROM python:3.12-slim WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ - libpq5 curl tini supervisor \ + libpq5 curl tini supervisor nginx \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean -# Copy installed dependencies from builder +# Copy Python deps from builder COPY --from=builder /install /usr/local # Copy application code COPY apps/api/src ./src COPY apps/scanner/src ./src_scanner -COPY supervisord.conf /etc/supervisord.conf -# Move scanner source into api structure RUN if [ -d src_scanner ]; then \ cp -r src_scanner/* src/ 2>/dev/null || true; \ fi -# Healthcheck for API -HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD curl -f http://localhost:8000/health || exit 1 +# Copy built Admin UI static files +COPY --from=admin-builder /build/admin/dist /var/www/html + +# Copy configs +COPY apps/admin-ui/nginx.conf /etc/nginx/conf.d/default.conf +COPY supervisord.conf /etc/supervisord.conf + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -f http://localhost/health || exit 1 -# Use tini as init system for proper signal handling ENTRYPOINT ["/usr/bin/tini", "--"] - -# supervisord manages multiple processes CMD ["supervisord", "-c", "/etc/supervisord.conf"] diff --git a/apps/admin-ui/nginx.conf b/apps/admin-ui/nginx.conf index 408ad97..094db1d 100644 --- a/apps/admin-ui/nginx.conf +++ b/apps/admin-ui/nginx.conf @@ -1,14 +1,17 @@ server { listen 80; - root /usr/share/nginx/html; + root /var/www/html; index index.html; + # Health check endpoint for nginx itself + location = /health { + access_log off; + return 200 "nginx ok\n"; + add_header Content-Type text/plain; + } + # Banner entry points — cross-origin script loads from customer - # sites, so they need permissive CORS. Served from the web root - # because the loader derives the bundle URL from its own origin - # (see apps/banner/src/loader.ts). Declared before the SPA - # fallback so nginx doesn't rewrite them to index.html when the - # files aren't yet built in dev. + # sites, so they need permissive CORS. location = /consent-loader.js { add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; @@ -23,24 +26,31 @@ server { try_files $uri =404; } - # SPA fallback — serve index.html for all other routes - location / { - try_files $uri $uri/ /index.html; - } - - # Proxy API requests to the backend - # Uses Docker's embedded DNS with a variable so nginx resolves at request - # time rather than at startup — prevents crash if api is temporarily down. + # Proxy API requests to FastAPI backend location /api/ { - resolver 127.0.0.11 valid=10s; - set $upstream http://api:8000; - proxy_pass $upstream; + proxy_pass http://127.0.0.1:8000; 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 /docs, /openapi.json to FastAPI (Swagger UI) + location /docs { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + } + + location /openapi.json { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + } + + # SPA fallback — serve index.html for all other routes + location / { + try_files $uri $uri/ /index.html; + } + # Cache static assets location /assets/ { expires 1y; diff --git a/supervisord.conf b/supervisord.conf index f559fa6..82f126c 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -4,8 +4,20 @@ logfile=/var/log/supervisor/supervisord.log pidfile=/var/run/supervisord.pid user=root +[program:nginx] +command=nginx -g "daemon off;" -c /etc/nginx/conf.d/default.conf +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +stopwaitsecs=5 +killasgroup=true +priority=100 + [program:api] -command=sh -c "uvicorn src.main:app --host 0.0.0.0 --port 8000 --workers ${API_WORKERS:-4} --access-log --proxy-headers --forwarded-allow-ips '*'" +command=sh -c "uvicorn src.main:app --host 127.0.0.1 --port 8000 --workers ${API_WORKERS:-4} --access-log --proxy-headers --forwarded-allow-ips '*'" directory=/app autostart=true autorestart=true @@ -15,7 +27,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 stopwaitsecs=10 killasgroup=true -priority=100 +priority=200 [program:worker] command=celery -A src.celery_app worker --loglevel=info --concurrency=2 @@ -28,7 +40,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 stopwaitsecs=30 killasgroup=true -priority=200 +priority=300 [program:beat] command=celery -A src.celery_app beat --loglevel=info @@ -41,7 +53,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 stopwaitsecs=10 killasgroup=true -priority=300 +priority=400 [program:scanner] command=python -m src.worker @@ -54,5 +66,5 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 stopwaitsecs=10 killasgroup=true -priority=400 +priority=500 environment=PYTHONUNBUFFERED="1"