From d84b9b5ee7e9c3361fee24c361c8965b25c520bd Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 23 Apr 2026 16:06:07 +0300 Subject: [PATCH] chore(docker): add containerized deployment and nginx proxy --- docker-compose.dev.yml | 15 ++++++++++ docker-compose.yml | 59 ++++++++++++++++++++++++++++++++++++++ docker/backend.Dockerfile | 51 ++++++++++++++++++++++++++++++++ docker/frontend.Dockerfile | 29 +++++++++++++++++++ docker/nginx.conf | 38 ++++++++++++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 docker/backend.Dockerfile create mode 100644 docker/frontend.Dockerfile create mode 100644 docker/nginx.conf diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..861fe11 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,15 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + ports: + - "5432:5432" + volumes: + - pgdata_dev:/var/lib/postgresql/data + +volumes: + pgdata_dev: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b3ef795 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,59 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 10 + + backend: + build: + context: . + dockerfile: docker/backend.Dockerfile + restart: unless-stopped + env_file: .env + environment: + NODE_ENV: production + UPLOADS_DIR: /app/apps/backend/uploads + BACKEND_PORT: 3000 + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + depends_on: + postgres: + condition: service_healthy + volumes: + - uploads:/app/apps/backend/uploads + healthcheck: + test: + [ + "CMD", + "wget", + "-qO-", + "http://127.0.0.1:3000/api/health", + ] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + + frontend: + build: + context: . + dockerfile: docker/frontend.Dockerfile + restart: unless-stopped + depends_on: + backend: + condition: service_started + ports: + - "8080:80" + +volumes: + pgdata: + uploads: diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile new file mode 100644 index 0000000..ee15854 --- /dev/null +++ b/docker/backend.Dockerfile @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:1.7 +FROM node:20-alpine AS base +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable && corepack prepare pnpm@9.12.0 --activate +RUN apk add --no-cache openssl + +# ---------- deps ---------- +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./ +COPY tsconfig.base.json ./ +COPY packages/shared/package.json packages/shared/ +COPY apps/backend/package.json apps/backend/ +COPY apps/frontend/package.json apps/frontend/ +# Prisma schema is needed before `prisma generate` so we copy it at this stage. +COPY apps/backend/prisma apps/backend/prisma +# Install only the backend workspace and its deps. +RUN pnpm install --filter @family-wishlist/backend... --frozen-lockfile || \ + pnpm install --filter @family-wishlist/backend... +# Generate @prisma/client into node_modules so it's present in the runtime stage. +RUN pnpm --filter @family-wishlist/backend prisma:generate + +# ---------- build ---------- +FROM deps AS build +WORKDIR /app +COPY packages/shared packages/shared +COPY apps/backend apps/backend +RUN pnpm --filter @family-wishlist/backend build + +# ---------- runtime ---------- +FROM base AS runtime +WORKDIR /app +ENV NODE_ENV=production +# Copy everything needed for running: hoisted node_modules (with the generated +# @prisma/client inside), workspace packages, and the compiled backend. +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./package.json +COPY --from=build /app/pnpm-workspace.yaml ./pnpm-workspace.yaml +COPY --from=build /app/packages ./packages +COPY --from=build /app/apps/backend/node_modules ./apps/backend/node_modules +COPY --from=build /app/apps/backend/dist ./apps/backend/dist +COPY --from=build /app/apps/backend/prisma ./apps/backend/prisma +COPY --from=build /app/apps/backend/package.json ./apps/backend/package.json +COPY --from=build /app/apps/backend/scripts ./apps/backend/scripts + +WORKDIR /app/apps/backend +EXPOSE 3000 +# Apply schema (idempotent; uses `db push` so no prior migrations required) + +# seed env users + start server. +CMD ["sh", "-c", "pnpm exec prisma db push --accept-data-loss --skip-generate && pnpm seed && node dist/index.js"] diff --git a/docker/frontend.Dockerfile b/docker/frontend.Dockerfile new file mode 100644 index 0000000..70b41c0 --- /dev/null +++ b/docker/frontend.Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1.7 +FROM node:20-alpine AS base +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable && corepack prepare pnpm@9.12.0 --activate + +# ---------- deps ---------- +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./ +COPY tsconfig.base.json ./ +COPY packages/shared/package.json packages/shared/ +COPY apps/backend/package.json apps/backend/ +COPY apps/frontend/package.json apps/frontend/ +RUN pnpm install --filter @family-wishlist/frontend... --frozen-lockfile || \ + pnpm install --filter @family-wishlist/frontend... + +# ---------- build ---------- +FROM deps AS build +WORKDIR /app +COPY packages/shared packages/shared +COPY apps/frontend apps/frontend +RUN pnpm --filter @family-wishlist/frontend build + +# ---------- runtime (nginx) ---------- +FROM nginx:1.27-alpine AS runtime +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/apps/frontend/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..440382d --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,38 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Static files with long cache + location ~* \.(?:js|css|woff2?|svg|png|jpg|jpeg|gif|webp|ico)$ { + expires 7d; + add_header Cache-Control "public"; + try_files $uri =404; + } + + # API proxy + location /api/ { + proxy_pass http://backend:3000/api/; + proxy_http_version 1.1; + 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_set_header Connection ""; + client_max_body_size 10m; + } + + # Uploaded files (images) + location /uploads/ { + proxy_pass http://backend:3000/uploads/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_cache_valid 200 1h; + } + + # SPA fallback + location / { + try_files $uri /index.html; + } +} -- 2.49.1