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
3 changes: 0 additions & 3 deletions frontend/goquery-ui/.env.development

This file was deleted.

3 changes: 3 additions & 0 deletions frontend/goquery-ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ package-lock.json
# deps and distribution
node_modules/
dist/

# local runtime override for the prod compose (host-specific)
.env
21 changes: 19 additions & 2 deletions frontend/goquery-ui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,35 @@ FROM caddy:2-alpine AS caddy-builder
# Use Alpine as the base for better security and fewer vulnerabilities
FROM alpine:3.19

# Install ca-certificates for HTTPS and gettext for envsubst (env.js generation)
RUN apk add --no-cache ca-certificates gettext
# Install ca-certificates for HTTPS, gettext for envsubst (env.js generation),
# and libcap to strip file capabilities from the Caddy binary (see below).
RUN apk add --no-cache ca-certificates gettext libcap

# Copy Caddy binary from the builder image
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy

# The official Caddy binary carries cap_net_bind_service=+ep so it can bind to
# :80/:443 as non-root. BuildKit's COPY preserves that file capability. Under a
# hardened runtime (allowPrivilegeEscalation: false → no_new_privs), the kernel
# refuses to exec() a binary that carries file caps and returns EPERM
# ("caddy: Operation not permitted"). We listen on :5137 (non-privileged), so the
# capability is unnecessary — strip it to keep the binary execable when hardened.
RUN setcap -r /usr/bin/caddy

# Create a non-root user for running Caddy
RUN addgroup -g 1000 caddy && \
adduser -D -u 1000 -G caddy -h /var/lib/caddy -s /sbin/nologin caddy
RUN mkdir -p /var/run/env /opt/app/www /etc/caddy /data /config && \
chown -R caddy:caddy /var/run/env /opt/app/www /etc/caddy /data /config

# Point Caddy's storage at the dirs we create + back with writable volumes.
# The official caddy image sets these; our alpine base does not, so without them
# Caddy falls back to $HOME/.local/share & $HOME/.config — which are on the
# read-only rootfs in production, producing errors on startup. (The /data and
# /config volume mounts in the Helm chart / compose are inert until this is set.)
ENV XDG_DATA_HOME=/data \
XDG_CONFIG_HOME=/config

# static files go to a read-only dir
COPY --from=build --chown=caddy:caddy /app/dist /opt/app/www
## ensure protocol map is present as a static asset as well
Expand Down
7 changes: 0 additions & 7 deletions frontend/goquery-ui/Dockerfile.dev

This file was deleted.

16 changes: 8 additions & 8 deletions frontend/goquery-ui/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ help:
@echo " make generate-index - create a minimal index.html if missing"
@echo " make types - fetch OpenAPI $(VERSION) and generate TS types ($(GENERATED_TYPES))"
@echo " make regen - install + types + build"
@echo " make docker-dev - docker compose dev server (hot reload)"
@echo " make docker-stop - stop docker compose dev server"
@echo " make docker-logs - view docker compose dev logs"
@echo " make docker-up - run the hardened image via docker compose"
@echo " make docker-stop - stop the docker compose stack"
@echo " make docker-logs - view docker compose logs"
@echo " make docker-build - build production image locally"

# install: re-run if package.json or chosen lockfile changes
Expand Down Expand Up @@ -81,7 +81,7 @@ serve: build

.PHONY: dev
dev: build
@echo "DEPRECATED: preferably use \"make docker-dev\""
@echo "DEPRECATED: preferably use \"npm run dev\" (hot reload on :5173)"
@echo "Run these in two terminals for live development:" \
"\n Terminal 1: make watch" \
"\n Terminal 2: make serve" \
Expand All @@ -95,10 +95,10 @@ clean:
format: install
$(NPM) exec -- prettier --write "src/**/*.{js,jsx,ts,tsx}"

.PHONY: docker-dev
docker-dev:
@echo "Starting Docker development server with hot reload..."
@echo "Access the application at http://localhost:5173"
.PHONY: docker-up
docker-up:
@echo "Starting the hardened Caddy image (mirrors the Helm chart securityContext)..."
@echo "Access the application at http://localhost:8080"
docker compose up --build

.PHONY: docker-stop
Expand Down
25 changes: 8 additions & 17 deletions frontend/goquery-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,13 @@ npm ci

## Develop

Run with Docker Compose from this folder.

### Dev (hot reload via webpack-dev-server)

```bash
docker compose --profile dev up
```

Open <http://localhost:5173>

### Without Docker

Start webpack in watch mode and open `index.html` in a local static server (or your browser directly):
Hot reload via webpack-dev-server:

```bash
npm run dev
```

The bundle is emitted to `dist/` and `index.html` loads it.
Open <http://localhost:5173>

## Build

Expand All @@ -45,8 +33,12 @@ npm run build

### Prod-like (Caddy serves built SPA with runtime env.js)

Runs the production image hardened to mirror the Helm chart's `securityContext`
(read-only rootfs, non-root, no-new-privileges, all caps dropped), so
deployment-time issues surface locally:

```bash
docker compose --profile prod up --build
docker compose up --build
```

Open <http://localhost:8080>
Expand All @@ -62,8 +54,7 @@ SSE_ON_LOAD=true
Makefile shortcuts:

```bash
make docker-dev # same as compose dev profile
make docker-prod # same as compose prod profile
make docker-up # run the hardened image (docker compose up --build)
make docker-build # build Caddy image locally
```

Expand Down
63 changes: 51 additions & 12 deletions frontend/goquery-ui/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,61 @@
# Local development only — hot reload with webpack dev server.
# Production builds use the self-contained Dockerfile directly.
# Production Caddy image (Dockerfile), hardened to mirror the goquery-ui Helm
# chart's securityContext so deployment-time issues (e.g. the Caddy
# file-capability / no-new-privileges EPERM) surface locally instead of in the
# cluster. Local hot-reload development is `npm run dev` (no container needed).
#
# Usage:
# docker compose up (or: make docker-dev)
# docker compose up --build (or: make docker-up)
# docker compose down

services:
dev:
goquery-ui:
build:
context: .
dockerfile: Dockerfile.dev
command: npm run dev
dockerfile: Dockerfile
ports:
- "5173:5173"
env_file:
- .env.development
- "8080:5137"
# Runtime config — override via an optional .env next to this file.
environment:
NODE_ENV: development
GQ_API_BASE_URL: "${GQ_API_BASE_URL:-}"
HOST_RESOLVER_TYPES: "${HOST_RESOLVER_TYPES:-string}"
SSE_ON_LOAD: "${SSE_ON_LOAD:-true}"
GQ_BACKEND_HOST: "${GQ_BACKEND_HOST:-}"

# --- Hardening: 1:1 with charts/goquery-ui/values.yaml securityContext ---
# k8s runAsNonRoot/runAsUser/runAsGroup
user: "1000:1000"
# k8s readOnlyRootFilesystem: true
read_only: true
security_opt:
# k8s allowPrivilegeEscalation: false (sets no_new_privs)
- "no-new-privileges:true"
# k8s seccompProfile.type: RuntimeDefault has no compose line — Docker
# applies its default seccomp profile automatically (omitting it == default;
# only "seccomp:unconfined" would disable it).
cap_drop:
# k8s capabilities.drop: [ALL]
- ALL
# readOnlyRootFilesystem is on, so every path the entrypoint/Caddy writes
# to needs a writable mount — mirrors the chart's four emptyDir volumes.
# mode 01777 (sticky, world-writable) lets the non-root user (uid 1000)
# write: Docker mounts tmpfs over the image's pre-created dirs as root:755,
# and there is no docker equivalent of the chart's fsGroup to fix ownership.
volumes:
- ./:/app:delegated
- /app/node_modules
- type: tmpfs
target: /var/run/env # generated env.js (served at /env.js)
tmpfs: { mode: 01777 }
- type: tmpfs
target: /tmp # entrypoint scratch (env.js template)
tmpfs: { mode: 01777 }
- type: tmpfs
target: /data # Caddy data dir
tmpfs: { mode: 01777 }
- type: tmpfs
target: /config # Caddy config dir
tmpfs: { mode: 01777 }
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:5137/"]
interval: 10s
timeout: 3s
retries: 5
start_period: 5s
Loading