From 52d8e0f7e0c66ca831013ceaa2112d9e551b2cd9 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Thu, 8 Oct 2026 11:03:52 +0300 Subject: [PATCH] Container images and Woodpecker CI pipeline Co-Authored-By: Claude Sonnet 5.5 --- .dockerignore | 12 ++++++ .woodpecker/ci.yaml | 70 +++++++++++++++++++++++++++++++++ README.md | 15 +++++++ docker/compose.example.yml | 31 +++++++++++++++ docker/mobile-client.Dockerfile | 19 +++++++++ docker/nginx-mobile.conf | 31 +++++++++++++++ docker/nginx-spa.conf | 14 +++++++ docker/server-be.Dockerfile | 26 ++++++++++++ docker/server-fe.Dockerfile | 14 +++++++ package.json | 1 + 10 files changed, 233 insertions(+) create mode 100644 .dockerignore create mode 100644 .woodpecker/ci.yaml create mode 100644 docker/compose.example.yml create mode 100644 docker/mobile-client.Dockerfile create mode 100644 docker/nginx-mobile.conf create mode 100644 docker/nginx-spa.conf create mode 100644 docker/server-be.Dockerfile create mode 100644 docker/server-fe.Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3f16e74 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +**/node_modules +**/dist +**/dist-capacitor +mobile-client/android +mobile-client/updates +data +.git +.woodpecker +.idea +.claude +**/.env +*.log diff --git a/.woodpecker/ci.yaml b/.woodpecker/ci.yaml new file mode 100644 index 0000000..735ac1e --- /dev/null +++ b/.woodpecker/ci.yaml @@ -0,0 +1,70 @@ +# Woodpecker CI: typecheck + tests on every push/PR, container images on main and ci (tags: short SHA and branch name). +# +# Required secrets (Repository settings -> Secrets): +# registry_username, registry_password +# The repo must be marked "Trusted" (docker-buildx plugin runs privileged), +# or switch the publish steps to woodpeckerci/plugin-kaniko. +# +variables: + - &node_image 'node:22-alpine' + - &buildx 'woodpeckerci/plugin-docker-buildx:latest' + - ®istry 'git.aggtaa.com' + - &publish_when + - event: push + branch: [main, ci] + +steps: + - name: check + image: *node_image + commands: + - corepack enable + - pnpm install --frozen-lockfile + - pnpm typecheck + - pnpm test + when: + - event: [push, pull_request] + + - name: image-server-be + image: *buildx + depends_on: [check] + settings: + registry: *registry + repo: git.aggtaa.com/${CI_REPO_OWNER}/cardy-server-be + dockerfile: docker/server-be.Dockerfile + context: . + tags: ['${CI_COMMIT_SHA:0:8}', '${CI_COMMIT_BRANCH}'] + username: + from_secret: registry_username + password: + from_secret: registry_password + when: *publish_when + + - name: image-server-fe + image: *buildx + depends_on: [check] + settings: + registry: *registry + repo: git.aggtaa.com/${CI_REPO_OWNER}/cardy-server-fe + dockerfile: docker/server-fe.Dockerfile + context: . + tags: ['${CI_COMMIT_SHA:0:8}', '${CI_COMMIT_BRANCH}'] + username: + from_secret: registry_username + password: + from_secret: registry_password + when: *publish_when + + - name: image-mobile-client + image: *buildx + depends_on: [check] + settings: + registry: *registry + repo: git.aggtaa.com/${CI_REPO_OWNER}/cardy-mobile-client + dockerfile: docker/mobile-client.Dockerfile + context: . + tags: ['${CI_COMMIT_SHA:0:8}', '${CI_COMMIT_BRANCH}'] + username: + from_secret: registry_username + password: + from_secret: registry_password + when: *publish_when diff --git a/README.md b/README.md index 3011b5c..9026448 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,18 @@ with `/api` stripped; `DEV_REMOTE_EMAIL` in `server-fe/.env` fakes the Authelia pnpm test pnpm typecheck ``` + +## Containers and CI (Woodpecker) + +`docker/*.Dockerfile` build the three images from the repository root (`docker build -f docker/server-be.Dockerfile .`): + +| Image | Port | Notes | +|---|---|---| +| `server-be` | 10002 | Node, data in volume `/data`. Listens on `0.0.0.0`: set `ADMIN_PROXY_SECRET` and don't publish the port publicly | +| `server-fe` | 10001 | nginx with the admin UI build | +| `mobile-client` | 10003 | nginx with the PWA; Android updates are mounted at `/updates` | + +`.woodpecker/ci.yaml`: typecheck + tests on every push/PR, then images are pushed on `main` and `ci` +(tags: short commit SHA and branch name, e.g. `:main`). Images go to the Gitea registry (`git.aggtaa.com/anton/cardy-*`); add secrets `registry_username` +(Gitea user + access token with `write:package`) and `registry_password`, and mark the repository as Trusted (buildx plugin is privileged). +`docker/compose.example.yml` shows how to run the images. APK/OTA publishing stays local (needs the signing key). diff --git a/docker/compose.example.yml b/docker/compose.example.yml new file mode 100644 index 0000000..c9c4fb9 --- /dev/null +++ b/docker/compose.example.yml @@ -0,0 +1,31 @@ +# Example deployment. Put the reverse proxy (Authelia on /api/admin) in front, as described in README. +services: + server-be: + image: git.aggtaa.com/anton/cardy-server-be:main + restart: unless-stopped + environment: + KEY_ENCRYPTION_SECRET: ${KEY_ENCRYPTION_SECRET} + SUPERADMIN_EMAIL: ${SUPERADMIN_EMAIL} + ADMIN_PROXY_SECRET: ${ADMIN_PROXY_SECRET} # the container listens on 0.0.0.0, so set it + PUBLIC_API_URL: https://dev.aggtaa.com/api + volumes: + - cardy-data:/data + ports: + - "127.0.0.1:10002:10002" + + server-fe: + image: git.aggtaa.com/anton/cardy-server-fe:main + restart: unless-stopped + ports: + - "127.0.0.1:10001:10001" + + mobile-client: + image: git.aggtaa.com/anton/cardy-mobile-client:main + restart: unless-stopped + volumes: + - ./updates/android:/updates:ro # output of `publish:android` / `publish:android-apk` + ports: + - "127.0.0.1:10003:10003" + +volumes: + cardy-data: diff --git a/docker/mobile-client.Dockerfile b/docker/mobile-client.Dockerfile new file mode 100644 index 0000000..e573c4e --- /dev/null +++ b/docker/mobile-client.Dockerfile @@ -0,0 +1,19 @@ +# Build context: repository root. +FROM node:22-alpine AS build +RUN corepack enable +WORKDIR /repo +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json tsconfig.base.json ./ +COPY shared ./shared +COPY mobile-client ./mobile-client +RUN pnpm install --frozen-lockfile --filter @cardy/mobile-client... +# CARDY_UPDATE_URL is baked into the web build (where the Android app looks for live updates). +ARG CARDY_UPDATE_URL=https://dev.aggtaa.com/m/android +ENV CARDY_UPDATE_URL=$CARDY_UPDATE_URL +RUN pnpm --filter @cardy/mobile-client exec vite build + +FROM nginxinc/nginx-unprivileged:alpine +COPY docker/nginx-mobile.conf /etc/nginx/conf.d/default.conf +COPY --from=build /repo/mobile-client/dist /usr/share/nginx/html +# Android updates (version.json, bundles, APK) published with `publish:android*` are mounted here. +VOLUME /updates +EXPOSE 10003 diff --git a/docker/nginx-mobile.conf b/docker/nginx-mobile.conf new file mode 100644 index 0000000..2738881 --- /dev/null +++ b/docker/nginx-mobile.conf @@ -0,0 +1,31 @@ +# The reverse proxy strips /m before forwarding, so files are looked up at the root. +# The "/m/" form is also accepted for direct access. +server { + listen 10003; + root /usr/share/nginx/html; + index index.html; + + rewrite ^/m/(.*)$ /$1 last; + + # Published Android updates; open CORS because the app runs on https://localhost. + location /android/ { + alias /updates/; + add_header Access-Control-Allow-Origin "*"; + add_header Cache-Control "no-cache"; + try_files $uri =404; + } + + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # Service worker and manifest must always be revalidated. + location ~ ^/(sw\.js|workbox-.*\.js|registerSW\.js|manifest\.webmanifest|index\.html)$ { + add_header Cache-Control "no-cache"; + } + + location / { + add_header Cache-Control "no-cache"; + try_files $uri /index.html; + } +} diff --git a/docker/nginx-spa.conf b/docker/nginx-spa.conf new file mode 100644 index 0000000..b6c0ec8 --- /dev/null +++ b/docker/nginx-spa.conf @@ -0,0 +1,14 @@ +server { + listen 10001; + root /usr/share/nginx/html; + index index.html; + + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + location / { + add_header Cache-Control "no-cache"; + try_files $uri /index.html; + } +} diff --git a/docker/server-be.Dockerfile b/docker/server-be.Dockerfile new file mode 100644 index 0000000..56f2022 --- /dev/null +++ b/docker/server-be.Dockerfile @@ -0,0 +1,26 @@ +# Build context: repository root. +FROM node:22-alpine AS build +RUN corepack enable +WORKDIR /repo +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json tsconfig.base.json ./ +COPY shared ./shared +COPY server-be ./server-be +# Only the workspace packages needed by server-be (the other ones are not copied). +RUN pnpm install --frozen-lockfile --filter @cardy/server-be... +RUN pnpm --filter @cardy/server-be build \ + && pnpm --filter @cardy/server-be --prod deploy --legacy /out + +FROM node:22-alpine +ENV NODE_ENV=production \ + DATA_DIR=/data \ + HOST=0.0.0.0 \ + PORT=10002 +WORKDIR /app +COPY --from=build /out/package.json ./package.json +COPY --from=build /out/node_modules ./node_modules +COPY --from=build /out/dist ./dist +RUN mkdir -p /data && chown node:node /data +USER node +VOLUME /data +EXPOSE 10002 +CMD ["node", "dist/index.js"] diff --git a/docker/server-fe.Dockerfile b/docker/server-fe.Dockerfile new file mode 100644 index 0000000..ea035e1 --- /dev/null +++ b/docker/server-fe.Dockerfile @@ -0,0 +1,14 @@ +# Build context: repository root. +FROM node:22-alpine AS build +RUN corepack enable +WORKDIR /repo +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json tsconfig.base.json ./ +COPY shared ./shared +COPY server-fe ./server-fe +RUN pnpm install --frozen-lockfile --filter @cardy/server-fe... +RUN pnpm --filter @cardy/server-fe build + +FROM nginxinc/nginx-unprivileged:alpine +COPY docker/nginx-spa.conf /etc/nginx/conf.d/default.conf +COPY --from=build /repo/server-fe/dist /usr/share/nginx/html +EXPOSE 10001 diff --git a/package.json b/package.json index e186705..78e3c4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "cardy", "private": true, + "packageManager": "pnpm@11.4.0", "type": "module", "scripts": { "build": "pnpm -r build",