From 37c1a5944aec26264222b392c3e0e5674de831c8 Mon Sep 17 00:00:00 2001 From: ollyhearn Date: Sat, 6 Jun 2026 13:00:27 +0300 Subject: [PATCH] feat: docker & build --- .dockerignore | 10 ++++++++++ dockerfiles/Dockerfile.dev | 23 +++++++++++++++++++++++ dockerfiles/Dockerfile.prod | 29 +++++++++++++++++++++++++++++ dockerfiles/nginx.conf | 21 +++++++++++++++++++++ package-lock.json | 5 +++-- package.json | 2 +- rsbuild.config.ts | 14 ++++++++++++++ 7 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 dockerfiles/Dockerfile.dev create mode 100644 dockerfiles/Dockerfile.prod create mode 100644 dockerfiles/nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7d40bb3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.gitignore +node_modules +dist +storybook-static +.env +*.md +tests/ +.claude/ +.agents/ diff --git a/dockerfiles/Dockerfile.dev b/dockerfiles/Dockerfile.dev new file mode 100644 index 0000000..0bcfc12 --- /dev/null +++ b/dockerfiles/Dockerfile.dev @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile:1 +# DEV image: source bind-mounted by compose, rsbuild dev server with HMR. +# node_modules lives in a named volume (see compose) so the host dir never +# shadows the container install. Build context = mcma-webui/. +FROM node:22-slim + +# `modern-sk` is a git dependency (git+https://...) — npm needs git to fetch it. +RUN apt-get update \ + && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY package.json package-lock.json modern-sk-*.tgz ./ +RUN npm ci + +COPY . . + +EXPOSE 3000 + +# host 0.0.0.0 + HMR client port come from env (RSBUILD_* in compose). +# `--open` is dropped on purpose: no browser in a container. +CMD ["npx", "rsbuild", "dev"] diff --git a/dockerfiles/Dockerfile.prod b/dockerfiles/Dockerfile.prod new file mode 100644 index 0000000..1198b76 --- /dev/null +++ b/dockerfiles/Dockerfile.prod @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 +# PROD image: build static SPA, serve with nginx. Self-contained. +# Build context = mcma-webui/. The future prod compose puts a real proxy in +# front; this image just serves the built assets with SPA fallback. + +# -- build stage ---------------------------------------------------------- +FROM node:22-slim AS build + +WORKDIR /app + +COPY package.json package-lock.json modern-sk-*.tgz ./ +RUN npm ci + +COPY . . + +# Bake the API base URL at build time (rsbuild inlines PUBLIC_* vars). +# Same-origin default ('/api/v1') works behind any reverse proxy. +ARG PUBLIC_API_BASE_URL=/api/v1 +ENV PUBLIC_API_BASE_URL=$PUBLIC_API_BASE_URL +RUN npm run build + +# -- runtime stage -------------------------------------------------------- +FROM nginx:1.27-alpine AS runtime + +COPY dockerfiles/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/dockerfiles/nginx.conf b/dockerfiles/nginx.conf new file mode 100644 index 0000000..9652299 --- /dev/null +++ b/dockerfiles/nginx.conf @@ -0,0 +1,21 @@ +# Static SPA server baked into the webui PROD image (Dockerfile.prod). +# Only serves the built assets + SPA fallback. API routing belongs to the +# outer reverse proxy in the future prod compose. +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Long-cache hashed build assets. + location /static/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # SPA: every unknown path falls back to index.html (client-side router). + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/package-lock.json b/package-lock.json index e5796c4..739015b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@phosphor-icons/react": "^2.1.10", "@reduxjs/toolkit": "^2.12.0", - "modern-sk": "git+https://git.ollyhearn.ru/olly/modern-sk.git", + "modern-sk": "file:./modern-sk-0.1.2.tgz", "react": "^19.2.6", "react-dom": "^19.2.6", "react-redux": "^9.3.0", @@ -3879,7 +3879,8 @@ }, "node_modules/modern-sk": { "version": "0.1.2", - "resolved": "git+https://git.ollyhearn.ru/olly/modern-sk.git#37f0592464b5ba88373627d47ed534b7c6807d96", + "resolved": "file:modern-sk-0.1.2.tgz", + "integrity": "sha512-tKSxbtUxT0CLkGc8DK+SABlVmKsMqqQr61uvAJ8EDcrutzm+VD230hTRVzk9hp2oSo6nXeeMig7KS8v0Lz5mWw==", "license": "MIT", "dependencies": { "@phosphor-icons/react": "^2.1.10", diff --git a/package.json b/package.json index 136e14b..7931225 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dependencies": { "@phosphor-icons/react": "^2.1.10", "@reduxjs/toolkit": "^2.12.0", - "modern-sk": "git+https://git.ollyhearn.ru/olly/modern-sk.git", + "modern-sk": "file:./modern-sk-0.1.2.tgz", "react": "^19.2.6", "react-dom": "^19.2.6", "react-redux": "^9.3.0", diff --git a/rsbuild.config.ts b/rsbuild.config.ts index ba21de4..35fdd33 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -2,7 +2,21 @@ import { defineConfig } from '@rsbuild/core'; import { pluginBabel } from '@rsbuild/plugin-babel'; import { pluginReact } from '@rsbuild/plugin-react'; +// In docker dev the container binds 0.0.0.0:3000 and the browser reaches it +// through nginx on :80 — so HMR must be told the client-facing port. All three +// vars are unset for a plain `npm run dev`, where the defaults apply. +const { RSBUILD_HOST, RSBUILD_PORT, RSBUILD_HMR_CLIENT_PORT } = process.env; + export default defineConfig({ + server: { + host: RSBUILD_HOST ?? 'localhost', + port: RSBUILD_PORT ? Number(RSBUILD_PORT) : 3000, + }, + dev: { + client: RSBUILD_HMR_CLIENT_PORT + ? { port: Number(RSBUILD_HMR_CLIENT_PORT) } + : undefined, + }, plugins: [ pluginReact(), pluginBabel({