diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f6f49008 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,58 @@ +# ============================================================================ +# Docker Build Context Exclusions +# Keep this file minimal — only exclude what is NOT needed for the build. +# ============================================================================ + +# Version control +.git +.github +.gitignore + +# Dependencies (rebuilt inside container) +node_modules + +# Build output (rebuilt inside container) +dist +build + +# Testing and coverage +tests +integrations +coverage +.nyc_output + +# Documentation (not needed for build) +docs +README.md +ROADMAP.md +PR_DESCRIPTION.md + +# Temporary / scratch files +temp-prisma +*.log +lint_*.txt +lint_output.json +server_error*.txt + +# Environment files — secrets must NEVER be in the image +.env* +!.env.example + +# IDE and editor files +.vscode +.idea +*.swp +*.swo +*~ + +# OS-specific files +.DS_Store +Thumbs.db + +# Config files not required for compilation +.prettierrc +.prettierignore +eslint.config.ts +vitest.config.js +nodemon.json +swagger-server.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d2ad7dbb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,103 @@ +# ============================================================================ +# Learnault API — Production Docker Image +# +# Multi-stage build with three stages: +# deps – install production-only node_modules (cached layer) +# build – install all deps, generate Prisma client, compile TypeScript +# runtime – minimal image with non-root user, health check, entrypoints +# +# Build: +# docker build -t learnault-api . +# +# Run (API): +# docker run -d -p 5000:5000 --env-file .env learnault-api +# +# Run (Worker): +# docker run -d --env-file .env \ +# -e WORKER_SCRIPT=dist/workers/credit.js \ +# --entrypoint ./entrypoint-worker.sh learnault-api +# +# Migrations (one-off): +# docker run --rm --env-file .env -e RUN_MIGRATIONS=true learnault-api +# ============================================================================ + +# --------------------------------------------------------------------------- +# Stage 1: Production dependencies (cache-friendly) +# --------------------------------------------------------------------------- +FROM node:20-slim AS deps + +RUN corepack enable && corepack prepare pnpm@10 --activate + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ + +RUN pnpm install --frozen-lockfile --prod + +# --------------------------------------------------------------------------- +# Stage 2: Build — TypeScript compilation + Prisma client generation +# --------------------------------------------------------------------------- +FROM node:20-slim AS build + +RUN corepack enable && corepack prepare pnpm@10 --activate + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY prisma ./prisma +COPY prisma.config.ts ./ +COPY src ./src +COPY tsconfig.json ./ + +RUN npx prisma generate +RUN pnpm build + +# --------------------------------------------------------------------------- +# Stage 3: Runtime — minimal production image +# --------------------------------------------------------------------------- +FROM node:20-slim AS runtime + +ENV NODE_ENV=production + +WORKDIR /app + +# Prisma CLI (globally) — needed by entrypoints for optional migrate deploy +RUN npm install -g prisma@7.4.2 && npm cache clean --force + +# Production dependencies from stage 1 +COPY --from=deps /app/node_modules ./node_modules + +# Generated Prisma client from stage 2 +COPY --from=build /app/node_modules/.prisma ./node_modules/.prisma + +# Compiled application +COPY --from=build /app/dist ./dist + +# Application metadata +COPY --from=build /app/package.json ./ + +# Prisma schema + migrations (required at runtime for migrate deploy) +COPY --from=build /app/prisma ./prisma +COPY --from=build /app/prisma.config.ts ./ + +# Entrypoint scripts +COPY docker/entrypoint-api.sh ./entrypoint-api.sh +COPY docker/entrypoint-worker.sh ./entrypoint-worker.sh + +RUN chmod +x entrypoint-api.sh entrypoint-worker.sh + +# Non-root user +RUN groupadd --gid 1001 appgroup && \ + useradd --uid 1001 --gid appgroup --shell /bin/sh --create-home appuser && \ + chown -R appuser:appgroup /app + +USER appuser + +EXPOSE 5000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD node -e "fetch('http://localhost:5000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +ENTRYPOINT ["./entrypoint-api.sh"] diff --git a/docker/entrypoint-api.sh b/docker/entrypoint-api.sh new file mode 100755 index 00000000..755519d2 --- /dev/null +++ b/docker/entrypoint-api.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +# --------------------------------------------------------------------------- +# Learnault API — Container Entrypoint +# +# Environment variables: +# RUN_MIGRATIONS = "true" → run `prisma migrate deploy` before starting +# PORT → listen port (default 5000) +# --------------------------------------------------------------------------- + +if [ "${RUN_MIGRATIONS}" = "true" ]; then + echo "[entrypoint] Running database migrations …" + npx prisma migrate deploy + echo "[entrypoint] Migrations applied." +fi + +PORT="${PORT:-5000}" +echo "[entrypoint] Starting API server on port ${PORT} …" +exec node dist/server.js diff --git a/docker/entrypoint-worker.sh b/docker/entrypoint-worker.sh new file mode 100755 index 00000000..70754357 --- /dev/null +++ b/docker/entrypoint-worker.sh @@ -0,0 +1,25 @@ +#!/bin/sh +set -e + +# --------------------------------------------------------------------------- +# Learnault Worker — Container Entrypoint +# +# Environment variables: +# RUN_MIGRATIONS = "true" → run `prisma migrate deploy` before starting +# WORKER_SCRIPT → path to worker entry point (required) +# --------------------------------------------------------------------------- + +if [ "${RUN_MIGRATIONS}" = "true" ]; then + echo "[entrypoint] Running database migrations …" + npx prisma migrate deploy + echo "[entrypoint] Migrations applied." +fi + +if [ -z "${WORKER_SCRIPT}" ]; then + echo "[entrypoint] ERROR: WORKER_SCRIPT environment variable is not set." >&2 + echo "[entrypoint] Set WORKER_SCRIPT to the path of your worker entry point." >&2 + exit 1 +fi + +echo "[entrypoint] Starting worker: ${WORKER_SCRIPT} …" +exec node "${WORKER_SCRIPT}"