diff --git a/infra/docker/Dockerfile.backend b/infra/docker/Dockerfile.backend index ee01a33..71b81a4 100644 --- a/infra/docker/Dockerfile.backend +++ b/infra/docker/Dockerfile.backend @@ -5,17 +5,20 @@ WORKDIR /app # Install pnpm RUN npm install -g pnpm +# Create non-root user +RUN addgroup -S app && adduser -S -G app app + # Copy workspace files COPY pnpm-workspace.yaml . COPY pnpm-lock.yaml . COPY package.json . -# Copy packages (workspace dependencies) -COPY packages/shared ./packages/shared - # Copy patches (pnpm patchedDependencies) COPY patches ./patches +# Copy packages (workspace dependencies) +COPY packages/shared ./packages/shared + # Copy service COPY services/backend ./services/backend @@ -28,8 +31,18 @@ RUN pnpm --filter './packages/shared' run build # Build backend RUN pnpm --filter './services/backend' run build +# Own dist + node_modules by non-root user +RUN chown -R app:app /app + +# Switch to non-root user +USER app + # Expose port -EXPOSE 3001 +EXPOSE 3000 + +# Healthcheck — backend serves HTTP on port 3000 +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD wget -qO- http://localhost:3000/api/health || exit 1 # Start backend CMD ["node", "services/backend/dist/index.js"] diff --git a/infra/docker/Dockerfile.discord-gateway b/infra/docker/Dockerfile.discord-gateway index a00aa74..6a18b51 100644 --- a/infra/docker/Dockerfile.discord-gateway +++ b/infra/docker/Dockerfile.discord-gateway @@ -5,6 +5,9 @@ WORKDIR /app # Install pnpm RUN npm install -g pnpm +# Create non-root user +RUN addgroup -S app && adduser -S -G app app + # Copy workspace files COPY pnpm-workspace.yaml . COPY pnpm-lock.yaml . @@ -37,5 +40,17 @@ RUN pnpm --filter './services/discord-gateway' run build # Create recordings directory RUN mkdir -p /app/recordings +# Own everything by non-root user +RUN chown -R app:app /app + +# Switch to non-root user +USER app + +# Expose no HTTP port (gateway is internal-only) + +# Healthcheck — verify PID 1 (node) is still running (no HTTP server in this service) +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD sh -c "kill -0 1" + # Start discord gateway CMD ["node", "services/discord-gateway/dist/index.js"] diff --git a/infra/docker/Dockerfile.frontend b/infra/docker/Dockerfile.frontend index 94e3bcf..a7e4419 100644 --- a/infra/docker/Dockerfile.frontend +++ b/infra/docker/Dockerfile.frontend @@ -1,4 +1,5 @@ -FROM node:22-alpine +# ---- Builder Stage ---- +FROM node:22-alpine AS builder ARG VITE_BE_API_URL ARG VITE_BE_WS_URL @@ -25,8 +26,15 @@ RUN pnpm install --frozen-lockfile # Build frontend (env vars injected at build time) RUN VITE_BE_API_URL=${VITE_BE_API_URL} VITE_BE_WS_URL=${VITE_BE_WS_URL} pnpm --filter './services/frontend' run build -# Expose port +# ---- Runner Stage ---- +FROM nginx:alpine + +# Copy Nginx config +COPY infra/docker/nginx/nginx-frontend.conf /etc/nginx/conf.d/default.conf + +# Copy built static files from builder stage +COPY --from=builder /app/services/frontend/dist /usr/share/nginx/html + EXPOSE 3000 -# Start frontend (production preview) -CMD ["pnpm", "--filter", "./services/frontend", "run", "preview"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml index c9ccf8f..b7260cc 100644 --- a/infra/docker/docker-compose.yml +++ b/infra/docker/docker-compose.yml @@ -14,7 +14,15 @@ services: - "traefik.http.services.imphenbot.loadbalancer.server.port=80" depends_on: - backend - - frontend + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 5s + retries: 3 + deploy: + resources: + limits: + memory: 64M networks: - app-shared-net @@ -28,8 +36,17 @@ services: environment: NODE_ENV: production WEBSERVER_PORT: 3000 - depends_on: - - discord-gateway + # Backend talks to gateway via Redis+Postgres, not directly — no depends_on needed + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + start_period: 15s + retries: 3 + deploy: + resources: + limits: + memory: 256M networks: - app-shared-net @@ -44,14 +61,35 @@ services: NODE_ENV: production volumes: - ./recordings:/app/recordings + # Gateway has no HTTP server — check if PID 1 (node) is alive + healthcheck: + test: ["CMD-SHELL", "kill -0 1 || exit 1"] + interval: 30s + timeout: 5s + start_period: 30s + retries: 3 + deploy: + resources: + limits: + memory: 512M networks: - app-shared-net - # Frontend Service (React Dashboard) + # Frontend Service (React Dashboard) — Nginx serving static files frontend: image: ghcr.io/${OWNER:-mytheclipse}/bete-frontend:latest container_name: imphenbot-frontend restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/"] + interval: 30s + timeout: 5s + start_period: 5s + retries: 3 + deploy: + resources: + limits: + memory: 32M networks: - app-shared-net diff --git a/infra/docker/nginx/nginx-frontend.conf b/infra/docker/nginx/nginx-frontend.conf new file mode 100644 index 0000000..6b1c57d --- /dev/null +++ b/infra/docker/nginx/nginx-frontend.conf @@ -0,0 +1,24 @@ +# Nginx config for serving Vite-built static frontend files +server { + listen 3000; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Gzip compression for faster load times + gzip on; + gzip_types text/plain text/css application/json application/javascript image/svg+xml; + gzip_min_length 256; + + # Cache static assets (JS/CSS hashed filenames) + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA fallback — all non-file routes serve index.html + location / { + try_files $uri $uri/ /index.html; + } +}