From f1fd00789474bebb5893087fec90669b8d637494 Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Wed, 9 Sep 2026 15:17:44 -0400 Subject: [PATCH] feat: add production docker packaging --- .dockerignore | 11 ++++++ Dockerfile.node | 20 +++++++++++ Dockerfile.web | 12 +++++++ compose.production.yaml | 75 +++++++++++++++++++++++++++++++++++++++++ deploy/nginx.conf | 16 +++++++++ docs/DEPLOYMENT.md | 24 +++++++++++++ 6 files changed, 158 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile.node create mode 100644 Dockerfile.web create mode 100644 compose.production.yaml create mode 100644 deploy/nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..391ba39 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +.env +.env.* +!.env.example +.git +.gitignore +.local +coverage +dist +apps/web/storybook-static +test-results diff --git a/Dockerfile.node b/Dockerfile.node new file mode 100644 index 0000000..ffec597 --- /dev/null +++ b/Dockerfile.node @@ -0,0 +1,20 @@ +FROM node:22-bookworm-slim AS base +WORKDIR /app +COPY package.json package-lock.json ./ +COPY apps/api/package.json apps/api/package.json +COPY apps/worker/package.json apps/worker/package.json +COPY apps/web/package.json apps/web/package.json +COPY packages/contracts/package.json packages/contracts/package.json +COPY packages/validation/package.json packages/validation/package.json +RUN npm ci +COPY apps/api apps/api +COPY apps/worker apps/worker +COPY packages packages +COPY database database +COPY drizzle.config.js . + +FROM base AS api +CMD ["npm", "run", "start", "--workspace=@pokemon-inventory/api"] + +FROM base AS worker +CMD ["npm", "run", "start", "--workspace=@pokemon-inventory/worker"] diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..500a2b1 --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,12 @@ +FROM node:22-bookworm-slim AS build +WORKDIR /app +COPY package.json package-lock.json ./ +COPY apps/web/package.json apps/web/package.json +RUN npm ci +COPY apps/web apps/web +RUN npm run build --workspace=@pokemon-inventory/web + +FROM nginx:1.27-alpine +COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/apps/web/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/compose.production.yaml b/compose.production.yaml new file mode 100644 index 0000000..55e1b49 --- /dev/null +++ b/compose.production.yaml @@ -0,0 +1,75 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:?Set POSTGRES_DB} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD} + POSTGRES_USER: ${POSTGRES_USER:?Set POSTGRES_USER} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 3s + retries: 10 + + redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 10 + + api: + build: + context: . + dockerfile: Dockerfile.node + target: api + restart: unless-stopped + env_file: .env + environment: + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + REDIS_URL: redis://redis:6379 + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + expose: + - "3000" + + worker: + build: + context: . + dockerfile: Dockerfile.node + target: worker + restart: unless-stopped + env_file: .env + environment: + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + REDIS_URL: redis://redis:6379 + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + + web: + build: + context: . + dockerfile: Dockerfile.web + restart: unless-stopped + depends_on: + - api + ports: + - "${WEB_PORT:-8080}:80" + +volumes: + postgres-data: + redis-data: diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..5350b34 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + + location /api/ { + proxy_pass http://api:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index fd90190..a8d12e4 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -21,6 +21,30 @@ Persistent data: - Redis data where needed - image storage +## Production Compose + +The repository includes `compose.production.yaml` with separate API, worker, +web, PostgreSQL, and Redis services. It exposes only the Nginx web service and +keeps database and Redis ports internal to the Compose network. + +Before starting, create a production `.env` containing real values for +`POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `DATABASE_URL`, +`REDIS_URL`, `SMTP_URL`, `SMTP_FROM`, and a stable base64-encoded 32-byte +`MFA_ENCRYPTION_KEY`. Do not use the fixture pricing provider in production. + +Start the stack and apply migrations with: + +```sh +docker compose -f compose.production.yaml up -d --build postgres redis +docker compose -f compose.production.yaml run --rm api npm run db:migrate +docker compose -f compose.production.yaml up -d --build api worker web +``` + +The default web port is `8080` and can be changed with `WEB_PORT`. The API and +worker images share the same Node workspace image; the web image builds the Vue +application and serves it through Nginx with SPA fallback and internal `/api` +proxying. + ## Reverse proxy Responsible for: