# syntax=docker/dockerfile:1.7 # All-in-one Auth Portal (Fastify + SPA + SQLite). Analog cfdm / evofw. # Закреплён по digest (воспроизводимая сборка); обновляйте вместе с node:22-alpine # (digest: docker buildx imagetools inspect node:22-alpine). ARG BASE_NODE=docker.io/library/node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 FROM ${BASE_NODE} AS deps WORKDIR /repo RUN apk add --no-cache python3 make g++ \ && corepack enable && corepack prepare pnpm@10.12.1 --activate COPY package.json pnpm-workspace.yaml pnpm-lock.yaml turbo.json ./ COPY apps/web/package.json apps/web/ COPY apps/api/package.json apps/api/ COPY packages/ui/package.json packages/ui/ COPY packages/shared/package.json packages/shared/ COPY packages/db/package.json packages/db/ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm install --frozen-lockfile FROM deps AS build RUN apk add --no-cache brotli COPY apps/web apps/web COPY apps/api apps/api COPY packages/ui packages/ui COPY packages/shared packages/shared COPY packages/db packages/db # Preview-маршруты дизайна (mock-данные) не нужны в проде: вырезаем до генерации routeTree # (-45% файлов и ~-10% gzip в бандле). Общие компоненты из src/design-preview остаются. RUN rm -f apps/web/src/routes/design-preview*.tsx # API собирается tsup в один файл (все зависимости внутри, кроме нативных). RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm turbo build --filter=web --filter=@authportal/api # Рантайм-слой: бандл + статика (только brotli) + нативные модули с теми же версиями, что в lockfile. RUN mkdir -p /out \ && cp -r apps/api/dist /out/dist \ && cp -r apps/web/dist /out/static \ && find /out/static -type f \( -name '*.js' -o -name '*.css' -o -name '*.html' -o -name '*.svg' -o -name '*.json' \) \ -exec sh -c 'brotli -f -q 11 -o "$1.br" "$1"' _ {} \; \ && node -e "const v=(p)=>require(p).version;require('fs').writeFileSync('/out/package.json',JSON.stringify({name:'authportal-runtime',private:true,type:'module',dependencies:{'better-sqlite3':v('./packages/db/node_modules/better-sqlite3/package.json'),'@node-rs/argon2':v('./apps/api/node_modules/@node-rs/argon2/package.json')}}))" RUN --mount=type=cache,target=/root/.npm,sharing=locked \ cd /out && npm install --omit=dev --no-audit --no-fund \ && rm -rf node_modules/better-sqlite3/deps node_modules/better-sqlite3/src \ node_modules/better-sqlite3/build/Release/obj node_modules/better-sqlite3/build/Release/.deps \ node_modules/better-sqlite3/build/deps \ && node -e "const D=require('better-sqlite3');new D(':memory:').prepare('select 1').get();import('@node-rs/argon2').then(m=>m.hash('x')).then(()=>console.log('native modules ok'))" FROM ${BASE_NODE} AS runtime WORKDIR /app # Не-root runtime; /data — bind mount с хоста, на сервере нужен # `chown -R 1000:1000 ./data` (см. deploy/docker-compose.traefik.yml). RUN mkdir -p /data && chown node:node /data ARG VERSION=dev ARG GIT_SHA=unknown ARG BUILD_TIME= LABEL org.opencontainers.image.title="auth-portal" \ org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.revision="${GIT_SHA}" \ org.opencontainers.image.created="${BUILD_TIME}" # Куча V8 ограничена под контейнер 256 МиБ (RSS = куча + нативная память + argon2); # при другом mem_limit переопределите NODE_OPTIONS. ENV NODE_ENV=production \ NODE_OPTIONS=--max-old-space-size=160 \ UV_THREADPOOL_SIZE=4 \ STATIC_DIR=/app/static \ DATABASE_URL=sqlite:/data/app.db \ SERVER_PORT=8080 \ APP_VERSION=${VERSION} \ GIT_SHA=${GIT_SHA} \ BUILD_TIME=${BUILD_TIME} COPY --from=build --chown=node:node /out ./ USER node EXPOSE 8080 STOPSIGNAL SIGTERM VOLUME ["/data"] # busybox wget вместо запуска второго процесса node каждые 30 с. HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/ready"] CMD ["node", "dist/server.js"]