From e31ab35a5ac51b7dbd1509e35c4ce8e9f8fd4e92 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 16 Sep 2026 09:41:38 +0300 Subject: [PATCH] feat: add frontend container delivery --- .dockerignore | 15 ++++++++++ .gitea/workflows/image.yml | 61 ++++++++++++++++++++++++++++++++++++++ Dockerfile | 30 +++++++++++++++++++ README.md | 25 ++++++++++++++++ docker/nginx.conf | 43 +++++++++++++++++++++++++++ vite.config.ts | 7 +++++ 6 files changed, 181 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/image.yml create mode 100644 Dockerfile create mode 100644 docker/nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ec510c1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +** +!package.json +!package-lock.json +!.nvmrc +!index.html +!tsconfig*.json +!vite.config.ts +!.storybook/ +!.storybook/** +!src/ +!src/** +!public/ +!public/** +!docker/ +!docker/** diff --git a/.gitea/workflows/image.yml b/.gitea/workflows/image.yml new file mode 100644 index 0000000..4e48340 --- /dev/null +++ b/.gitea/workflows/image.yml @@ -0,0 +1,61 @@ +name: Frontend image + +on: + push: + tags: ['v*'] + +concurrency: + group: frontend-image-publication + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + env: + IMAGE: git.personal.ksv741.ru/wealthledger/frontend + RELEASE_TAG: ${{ gitea.ref_name }} + REVISION: ${{ gitea.sha }} + DOCKER_CONFIG: ${{ runner.temp }}/wealthledger-frontend-docker + steps: + - uses: https://github.com/actions/checkout@v4 + - uses: https://github.com/actions/setup-node@v4 + with: + node-version-file: .nvmrc + - name: Validate release version + run: | + node --input-type=commonjs -e 'const p = require("./package.json"); const l = require("./package-lock.json"); if (!/^v\d+\.\d+\.\d+$/.test(process.env.RELEASE_TAG) || process.env.RELEASE_TAG !== `v${p.version}` || l.version !== p.version || l.packages[""].version !== p.version) process.exit(1)' + test "$(git rev-parse HEAD)" = "$REVISION" + docker version + docker buildx version + - run: npm ci + env: + HUSKY: '0' + - run: npm run verify + env: + KNIP_DISABLE_RAW_TRANSFER: '1' + - name: Login with dedicated package writer + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + test -n "$REGISTRY_USERNAME" && test -n "$REGISTRY_TOKEN" + install -d -m 700 "$DOCKER_CONFIG" + printf '%s' "$REGISTRY_TOKEN" | docker login git.personal.ksv741.ru --username "$REGISTRY_USERNAME" --password-stdin + - name: Publish immutable release tag + run: | + VERSION="${RELEASE_TAG#v}" + if docker manifest inspect "$IMAGE:$VERSION" >/dev/null 2>manifest-error.log; then + echo 'Refusing to overwrite an existing release tag'; exit 1 + fi + if ! grep -Eq 'manifest unknown|no such manifest' manifest-error.log; then + echo 'Cannot prove tag absence; refusing publication'; exit 1 + fi + docker buildx build --target production --build-arg VERSION="$VERSION" --build-arg REVISION="$REVISION" --tag "$IMAGE:$VERSION" --metadata-file image-metadata.json --push . + DIGEST="$(node --input-type=commonjs -p 'require("./image-metadata.json")["containerimage.digest"]')" + docker pull "$IMAGE@$DIGEST" + test "$(docker image inspect "$IMAGE@$DIGEST" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$REVISION" + docker run --rm --entrypoint sh "$IMAGE@$DIGEST" -c 'test -s /usr/share/nginx/html/index.html && nginx -t && nginx && wget -q -O /dev/null http://127.0.0.1:8080/' + printf 'Frontend image: %s@%s\n' "$IMAGE" "$DIGEST" + - name: Remove registry session + if: always() + run: rm -rf "$DOCKER_CONFIG" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..88feb2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:24.21.0-bookworm-slim AS dependencies +WORKDIR /app +ENV HUSKY=0 +COPY package.json package-lock.json ./ +RUN npm ci + +FROM dependencies AS development +COPY . . +ENV VITE_API_BASE_URL="" API_PROXY_TARGET=http://backend:3000 +EXPOSE 8080 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "8080", "--strictPort"] + +FROM dependencies AS build +COPY . . +ENV VITE_API_BASE_URL="" +RUN npm run build + +FROM nginx:1.28.0-alpine AS production +ARG VERSION +ARG REVISION +LABEL org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.revision=$REVISION +COPY docker/nginx.conf /etc/nginx/nginx.conf +COPY --from=build /app/dist /usr/share/nginx/html +USER nginx +EXPOSE 8080 +ENTRYPOINT ["nginx"] +CMD ["-g", "daemon off;"] +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=6 \ + CMD wget -q -O /dev/null http://127.0.0.1:8080/ || exit 1 diff --git a/README.md b/README.md index 805f09d..d5f4d81 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,31 @@ staged diff с полной редакцией секретов, затем за Находка или отсутствие Gitleaks блокирует commit. Проверка этого поведения: `node --test scripts/test-pre-commit.mjs`. +## Контейнерный image + +`Dockerfile` имеет targets `development` (Vite HMR и proxy `/api/` во внутренний backend) +и `production` (nginx на 8080, SPA fallback и same-origin API proxy). Node соответствует `.nvmrc`. +Build context ограничен `.dockerignore`; `.env`, local DB и host node_modules +не попадают в image. + +```bash +docker build --target development -t wealthledger-frontend:development . +docker build --target production -t wealthledger-frontend:production . +``` + +Gitea `image.yml` запускается на защищённом release tag `vX.Y.Z`, проверяет +package/lock version и canonical gates. Runner `ubuntu-latest` должен иметь +Docker/buildx. Dedicated package writer задаётся Actions secrets +`REGISTRY_USERNAME`/`REGISTRY_TOKEN`; server использует отдельную read-only +identity. Публикуются semver tags, без `latest`; существующие +release tags не перезаписываются. Production выбирает опубликованный version tag. + +Compose topology принадлежит workspace, эксплуатационная последовательность — +[документация](https://git.personal.ksv741.ru/WealthLedger/docs), +страница `architecture/deployment.md`. +Проверка публикации требует настроенного registry/runner; локальный build +не доказывает package permissions. + ## Проверка ```bash diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..ef37b03 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,43 @@ +worker_processes auto; +pid /tmp/nginx.pid; +error_log /dev/stderr warn; +events { worker_connections 1024; } +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + # Do not log query strings, cookies, authorization or bodies. + log_format safe '$request_method $uri $status'; + access_log /dev/stdout safe; + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + map $http_x_forwarded_proto $forwarded_proto { + default $http_x_forwarded_proto; + '' $scheme; + } + server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + resolver 127.0.0.11 valid=5s ipv6=off; + location /api/ { + set $backend http://backend:3000; + proxy_pass $backend$request_uri; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Proto $forwarded_proto; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Request-ID $http_x_request_id; + } + location /assets/ { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache" always; + } + } +} diff --git a/vite.config.ts b/vite.config.ts index 4c8a6bc..a994fd2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,4 +7,11 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [react(), babel({ presets: [reactCompilerPreset()] })], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }, + server: { + proxy: { + "/api/": { + target: process.env.API_PROXY_TARGET ?? "http://localhost:3000", + }, + }, + }, });