Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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
103 changes: 103 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
20 changes: 20 additions & 0 deletions docker/entrypoint-api.sh
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions docker/entrypoint-worker.sh
Original file line number Diff line number Diff line change
@@ -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}"
Loading