From eff30fd587dd6b1771b438f2846499b78c4d001d Mon Sep 17 00:00:00 2001 From: Lantum-Brendan Date: Mon, 10 Aug 2026 21:03:43 +0100 Subject: [PATCH] feat: Add docker image build and publish pipeline The webui has no published Docker image today. Every downstream deployment template (compose stack, Helm, cloud providers) needs ghcr.io/trakli/webui to exist. Dockerfile (multi-stage): - Stage 1 (node:lts): npm ci + npm run build with a placeholder API URL baked into the output. Nuxt 3 runtimeConfig.public values are embedded at build time, so we cannot set the real URL here. - Stage 2 (node:lts-alpine): copies only .output/ from the build stage. Alpine-based for a small final image. docker/entrypoint.sh: - At container start, sed-replaces the build-time placeholder (http://__NUXT_PUBLIC_API_BASE_URL__) in all .output/ JS files with the actual NUXT_PUBLIC_API_BASE_URL env var, then starts the Node server. Standard Nuxt 3 pattern for a single image across deployments. .github/workflows/build-and-publish.yml: - Mirrors the webservice workflow: SHA tag on dev push, latest + release semver tag on GitHub Release publish. Pushes to ghcr.io. .dockerignore: - Excludes node_modules, .nuxt, .output, .git, .env*, tests. Issue: trakli/webservice#308 --- .dockerignore | 10 +++++ .github/workflows/build-and-publish.yml | 52 +++++++++++++++++++++++++ Dockerfile | 40 +++++++++++++++++++ docker/entrypoint.sh | 13 +++++++ 4 files changed, 115 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/build-and-publish.yml create mode 100644 Dockerfile create mode 100755 docker/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..476d808 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +.nuxt +.output +.git +.env +.env.* +!.env.example +*.log +.DS_Store +tests diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..5a6b95e --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,52 @@ +name: Build and Publish Docker Image + +on: + push: + branches: + - dev + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve short commit SHA + id: meta + if: ${{ github.event_name == 'push' }} + run: echo "sha=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT" + + - name: Build and push commit image + if: ${{ github.event_name == 'push' }} + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ghcr.io/trakli/webui:${{ steps.meta.outputs.sha }} + + - name: Build and push latest + if: ${{ github.event_name == 'release' }} + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ghcr.io/trakli/webui:latest + ghcr.io/trakli/webui:${{ github.event.release.tag_name }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..59529a5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Trakli web UI — multi-stage build for production +# +# Nuxt 3 bakes runtimeConfig.public values into the .output/ at build time. +# We build with a placeholder and replace it at container start via entrypoint, +# so a single image works across deployments with different API URLs. + +# ── Stage 1: Build ────────────────────────────────────────────────────────── +FROM node:lts AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +# Placeholder — replaced at runtime by docker/entrypoint.sh +ENV NUXT_PUBLIC_API_BASE_URL=http://__NUXT_PUBLIC_API_BASE_URL__ +RUN npm run build + +# ── Stage 2: Runtime ──────────────────────────────────────────────────────── +FROM node:lts-alpine + +LABEL org.opencontainers.image.source=https://github.com/trakli/webui +LABEL org.opencontainers.image.description="Trakli web dashboard" +LABEL org.opencontainers.image.vendor="WhileSmart LLC" +LABEL org.opencontainers.image.licenses=MIT + +WORKDIR /app + +COPY --from=builder /app/.output .output +COPY --from=builder /app/docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD wget -qO- http://localhost:3000/ || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..8d50d57 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +# Replace the build-time placeholder with the actual runtime value. +# This is how a single Docker image serves deployments with different API URLs +# without rebuilding — Nuxt 3 bakes runtimeConfig.public into .output/ at +# build time, so we patch the built files before starting the server. +if [ -n "$NUXT_PUBLIC_API_BASE_URL" ]; then + find /app/.output -type f \( -name '*.mjs' -o -name '*.js' -o -name '*.cjs' \) \ + -exec sed -i "s|http://__NUXT_PUBLIC_API_BASE_URL__|${NUXT_PUBLIC_API_BASE_URL}|g" {} + +fi + +exec node .output/server/index.mjs