From 4ecafcfe2344b4c8b0f11c0e3a38a555d601a27f Mon Sep 17 00:00:00 2001 From: Stas <39103584+whekin@users.noreply.github.com> Date: Thu, 5 Mar 2026 04:01:08 +0300 Subject: [PATCH] feat(infra): add docker image build and deploy pipeline (#13) --- .dockerignore | 13 ++++ .github/workflows/cd.yml | 44 +++++++++++- .github/workflows/ci.yml | 33 +++++++++ README.md | 7 ++ apps/bot/Dockerfile | 40 +++++++++++ apps/miniapp/Dockerfile | 34 +++++++++ apps/miniapp/nginx.conf | 16 +++++ docker-compose.yml | 19 +++++ docs/runbooks/dev-setup.md | 13 ++++ .../HOUSEBOT-060-docker-image-pipeline.md | 72 +++++++++++++++++++ package.json | 6 +- 11 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 apps/bot/Dockerfile create mode 100644 apps/miniapp/Dockerfile create mode 100644 apps/miniapp/nginx.conf create mode 100644 docker-compose.yml create mode 100644 docs/specs/HOUSEBOT-060-docker-image-pipeline.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..27265df --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.github +.linear +.codex +node_modules +**/node_modules +**/dist +infra/terraform/.terraform +infra/terraform/.terraform.lock.hcl +.env +.env.* +!.env.example +.DS_Store diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 1484dd9..d7ae0a1 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -63,6 +63,11 @@ jobs: needs: check-secrets timeout-minutes: 30 if: ${{ needs.check-secrets.outputs.eligible_event == 'true' && needs.check-secrets.outputs.secrets_ok == 'true' }} + env: + GCP_REGION: ${{ vars.GCP_REGION || 'europe-west1' }} + ARTIFACT_REPOSITORY: ${{ vars.ARTIFACT_REPOSITORY || 'household-bot' }} + CLOUD_RUN_SERVICE_BOT: ${{ vars.CLOUD_RUN_SERVICE_BOT || 'household-dev-bot-api' }} + CLOUD_RUN_SERVICE_MINI: ${{ vars.CLOUD_RUN_SERVICE_MINI || 'household-dev-mini-app' }} steps: - name: Checkout deployment ref @@ -95,11 +100,43 @@ jobs: - name: Setup gcloud uses: google-github-actions/setup-gcloud@v2 + - name: Configure Artifact Registry auth + run: | + gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet + + - name: Resolve image tags + id: images + run: | + bot_image="${GCP_REGION}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${ARTIFACT_REPOSITORY}/bot:${GITHUB_SHA}" + mini_image="${GCP_REGION}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${ARTIFACT_REPOSITORY}/miniapp:${GITHUB_SHA}" + + echo "bot_image=$bot_image" >> "$GITHUB_OUTPUT" + echo "mini_image=$mini_image" >> "$GITHUB_OUTPUT" + + - name: Build and push bot image + run: | + docker build -f apps/bot/Dockerfile -t "${{ steps.images.outputs.bot_image }}" . + docker push "${{ steps.images.outputs.bot_image }}" + + - name: Build and push mini app image + run: | + docker build -f apps/miniapp/Dockerfile -t "${{ steps.images.outputs.mini_image }}" . + docker push "${{ steps.images.outputs.mini_image }}" + - name: Deploy bot service run: | - gcloud run deploy "${{ vars.CLOUD_RUN_SERVICE_BOT || 'household-bot' }}" \ - --source . \ - --region "${{ vars.GCP_REGION || 'europe-west1' }}" \ + gcloud run deploy "${CLOUD_RUN_SERVICE_BOT}" \ + --image "${{ steps.images.outputs.bot_image }}" \ + --region "${GCP_REGION}" \ + --project "${{ secrets.GCP_PROJECT_ID }}" \ + --allow-unauthenticated \ + --quiet + + - name: Deploy mini app service + run: | + gcloud run deploy "${CLOUD_RUN_SERVICE_MINI}" \ + --image "${{ steps.images.outputs.mini_image }}" \ + --region "${GCP_REGION}" \ --project "${{ secrets.GCP_PROJECT_ID }}" \ --allow-unauthenticated \ --quiet @@ -116,3 +153,4 @@ jobs: echo "CD skipped: configure required GitHub secrets." echo "Required: GCP_PROJECT_ID, GCP_WORKLOAD_IDENTITY_PROVIDER, GCP_SERVICE_ACCOUNT" echo "Optional for auto-migrations: DATABASE_URL" + echo "Optional repo/service vars: GCP_REGION, ARTIFACT_REPOSITORY, CLOUD_RUN_SERVICE_BOT, CLOUD_RUN_SERVICE_MINI" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eaa2dc6..c32c78d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,3 +101,36 @@ jobs: run: | terraform -chdir=infra/terraform init -backend=false terraform -chdir=infra/terraform validate + + images: + name: Docker / build ${{ matrix.service }} + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + service: + - bot + - miniapp + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build container image + run: | + case "${{ matrix.service }}" in + bot) + docker build -f apps/bot/Dockerfile -t household-bot:ci . + ;; + miniapp) + docker build -f apps/miniapp/Dockerfile -t household-miniapp:ci . + ;; + *) + echo "Unknown service: ${{ matrix.service }}" + exit 1 + ;; + esac diff --git a/README.md b/README.md index fa9ac08..ef001b2 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,10 @@ Telegram household finance bot and mini app built with Bun workspaces. See the [development setup runbook](docs/runbooks/dev-setup.md) for full setup, quality-check, and local development commands. + +For container smoke testing, run: + +```bash +bun run docker:build +bun run docker:smoke +``` diff --git a/apps/bot/Dockerfile b/apps/bot/Dockerfile new file mode 100644 index 0000000..9b6bcfb --- /dev/null +++ b/apps/bot/Dockerfile @@ -0,0 +1,40 @@ +# syntax=docker/dockerfile:1.7 + +FROM oven/bun:1.3.10 AS deps +WORKDIR /app + +COPY bun.lock package.json tsconfig.base.json ./ +COPY apps/bot/package.json apps/bot/package.json +COPY apps/miniapp/package.json apps/miniapp/package.json +COPY packages/application/package.json packages/application/package.json +COPY packages/config/package.json packages/config/package.json +COPY packages/contracts/package.json packages/contracts/package.json +COPY packages/db/package.json packages/db/package.json +COPY packages/domain/package.json packages/domain/package.json +COPY packages/observability/package.json packages/observability/package.json +COPY packages/ports/package.json packages/ports/package.json + +RUN bun install --frozen-lockfile + +FROM deps AS build +WORKDIR /app + +COPY apps ./apps +COPY packages ./packages + +RUN bun run --filter @household/bot build + +FROM oven/bun:1.3.10 AS runtime +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=8080 + +COPY --from=build /app/apps/bot/dist ./apps/bot/dist + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD bun -e "fetch('http://127.0.0.1:' + (process.env.PORT ?? '8080') + '/health').then((res) => process.exit(res.ok ? 0 : 1)).catch(() => process.exit(1))" + +CMD ["bun", "apps/bot/dist/index.js"] diff --git a/apps/miniapp/Dockerfile b/apps/miniapp/Dockerfile new file mode 100644 index 0000000..7ce621c --- /dev/null +++ b/apps/miniapp/Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1.7 + +FROM oven/bun:1.3.10 AS deps +WORKDIR /app + +COPY bun.lock package.json tsconfig.base.json ./ +COPY apps/bot/package.json apps/bot/package.json +COPY apps/miniapp/package.json apps/miniapp/package.json +COPY packages/application/package.json packages/application/package.json +COPY packages/config/package.json packages/config/package.json +COPY packages/contracts/package.json packages/contracts/package.json +COPY packages/db/package.json packages/db/package.json +COPY packages/domain/package.json packages/domain/package.json +COPY packages/observability/package.json packages/observability/package.json +COPY packages/ports/package.json packages/ports/package.json + +RUN bun install --frozen-lockfile + +FROM deps AS build +WORKDIR /app + +COPY apps/miniapp ./apps/miniapp + +RUN bun run --filter @household/miniapp build + +FROM nginx:1.27-alpine AS runtime + +COPY apps/miniapp/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/apps/miniapp/dist /usr/share/nginx/html + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1:8080/health >/dev/null || exit 1 diff --git a/apps/miniapp/nginx.conf b/apps/miniapp/nginx.conf new file mode 100644 index 0000000..2dd7a67 --- /dev/null +++ b/apps/miniapp/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 8080; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location = /health { + default_type application/json; + return 200 '{"ok":true}'; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..31ab956 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + bot: + build: + context: . + dockerfile: apps/bot/Dockerfile + environment: + PORT: '8080' + TELEGRAM_BOT_TOKEN: '${TELEGRAM_BOT_TOKEN:-000000:dev-token}' + TELEGRAM_WEBHOOK_SECRET: '${TELEGRAM_WEBHOOK_SECRET:-dev-secret}' + TELEGRAM_WEBHOOK_PATH: '${TELEGRAM_WEBHOOK_PATH:-/webhook/telegram}' + ports: + - '8080:8080' + + miniapp: + build: + context: . + dockerfile: apps/miniapp/Dockerfile + ports: + - '8081:8080' diff --git a/docs/runbooks/dev-setup.md b/docs/runbooks/dev-setup.md index 8858161..5cb78f5 100644 --- a/docs/runbooks/dev-setup.md +++ b/docs/runbooks/dev-setup.md @@ -37,6 +37,13 @@ bun run dev:bot bun run dev:miniapp ``` +## Docker smoke commands + +```bash +bun run docker:build +bun run docker:smoke +``` + ## Review commands ```bash @@ -59,12 +66,18 @@ bun run review:coderabbit - CI runs in parallel matrix jobs on push/PR to `main`: - `format:check`, `lint`, `typecheck`, `test`, `build` - `terraform fmt -check`, `terraform validate` + - docker image builds for `apps/bot` and `apps/miniapp` - CD deploys on successful `main` CI completion (or manual dispatch). - CD is enabled when GitHub secrets are configured: - `GCP_PROJECT_ID` - `GCP_WORKLOAD_IDENTITY_PROVIDER` - `GCP_SERVICE_ACCOUNT` - optional for automated migrations: `DATABASE_URL` +- Optional GitHub variables for deploy: + - `GCP_REGION` (default `europe-west1`) + - `ARTIFACT_REPOSITORY` (default `household-bot`) + - `CLOUD_RUN_SERVICE_BOT` (default `household-dev-bot-api`) + - `CLOUD_RUN_SERVICE_MINI` (default `household-dev-mini-app`) ## IaC Runbook diff --git a/docs/specs/HOUSEBOT-060-docker-image-pipeline.md b/docs/specs/HOUSEBOT-060-docker-image-pipeline.md new file mode 100644 index 0000000..9548c29 --- /dev/null +++ b/docs/specs/HOUSEBOT-060-docker-image-pipeline.md @@ -0,0 +1,72 @@ +# HOUSEBOT-060: Docker Images for Bot and Mini App + +## Summary + +Add production Docker images and CI/CD image flow so both services are deployable to Cloud Run from Artifact Registry. + +## Goals + +- Add reproducible Dockerfiles for `apps/bot` and `apps/miniapp`. +- Provide local Docker smoke execution for both services. +- Build images in CI and deploy Cloud Run from pushed images in CD. + +## Non-goals + +- Kubernetes manifests. +- Full production runbook and cutover checklist. +- Runtime feature changes in bot or mini app business logic. + +## Scope + +- In: Dockerfiles, nginx config for SPA serving, compose smoke setup, CI/CD workflow updates, developer scripts/docs. +- Out: Advanced image signing/SBOM/scanning. + +## Interfaces and Contracts + +- Bot container exposes `PORT` (default `8080`) and `/health`. +- Mini app container serves SPA on `8080` and provides `/health`. +- CD builds and pushes: + - `-docker.pkg.dev///bot:` + - `-docker.pkg.dev///miniapp:` + +## Domain Rules + +- None (infrastructure change). + +## Data Model Changes + +- None. + +## Security and Privacy + +- No secrets embedded in images. +- Runtime secrets remain injected via Cloud Run/Secret Manager. + +## Observability + +- Container health checks for bot and mini app. +- CD logs include image refs and deploy steps. + +## Edge Cases and Failure Modes + +- Missing Artifact Registry repository: image push fails. +- Missing Cloud Run service vars: deploy falls back to documented defaults. +- Missing DB secret: migrations are skipped but deploy continues. + +## Test Plan + +- Unit: N/A. +- Integration: CI docker build jobs for both images. +- E2E: local `docker compose up --build` smoke run with health endpoint checks. + +## Acceptance Criteria + +- [ ] Both services run locally via Docker. +- [ ] CI builds both images without manual patching. +- [ ] CD deploys Cloud Run from built Artifact Registry images. + +## Rollout Plan + +- Merge Docker + workflow changes. +- Configure optional GitHub vars (`GCP_REGION`, `ARTIFACT_REPOSITORY`, service names). +- Trigger `workflow_dispatch` CD once to validate image deploy path. diff --git a/package.json b/package.json index 9b60553..5e5c898 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,11 @@ "infra:fmt:check": "terraform -chdir=infra/terraform fmt -check -recursive", "infra:validate": "terraform -chdir=infra/terraform init -backend=false && terraform -chdir=infra/terraform validate", "dev:bot": "bun run --filter @household/bot dev", - "dev:miniapp": "bun run --filter @household/miniapp dev" + "dev:miniapp": "bun run --filter @household/miniapp dev", + "docker:build:bot": "docker build -f apps/bot/Dockerfile -t household-bot:local .", + "docker:build:miniapp": "docker build -f apps/miniapp/Dockerfile -t household-miniapp:local .", + "docker:build": "bun run docker:build:bot && bun run docker:build:miniapp", + "docker:smoke": "docker compose up --build" }, "devDependencies": { "@types/bun": "1.3.10",