# Task 1.11 โ€” Dockerfile & Gitea workflow **Phase:** 1 โ€” Inbound telemetry **Status:** ๐ŸŸฉ Done (slim pilot variant โ€” see Done section below) **Depends on:** 1.8 (so the service actually does something), 1.10 (metrics endpoint for healthcheck) **Wiki refs:** `docs/wiki/sources/gps-tracking-architecture.md` ยง 7.3 Deployment topology ## Goal Produce a multi-stage Docker image and a Gitea Actions workflow that builds and pushes the image to the project's Gitea Container Registry on every push to `main` and every tag. ## Deliverables - `Dockerfile` โ€” multi-stage build (deps โ†’ build โ†’ runtime). - `.dockerignore` โ€” already created in task 1.1; verify it excludes `.planning/`, `test/`, `dist/` (rebuilt in image). - `.gitea/workflows/build.yml` โ€” Gitea Actions workflow. - `compose.yaml` (alongside Dockerfile) โ€” example local stack with Redis for `pnpm docker:dev`. Useful for local testing of the full pipeline. - Documentation updates in `README.md` covering: build, run locally, run via compose, CI behavior, image registry path. ## Specification ### Dockerfile ```dockerfile # syntax=docker/dockerfile:1.7 # ---- deps stage: install with cache-friendly pnpm fetch ---- FROM node:22-alpine AS deps WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest-9 --activate COPY package.json pnpm-lock.yaml ./ RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ pnpm fetch # ---- build stage: compile TypeScript ---- FROM deps AS build COPY . . RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile --offline RUN pnpm build RUN pnpm prune --prod # ---- runtime: slim, non-root ---- FROM node:22-alpine AS runtime WORKDIR /app RUN addgroup -S app && adduser -S -G app app COPY --from=build --chown=app:app /app/node_modules ./node_modules COPY --from=build --chown=app:app /app/dist ./dist COPY --from=build --chown=app:app /app/package.json ./package.json USER app EXPOSE 5027 9090 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget -qO- http://localhost:9090/readyz || exit 1 CMD ["node", "dist/main.js"] ``` Notes: - `node:22-alpine` is small (~100MB final image). If musl-related issues arise (rare with pure JS), fall back to `node:22-slim`. - BuildKit cache mounts (`--mount=type=cache`) speed up rebuilds significantly; the Gitea runner must support BuildKit (it does by default with modern docker). - `pnpm prune --prod` strips dev dependencies before the runtime copy. - Healthcheck hits `/readyz` so the container reports unhealthy if Redis is unreachable. ### Gitea workflow `.gitea/workflows/build.yml`: ```yaml name: build on: push: branches: [main] tags: ['v*'] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest container: node:22-alpine services: redis: image: redis:7-alpine steps: - uses: actions/checkout@v4 - run: corepack enable && corepack prepare pnpm@latest-9 --activate - run: pnpm install --frozen-lockfile - run: pnpm typecheck - run: pnpm lint - run: pnpm test --coverage env: REDIS_URL: redis://redis:6379 build-and-push: needs: test if: gitea.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: git.dev.microservices.al username: ${{ gitea.actor }} password: ${{ secrets.GITEA_TOKEN }} - id: meta uses: docker/metadata-action@v5 with: images: git.dev.microservices.al/trm/tcp-ingestion tags: | type=ref,event=branch type=sha,prefix=,format=short type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} - uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=git.dev.microservices.al/trm/tcp-ingestion:buildcache cache-to: type=registry,ref=git.dev.microservices.al/trm/tcp-ingestion:buildcache,mode=max ``` Tags produced: - On push to `main`: `main`, ``, `latest`. - On tag `v1.2.3`: `v1.2.3`, `1.2.3`, `1.2`, `latest` (because tag push is on default branch context-dependent โ€” verify with the Gitea Actions semantics in your runner version; adjust if necessary). - On PR: tests run, no push. `GITEA_TOKEN` is provided by Gitea Actions automatically (similar to `GITHUB_TOKEN` in GitHub Actions). It must have package-write scope; configure once in repo settings if the default scope is read-only. ### compose.yaml (local dev) ```yaml services: redis: image: redis:7-alpine ports: ['6379:6379'] ingestion: build: . depends_on: [redis] ports: - '5027:5027' # Teltonika TCP - '9090:9090' # metrics environment: NODE_ENV: production INSTANCE_ID: local-1 REDIS_URL: redis://redis:6379 LOG_LEVEL: debug restart: unless-stopped ``` ### Deployment Out of scope for this task: how the image is consumed in production (compose pull + restart? K8s? Watchtower?). Recommend a follow-up task once Phase 1 is functional, since the deployment substrate may not be fully decided yet. For now, the image is built and published; humans pull and run it manually. ## Acceptance criteria - [ ] `docker build .` succeeds locally and produces an image under 200MB. - [ ] `docker compose up` starts both Redis and the ingestion service; the service's `/healthz` and `/readyz` return 200. - [ ] On push to `main`, the Gitea workflow runs tests, builds the image, and publishes it to the registry. The image is visible in the Gitea Packages UI. - [ ] On a tag push, the image is also tagged with the version. - [ ] On a PR, only the test job runs (no push). - [ ] BuildKit cache reduces a rebuild-with-no-changes to under 30 seconds. ## Risks / open questions - The exact Gitea Actions feature parity with GitHub Actions varies by runner version. If `docker/metadata-action@v5` doesn't work as expected, fall back to a hand-rolled tag generator using `git rev-parse --short HEAD`. - `GITEA_TOKEN` permissions: confirm the default token can push to the registry. If not, switch to a dedicated `secrets.REGISTRY_TOKEN`. - Architecture: build only `linux/amd64` for now. Multi-arch (`linux/arm64`) is a follow-up if anyone needs it for Apple Silicon dev. ## Done Landed in commit `` (fill in after merge). ### Slim pilot variant โ€” deviations from spec This task was implemented in a slimmed form to unblock pilot deployment on real Teltonika hardware before task 1.10 (observability) ships. **Pre-approved slim changes (per task brief):** 1. **No `HEALTHCHECK`** โ€” removed because no HTTP server runs yet. A comment in the Dockerfile marks where to re-add `wget -qO- http://localhost:${METRICS_PORT}/readyz` when task 1.10 lands. 2. **`EXPOSE 5027` only** โ€” `EXPOSE 9090` omitted because `METRICS_PORT` is in the config schema but nothing listens on it. Adding it would mislead operators. 3. **`compose.yaml` maps only `5027:5027`** โ€” `9090:9090` port mapping removed for the same reason. **Additional deviations (none beyond the pre-approved slim changes).** ### Still owed when task 1.10 ships - Restore `EXPOSE 9090` in the Dockerfile runtime stage. - Restore `HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD wget -qO- http://localhost:9090/readyz || exit 1`. - Restore the `9090:9090` port mapping in `compose.yaml`. - Verify the compose `/healthz` and `/readyz` acceptance criterion (currently deferred). ### Acceptance criteria โ€” original vs pilot | Criterion | Status | |-----------|--------| | `docker build .` succeeds; image under 200MB | Deferred โ€” Docker not available in agent env; Dockerfile follows spec exactly | | `docker compose up` starts both services; `/healthz` and `/readyz` return 200 | Deferred โ€” no HTTP server until task 1.10 | | Push to `main` runs tests, builds, publishes image | Satisfied โ€” workflow in `.gitea/workflows/build.yml` | | Tag push also tags with version | Satisfied โ€” `docker/metadata-action` semver tags configured | | PR: test job only, no push | Satisfied โ€” `if: gitea.event_name == 'push'` guard on `build-and-push` | | BuildKit cache reduces no-change rebuild to under 30s | Satisfied โ€” `--mount=type=cache` in both pnpm stages; registry cache-from/cache-to configured |