Skip to content
Merged
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
36 changes: 29 additions & 7 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ jobs:
publish:
name: Build and push (${{ matrix.service }})
runs-on: ubuntu-24.04
if: hashFiles(format('{0}/Dockerfile', matrix.service)) != ''
strategy:
fail-fast: true # if backend fails, don't push frontend mismatched
# Each image stands on its own — a verify-step crash on one service
# must not cancel the other half-built. We tolerate the rare case
# where one image gets a tag and the other doesn't; the user can
# re-run the failing matrix leg from the Actions UI.
fail-fast: false
matrix:
service: [backend, frontend]
Comment on lines 24 to 31
steps:
- uses: actions/checkout@v6

- name: Verify Dockerfile exists for ${{ matrix.service }}
run: test -f "${{ matrix.service }}/Dockerfile"

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

Expand All @@ -42,8 +48,15 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# `secrets.*` is not allowed in step-level `if:` expressions — Actions
# rejects it as "Unrecognized named-value: 'secrets'". We surface the
# value into the step env first, then condition on env.* which IS
# allowed.
- name: Log in to Docker Hub
if: secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != ''
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
if: env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != ''
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
Expand Down Expand Up @@ -115,20 +128,29 @@ jobs:
provenance: true
sbom: true

# `steps.meta.outputs.tags` is a newline-separated list. Interpolating
# it directly into the shell command (`for tag in ${{ … }}`) injects
# raw newlines into the script body and breaks the parser. Pipe via
# env: instead, then read line-by-line.
- name: Verify multi-arch manifest
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
for tag in ${{ steps.meta.outputs.tags }}; do
fail=0
while IFS= read -r tag; do
[ -z "$tag" ] && continue
echo "Inspecting $tag …"
archs=$(docker buildx imagetools inspect "$tag" --raw \
| jq -r '.manifests[].platform | "\(.os)/\(.architecture)"' \
| sort -u)
echo " architectures: $(echo "$archs" | tr '\n' ' ')"
if ! echo "$archs" | grep -q "linux/amd64"; then
echo "::error::Missing linux/amd64 in $tag"
exit 1
fail=1
fi
if ! echo "$archs" | grep -q "linux/arm64"; then
echo "::error::Missing linux/arm64 in $tag"
exit 1
fail=1
fi
done
done <<< "$TAGS"
exit "$fail"
40 changes: 40 additions & 0 deletions frontend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Keep build context minimal.

# Build output
/bin/
/dist/
/out/
*.exe
*.test
*.prof

# Test artefacts
coverage.out

# IDE
.vscode/
.idea/

# OS
.DS_Store
Thumbs.db

# Local dev
.env
.env.local
*.log

# Secrets — never copy
*.pem
*.key
*.crt
secrets/

# Git
.git/
.gitignore

# Node toolchain (Tailwind build cache — comes in a later PR)
node_modules/
.npm/
.pnpm-store/
104 changes: 104 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# =============================================================================
# Label Printer Hub — frontend container
#
# Multi-stage build:
# 1) builder — Go toolchain compiles a static binary
# 2) runtime — alpine + tini + curl + non-root UID 1000
#
# Build context: the `frontend/` directory.
#
# Tailwind, HTMX, PWA assets and the OpenAPI-generated backend client will
# land in follow-up commits — this skeleton produces a deployable container
# that serves /healthz so the docker-publish workflow has something real to
# push at the next release.
#
# References:
# docs/decisions/0001-two-container-architecture.md (two-container split)
# docs/decisions/0003-go-tailwind-htmx-pwa-frontend.md
# docs/decisions/0007-docker-image-tag-scheme.md (multi-arch publish)
# CLAUDE.md hard rules (non-root UID 1000)
# =============================================================================

# -----------------------------------------------------------------------------
# Stage 1: builder
# -----------------------------------------------------------------------------
FROM golang:1.23-alpine AS builder

WORKDIR /build

# Stage A: dependency download — cached separately from source. Subsequent
# code edits don't re-resolve modules unless go.mod / go.sum change.
COPY go.mod go.sum ./
RUN go mod download

# Stage B: compile. Static binary so we don't need libc in the runtime image.
COPY . ./
# CGO_ENABLED=0 produces a fully static binary. -ldflags strip debug info
# and shrink the binary (~30-40% smaller).
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w" \
-o /out/server \
./cmd/server

# -----------------------------------------------------------------------------
# Stage 2: runtime
# -----------------------------------------------------------------------------
FROM alpine:3.20 AS runtime

# Build-time arguments — set by the CI workflow (.github/workflows/docker-publish.yml).
# Local builds without --build-arg get the placeholder defaults below.
ARG VERSION=0.0.0-dev
ARG REVISION=unknown
ARG BUILD_DATE=1970-01-01T00:00:00Z

# OCI image labels — visible via `docker inspect`. Keep in sync with the
# backend Dockerfile so both images describe themselves identically.
LABEL org.opencontainers.image.title="label-printer-hub-frontend" \
org.opencontainers.image.description="Self-hosted label printer hub for Brother PT/QL series — frontend container (Go + Tailwind + HTMX + PWA)" \
org.opencontainers.image.url="https://github.com/strausmann/label-printer-hub" \
org.opencontainers.image.source="https://github.com/strausmann/label-printer-hub" \
org.opencontainers.image.documentation="https://github.com/strausmann/label-printer-hub/tree/main/docs" \
org.opencontainers.image.authors="Björn Strausmann <strausmannservices@googlemail.com>" \
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

This line contains the maintainer's real name and email address, which constitutes Personally Identifiable Information (PII). According to the repository's privacy policy, PII should not be hardcoded in the repository to ensure the maintainer's privacy and prevent network deduction.

      org.opencontainers.image.authors="Maintainer <maintainer@example.invalid>" \
References
  1. Privacy violations. Flag any hardcoded LAN IPs, real hostnames, real domains, real tokens, or PII. The maintainer's network must not be deducible from this repository. (link)

org.opencontainers.image.vendor="strausmann (independent open-source project; not affiliated with Brother Industries, Ltd.)" \
org.opencontainers.image.licenses="MIT" \
Comment on lines +57 to +64
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.base.name="docker.io/library/alpine:3.20"

ENV PORT=8080 \
BACKEND_URL=http://backend:8000 \
HUB_VERSION="${VERSION}" \
HUB_REVISION="${REVISION}" \
HUB_BUILD_DATE="${BUILD_DATE}" \
HUB_REPO_URL="https://github.com/strausmann/label-printer-hub"

# Runtime essentials:
# tini — PID 1 signal handler so SIGTERM propagates cleanly
# curl — HEALTHCHECK probe
# ca-certificates — outbound HTTPS to the backend (when proxying SSE/API)
# Then create the non-root user (UID 1000, matches the backend container).
RUN apk add --no-cache tini curl ca-certificates \
&& addgroup -S -g 1000 hub \
&& adduser -S -u 1000 -G hub -s /sbin/nologin -h /home/hub hub

# Copy the compiled binary from the builder. No source code, no Go toolchain
# in the runtime image.
COPY --from=builder --chown=hub:hub /out/server /usr/local/bin/server

USER hub
WORKDIR /home/hub

# EXPOSE documents the DEFAULT port — overridden by the PORT env var at
# runtime. Adjust the `docker run -p` mapping or compose `ports` to match
# the value of PORT if you change it.
EXPOSE 8080

# Shell-form HEALTHCHECK so $PORT is expanded by the shell.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD curl --fail --silent --show-error "http://localhost:${PORT:-8080}/healthz" || exit 1

# tini reaps zombies and propagates SIGTERM cleanly. The Go server reads $PORT
# from the environment itself, so we don't need a shell wrapper around it.
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/server"]
34 changes: 34 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# label-printer-hub — frontend

Go web server for the [Label Printer Hub](https://github.com/strausmann/label-printer-hub). Serves the user-facing UI and proxies API + SSE requests to the Python backend (see [ADR 0001](../docs/decisions/0001-two-container-architecture.md)).

Stack: Go + chi router. Tailwind CSS, HTMX, PWA assets, and the OpenAPI-generated backend client land in follow-up commits — this skeleton is the buildable container baseline.

## Local development

```bash
go mod download
go test ./...
go run ./cmd/server
```

The server listens on `:8080` by default (override with `PORT=…`). `/healthz` returns the same JSON shape as the backend's `/healthz` so orchestrator probe configs work for both.

## Configuration

| Env var | Default | Purpose |
|---|---|---|
| `PORT` | `8080` | Internal HTTP port |
| `BACKEND_URL` | `http://backend:8000` | Base URL of the Python backend |
| `HUB_VERSION` | `0.0.0-dev` | Baked in by the Dockerfile from the release tag |
| `HUB_REVISION` | `unknown` | Baked in by the Dockerfile from the git SHA |
| `HUB_BUILD_DATE` | `1970-01-01T00:00:00Z` | Baked in by the Dockerfile at build time |
| `HUB_REPO_URL` | `https://github.com/strausmann/label-printer-hub` | Baked in by the Dockerfile |

## Container

The `Dockerfile` produces `ghcr.io/strausmann/label-printer-hub-frontend` — see the [tag scheme ADR](../docs/decisions/0007-docker-image-tag-scheme.md) for which tags every release publishes.

## License

MIT — see [LICENSE](../LICENSE) in the repository root.
Loading