Files
tcp-ingestion/.planning/phase-1-telemetry/11-dockerfile-and-ci.md
T
julian c8a5f4cd68 Add Phase 1 and Phase 2 planning documents
ROADMAP plus granular task files per phase. Phase 1 (12 tasks + 1.13
device authority) covers Codec 8/8E/16 telemetry ingestion; Phase 2
(6 tasks) covers Codec 12/14 outbound commands; Phase 3 enumerates
deferred items.
2026-04-30 15:50:49 +02:00

6.5 KiB

Task 1.11 — Dockerfile & Gitea workflow

Phase: 1 — Inbound telemetry Status: Not started 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

# 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:

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, <short-sha>, 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)

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

(Fill in once complete.)