From a891b56360a63df907ea5d6c73a1978d99e1429c Mon Sep 17 00:00:00 2001 From: sylvain p Date: Thu, 7 May 2026 12:11:36 +0200 Subject: [PATCH] Add Docker stack for self-hosted deployment Brings full Postgres + api + web compose setup so the app can run on any machine with just Docker, instead of requiring a host PostgreSQL install. Includes a one-shot migrate service that pushes the Drizzle schema before the API starts. Web image serves the Vite build via nginx and proxies /api/* to the api-server. Co-Authored-By: Claude Opus 4.7 (1M context) --- .dockerignore | 11 +++++++++ .env.example | 4 ++++ .gitignore | 4 ++++ docker-compose.yml | 56 +++++++++++++++++++++++++++++++++++++++++++ docker/api.Dockerfile | 34 ++++++++++++++++++++++++++ docker/nginx.conf | 20 ++++++++++++++++ docker/web.Dockerfile | 31 ++++++++++++++++++++++++ 7 files changed, 160 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 docker-compose.yml create mode 100644 docker/api.Dockerfile create mode 100644 docker/nginx.conf create mode 100644 docker/web.Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3a5c690 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +**/node_modules +**/dist +**/.cache +**/.local +**/*.tsbuildinfo +.git +.replit +.replitignore +.vscode +.idea +.DS_Store diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..02515af --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +POSTGRES_USER=cra +POSTGRES_PASSWORD=cra +POSTGRES_DB=cra +WEB_PORT=8080 diff --git a/.gitignore b/.gitignore index 12bc7fa..58d0c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,7 @@ Thumbs.db # Replit .cache/ .local/ + +# Local env +.env + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0df0bc7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-cra} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cra} + POSTGRES_DB: ${POSTGRES_DB:-cra} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cra} -d ${POSTGRES_DB:-cra}"] + interval: 5s + timeout: 3s + retries: 10 + + migrate: + build: + context: . + dockerfile: docker/api.Dockerfile + target: migrate + environment: + DATABASE_URL: postgres://${POSTGRES_USER:-cra}:${POSTGRES_PASSWORD:-cra}@postgres:5432/${POSTGRES_DB:-cra} + depends_on: + postgres: + condition: service_healthy + restart: "no" + + api: + build: + context: . + dockerfile: docker/api.Dockerfile + target: runtime-api + restart: unless-stopped + environment: + DATABASE_URL: postgres://${POSTGRES_USER:-cra}:${POSTGRES_PASSWORD:-cra}@postgres:5432/${POSTGRES_DB:-cra} + PORT: "3000" + NODE_ENV: production + depends_on: + postgres: + condition: service_healthy + migrate: + condition: service_completed_successfully + + web: + build: + context: . + dockerfile: docker/web.Dockerfile + restart: unless-stopped + ports: + - "${WEB_PORT:-8080}:80" + depends_on: + - api + +volumes: + postgres_data: diff --git a/docker/api.Dockerfile b/docker/api.Dockerfile new file mode 100644 index 0000000..5479c4b --- /dev/null +++ b/docker/api.Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1.7 + +FROM node:24-alpine AS base +RUN npm install -g pnpm@10 +WORKDIR /repo + +FROM base AS deps +COPY .npmrc package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json tsconfig.json ./ +COPY artifacts/api-server/package.json artifacts/api-server/ +COPY artifacts/cra-app/package.json artifacts/cra-app/ +COPY artifacts/mockup-sandbox/package.json artifacts/mockup-sandbox/ +COPY lib/api-client-react/package.json lib/api-client-react/ +COPY lib/api-spec/package.json lib/api-spec/ +COPY lib/api-zod/package.json lib/api-zod/ +COPY lib/db/package.json lib/db/ +COPY scripts/package.json scripts/ +RUN pnpm install --frozen-lockfile + +FROM deps AS build-api +COPY lib lib +COPY artifacts/api-server artifacts/api-server +RUN pnpm --filter @workspace/api-server run build + +FROM node:24-alpine AS runtime-api +WORKDIR /app +COPY --from=build-api /repo/artifacts/api-server/dist ./dist +ENV NODE_ENV=production +EXPOSE 3000 +CMD ["node", "--enable-source-maps", "dist/index.mjs"] + +FROM deps AS migrate +COPY lib lib +WORKDIR /repo +CMD ["pnpm", "--filter", "@workspace/db", "run", "push-force"] diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..03645b2 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://api: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; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile new file mode 100644 index 0000000..d557380 --- /dev/null +++ b/docker/web.Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1.7 + +FROM node:24-slim AS base +RUN npm install -g pnpm@10 +WORKDIR /repo + +FROM base AS deps +COPY .npmrc package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json tsconfig.json ./ +COPY artifacts/api-server/package.json artifacts/api-server/ +COPY artifacts/cra-app/package.json artifacts/cra-app/ +COPY artifacts/mockup-sandbox/package.json artifacts/mockup-sandbox/ +COPY lib/api-client-react/package.json lib/api-client-react/ +COPY lib/api-spec/package.json lib/api-spec/ +COPY lib/api-zod/package.json lib/api-zod/ +COPY lib/db/package.json lib/db/ +COPY scripts/package.json scripts/ +RUN pnpm install --frozen-lockfile + +FROM deps AS build +COPY lib lib +COPY artifacts/cra-app artifacts/cra-app +COPY attached_assets attached_assets +ENV NODE_ENV=production +ENV PORT=80 +ENV BASE_PATH=/ +RUN pnpm --filter @workspace/cra-app run build + +FROM nginx:alpine AS runtime +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /repo/artifacts/cra-app/dist/public /usr/share/nginx/html +EXPOSE 80