Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excluding the Dockerfile and the .github directory from the build context is a best practice. It prevents meta-files from being included in layers and avoids unnecessary cache invalidation if only repository metadata changes.

Suggested change
node_modules
node_modules
Dockerfile
.github

.nuxt
.output
.git
.env
.env.*
!.env.example
*.log
.DS_Store
tests
52 changes: 52 additions & 0 deletions .github/workflows/build-and-publish.yml
Original file line number Diff line number Diff line change
@@ -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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current workflow duplicates build logic and lacks layer caching. Consolidating into a single build step using docker/metadata-action is more maintainable and ensures the exact same image is used for all tags. Adding type=gha cache will significantly speed up subsequent runs by persisting layers between builds.

Suggested change
with:
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/trakli/webui
tags: |
type=sha,prefix=,format=short,enable=${{ github.event_name == 'push' }}
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
type=ref,event=tag,enable=${{ github.event_name == 'release' }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

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 }}
40 changes: 40 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +37 to +38

ENTRYPOINT ["/entrypoint.sh"]
13 changes: 13 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The & character is a special backreference in sed replacement strings. If NUXT_PUBLIC_API_BASE_URL contains query parameters (e.g., ?key=val&other=1), the substitution will result in corrupted files because & will be replaced by the matched placeholder string. Additionally, using | as a delimiter will fail if the URL contains a pipe character. Escaping these characters ensures robust URL substitution.

Suggested change
if [ -n "$NUXT_PUBLIC_API_BASE_URL" ]; then
if [ -n "$NUXT_PUBLIC_API_BASE_URL" ]; then
# Escape special characters for sed: & (backreference) and | (delimiter)
SAFE_URL=$(printf '%s\n' "$NUXT_PUBLIC_API_BASE_URL" | sed 's/[&|]/\\&/g')
find /app/.output -type f \( -name '*.mjs' -o -name '*.js' -o -name '*.cjs' \) \
-exec sed -i "s|http://__NUXT_PUBLIC_API_BASE_URL__|${SAFE_URL}|g" {} +
fi

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
Comment on lines +8 to +11

exec node .output/server/index.mjs
Loading