- Multi-stage Dockerfile (Node 22 alpine, BuildKit cache, non-root user). HEALTHCHECK and metrics port (9090) deferred until task 1.10 ships; comments document the resume. - .gitea/workflows/build.yml — single build job following the pattern of other TRM repos (no services/container, ubuntu-latest direct). Tests + typecheck + lint inline; image tagged :main. - compose.dev.yaml — local-build variant for verifying Dockerfile changes pre-push. Production deploy lives in the sibling deploy/ repo. - .env.example documenting all runtime env vars. - README updated to point at deploy/ for production and explain CI. - Task 1.11 marked done (slim variant) in ROADMAP and task file.
8.5 KiB
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 forpnpm docker:dev. Useful for local testing of the full pipeline.- Documentation updates in
README.mdcovering: 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-alpineis small (~100MB final image). If musl-related issues arise (rare with pure JS), fall back tonode: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 --prodstrips dev dependencies before the runtime copy.- Healthcheck hits
/readyzso 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 upstarts both Redis and the ingestion service; the service's/healthzand/readyzreturn 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@v5doesn't work as expected, fall back to a hand-rolled tag generator usinggit rev-parse --short HEAD. GITEA_TOKENpermissions: confirm the default token can push to the registry. If not, switch to a dedicatedsecrets.REGISTRY_TOKEN.- Architecture: build only
linux/amd64for now. Multi-arch (linux/arm64) is a follow-up if anyone needs it for Apple Silicon dev.
Done
Landed in commit <SHA> (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):
- No
HEALTHCHECK— removed because no HTTP server runs yet. A comment in the Dockerfile marks where to re-addwget -qO- http://localhost:${METRICS_PORT}/readyzwhen task 1.10 lands. EXPOSE 5027only —EXPOSE 9090omitted becauseMETRICS_PORTis in the config schema but nothing listens on it. Adding it would mislead operators.compose.yamlmaps only5027:5027—9090:9090port mapping removed for the same reason.
Additional deviations (none beyond the pre-approved slim changes).
Still owed when task 1.10 ships
- Restore
EXPOSE 9090in 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:9090port mapping incompose.yaml. - Verify the compose
/healthzand/readyzacceptance 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 |