diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..52ac500 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +dist +dist-ssr +coverage +.git +.env +.env.docker +.env.local +*.log +.vite +*.tsbuildinfo diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..21c9874 --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,12 @@ +# Copy to .env.docker on the server (never commit secrets). +# +# Build-time: API base URL for the Vue app (/api = nginx proxies to backend). +VITE_API_URL=/api + +# Host port for the frontend container +FRONTEND_PORT=8080 + +# Backend env (used by ../backend-nest service in compose) +JWT_SECRET=replace-with-a-long-random-secret +# Must match the public URL users open in the browser (scheme + host + port). +CORS_ORIGIN=http://your-server:8080 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..590cd42 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# syntax=docker/dockerfile:1 + +FROM node:22.12.0-bookworm-slim AS builder + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY . . + +# Baked in at build time. Default keeps browser requests on /api (nginx proxy). +ARG VITE_API_URL=/api +ENV VITE_API_URL=${VITE_API_URL} + +RUN npm run build + +FROM nginx:1.27-alpine AS production + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0d26669 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +services: + backend: + build: + context: ../backend-nest + dockerfile: Dockerfile + image: child-gold-land-backend:latest + container_name: child-gold-land-backend + env_file: + - .env.docker + environment: + NODE_ENV: production + PORT: 3000 + HOST: 0.0.0.0 + DB_PATH: /app/data/app.db + JWT_SECRET: ${JWT_SECRET} + CORS_ORIGIN: ${CORS_ORIGIN} + volumes: + - backend_sqlite:/app/data + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + networks: + - child-gold-land + + frontend: + build: + context: . + dockerfile: Dockerfile + args: + VITE_API_URL: ${VITE_API_URL:-/api} + image: child-gold-land-frontend:latest + container_name: child-gold-land-frontend + ports: + - "${FRONTEND_PORT:-8080}:80" + depends_on: + backend: + condition: service_healthy + restart: unless-stopped + networks: + - child-gold-land + +volumes: + backend_sqlite: + name: child-gold-land-sqlite + +networks: + child-gold-land: + name: child-gold-land diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..3d75493 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,27 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # API requests proxied to the NestJS backend (same Docker network). + location /api/ { + proxy_pass http://backend:3000; + 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; + } + + location /health { + proxy_pass http://backend:3000/health; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location / { + try_files $uri $uri/ /index.html; + } +}