diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0758b80 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +node_modules +dist +.git +.github +coverage + +docker +docs +test + +.env* +tools +jest.config.ts +Taskfile.yml + +*.md +!README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0125a00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +# syntax=docker/dockerfile:1 + +ARG NODE_IMAGE=node:20-alpine + +# ----------------------------- +# Stage 1 — Install dependencies +# ----------------------------- +FROM ${NODE_IMAGE} AS deps + +RUN corepack enable pnpm + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ + +RUN pnpm install --frozen-lockfile + + +# ----------------------------- +# Stage 2 — Build application +# ----------------------------- +FROM ${NODE_IMAGE} AS builder + +RUN corepack enable pnpm + +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +RUN pnpm build + + +# ----------------------------- +# Stage 3 — Production deps only +# ----------------------------- +FROM ${NODE_IMAGE} AS prod-deps + +RUN corepack enable pnpm + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ + +RUN pnpm install --prod --frozen-lockfile + + +# ----------------------------- +# Stage 4 — Production runtime +# ----------------------------- +FROM ${NODE_IMAGE} AS production + +ENV NODE_ENV=production +ENV PORT=3000 + +WORKDIR /app + +# production dependencies +COPY --chown=node:node --from=prod-deps /app/node_modules ./node_modules + +# compiled application +COPY --chown=node:node --from=builder /app/dist ./dist + +# migrations (needed for runtime migrations if used) +COPY --chown=node:node --from=builder /app/migrations ./migrations + +# optional runtime metadata +COPY --chown=node:node package.json ./ + +EXPOSE ${PORT} + +# healthcheck using your /health endpoint +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \ + CMD wget -qO- http://localhost:${PORT}/health || exit 1 + +# run as non-root user +USER node + +CMD ["node", "dist/main.js"] diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 0ae9db4..fce3756 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -1,7 +1,6 @@ services: postgres: image: postgres:16 - container_name: reservation-dev-postgres restart: unless-stopped environment: POSTGRES_DB: reservation_db diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index b2d5329..7a5757e 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -1,13 +1,45 @@ services: + postgres: image: postgres:16 - container_name: reservation-prod-postgres + restart: unless-stopped environment: POSTGRES_DB: reservation_db POSTGRES_USER: reservation_app POSTGRES_PASSWORD: ${DATABASE_PASSWORD} volumes: - postgres_prod_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U reservation_app -d reservation_db"] + interval: 5s + timeout: 5s + retries: 5 + + migrate: + image: reservation-api + container_name: reservation-prod-migrate + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgres://reservation_app:${DATABASE_PASSWORD}@postgres:5432/reservation_db + command: ["node", "dist/tools/migrate.js"] + restart: "no" + + api: + image: reservation-api + container_name: reservation-prod-api + depends_on: + postgres: + condition: service_healthy + migrate: + condition: service_completed_successfully + environment: + NODE_ENV: production + DATABASE_URL: postgres://reservation_app:${DATABASE_PASSWORD}@postgres:5432/reservation_db + ports: + - "3000:3000" + restart: unless-stopped volumes: postgres_prod_data: diff --git a/docker/docker-compose.test.yml b/docker/docker-compose.test.yml index 913c613..cdd0e2f 100644 --- a/docker/docker-compose.test.yml +++ b/docker/docker-compose.test.yml @@ -1,7 +1,6 @@ services: postgres_test: image: postgres:16 - container_name: barber_test_db restart: no environment: POSTGRES_DB: barber_test