diff --git a/.env.example b/.env.example index 2954781..8d5275b 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,17 @@ -# Copy to .env and adjust as needed. -COMPOSE_PROJECT=hai-omni-docs -DOMAIN=hai-omni-pro-ii.l.warehack.ing +# Copy to .env and edit. `make dev` and `make prod` rewrite APP_ENV/APP_PORT +# in place, so leave those two lines as-is — the Makefile owns them. + +# Container + network names. Keep distinct so multiple stacks don't clash. +COMPOSE_PROJECT_NAME=omni-pca-docs + +# Mode toggle. dev = Astro dev server + HMR on 4321; prod = static caddy on 80. +APP_ENV=prod +APP_PORT=80 + +# Public hostname Caddy will route to this container. Match the DNS record. +PUBLIC_DOMAIN=hai-omni-pro-ii.warehack.ing + +# Vite HMR client hostname — only used in dev. Same as PUBLIC_DOMAIN when +# you want to develop through the live reverse proxy with a real cert, +# or 'localhost' for local-only. +VITE_HMR_HOST=hai-omni-pro-ii.warehack.ing diff --git a/Caddyfile b/Caddyfile index ebcc331..4588c2d 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,22 +1,13 @@ -# Inner Caddy: just serves the static dist on :80. -# The host's caddy-docker-proxy handles TLS and routing for the public hostname. - :80 { root * /srv - encode zstd gzip - file_server + encode gzip - # SPA-ish fallback: prefer the directory's index.html, then a 404 page. - try_files {path} {path}/ /404.html - - header { - # Light security defaults; the outer Caddy can override. - X-Content-Type-Options "nosniff" - Referrer-Policy "strict-origin-when-cross-origin" - Permissions-Policy "interest-cohort=()" + handle /health { + respond "ok" 200 } - # Long cache for fingerprinted assets emitted by Astro. - @hashed path_regexp \.[A-Za-z0-9_-]{8,}\.(js|css|woff2?|svg|png|jpg|jpeg|webp|avif)$ - header @hashed Cache-Control "public, max-age=31536000, immutable" + handle { + try_files {path} {path}/index.html {path}/ /index.html + file_server + } } diff --git a/Dockerfile b/Dockerfile index 6d899f5..2c9cd83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,39 +1,34 @@ # syntax=docker/dockerfile:1.7 +# +# Multi-stage Dockerfile matching the warehack-ing services convention: +# * dev — Astro dev server with HMR, source-mounted via compose volumes +# * build — runs `astro build`, produces /app/dist +# * prod — Caddy serves the static dist on port 80, plus a /health probe +# +# Pick the stage at compose-build time via APP_ENV={dev,prod} in .env. -# ---- builder ---- -# Pinned to lts-alpine so it works against Docker Hub's cached image set. -# Bump to node:25-alpine when 25 stabilises in the registry. -FROM node:lts-alpine AS builder - -ENV ASTRO_TELEMETRY_DISABLED=1 \ - NODE_ENV=production \ - CI=true - +# ── base: shared dependency install ────────────────────────────── +FROM node:lts-slim AS base WORKDIR /app - -# Install deps in their own layer for better caching. +ENV ASTRO_TELEMETRY_DISABLED=1 COPY package.json package-lock.json ./ -RUN --mount=type=cache,target=/root/.npm \ - npm ci --include=dev - -# Copy the rest of the source and build. +RUN npm ci COPY . . -RUN npm run build -# ---- runtime ---- -FROM caddy:latest AS runtime +# ── dev: Astro dev server with HMR ────────────────────────────── +FROM base AS dev +ENV HOST=0.0.0.0 +EXPOSE 4321 +CMD ["npx", "astro", "dev", "--host", "0.0.0.0", "--port", "4321"] -# Run as non-root. -RUN addgroup -S docs && adduser -S -G docs docs +# ── build: static site generation ─────────────────────────────── +FROM base AS build +RUN npx astro build -WORKDIR /srv - -COPY --from=builder /app/dist /srv +# ── prod: Caddy serving static files ──────────────────────────── +FROM caddy:2-alpine AS prod COPY Caddyfile /etc/caddy/Caddyfile - -# caddy:2-alpine ships with a sane default user model; keep ports unprivileged. +COPY --from=build /app/dist /srv EXPOSE 80 - -USER docs - -CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1:80/health || exit 1 diff --git a/Makefile b/Makefile index 287f27d..2e511b1 100644 --- a/Makefile +++ b/Makefile @@ -1,50 +1,37 @@ -# hai-omni-docs Makefile -# -# Local dev: make dev (Astro dev server, hot reload, no docker) -# Smoke build: make ci (npm run build — verify static output) -# Deploy: make build && make up -# -# Reads .env if present so DOMAIN / COMPOSE_PROJECT propagate to compose. +.PHONY: up down logs rebuild dev prod clean ci -SHELL := /bin/bash -COMPOSE := docker compose - -.PHONY: help dev install ci build up down restart logs ps clean - -help: - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ - | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' - -install: ## Install npm dependencies - npm install - -dev: ## Start Astro dev server with hot reload (host) - npm run dev - -ci: ## Smoke-test the static build locally - npm run build - -build: ## Build the docker image - $(COMPOSE) build - -up: ## Start the docs container in the background - $(COMPOSE) up -d +up: + docker compose up -d --build @echo @echo "==> Tailing recent logs ..." - @$(COMPOSE) logs -f --tail=20 + @docker compose logs -f --tail=20 -down: ## Stop and remove the docs container - $(COMPOSE) down +down: + docker compose down -restart: ## Recreate the docs container - $(COMPOSE) up -d --force-recreate - @$(COMPOSE) logs -f --tail=20 +logs: + docker compose logs -f --tail=50 -logs: ## Follow logs - $(COMPOSE) logs -f --tail=50 +rebuild: + docker compose down + docker compose up -d --build + @docker compose logs -f --tail=20 -ps: ## Show container status - $(COMPOSE) ps +# Flip .env into dev mode (Astro dev server + HMR on 4321) and rebuild. +dev: + @sed -i 's/^APP_ENV=.*/APP_ENV=dev/' .env + @sed -i 's/^APP_PORT=.*/APP_PORT=4321/' .env + $(MAKE) rebuild -clean: ## Remove build artifacts - rm -rf dist .astro +# Flip .env into prod mode (Caddy serving static dist on 80) and rebuild. +prod: + @sed -i 's/^APP_ENV=.*/APP_ENV=prod/' .env + @sed -i 's/^APP_PORT=.*/APP_PORT=80/' .env + $(MAKE) rebuild + +clean: + docker compose down --rmi local -v + +# Smoke-test the static build outside Docker. +ci: + npm ci && npm run build diff --git a/astro.config.mjs b/astro.config.mjs index 257cfb9..bde2960 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -84,6 +84,17 @@ export default defineConfig({ vite: { server: { host: '0.0.0.0', + // HMR behind Caddy reverse proxy: explicit hostname + wss + 443 + // (per CLAUDE.md "HMR Behind Reverse Proxy" section). Falls + // back to defaults when no VITE_HMR_HOST env var is set so + // `npm run dev` outside Docker still works. + hmr: process.env.VITE_HMR_HOST + ? { + host: process.env.VITE_HMR_HOST, + protocol: 'wss', + clientPort: 443, + } + : undefined, }, }, }); diff --git a/docker-compose.yml b/docker-compose.yml index 8ccf9b1..27e60fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,41 @@ +# Astro + Starlight docs site for warehack.ing/omni-pca. +# +# Two modes via APP_ENV in .env: +# APP_ENV=dev APP_PORT=4321 astro dev with HMR (volume-mounted) +# APP_ENV=prod APP_PORT=80 caddy serving the built static dist +# +# Switch with `make dev` or `make prod` (both rebuild + restart). + services: docs: build: context: . - dockerfile: Dockerfile - image: ${COMPOSE_PROJECT:-hai-omni-docs}/docs:latest - container_name: ${COMPOSE_PROJECT:-hai-omni-docs}-docs + target: ${APP_ENV:-dev} + container_name: ${COMPOSE_PROJECT_NAME:-omni-pca-docs} restart: unless-stopped + environment: + - PUBLIC_DOMAIN=${PUBLIC_DOMAIN} + - VITE_HMR_HOST=${VITE_HMR_HOST} networks: - caddy - expose: - - "80" labels: - caddy: ${DOMAIN:-hai-omni-pro-ii.l.warehack.ing} - caddy.reverse_proxy: "{{upstreams 80}}" + caddy: ${PUBLIC_DOMAIN} + caddy.reverse_proxy: "{{upstreams ${APP_PORT:-4321}}}" + # Streaming/HMR-friendly Caddy config (CLAUDE.md "HMR Behind Caddy"): + caddy.reverse_proxy.flush_interval: "-1" + caddy.reverse_proxy.transport: http + caddy.reverse_proxy.transport.read_timeout: "0" + caddy.reverse_proxy.transport.write_timeout: "0" + caddy.reverse_proxy.transport.keepalive: 5m + caddy.reverse_proxy.transport.keepalive_idle_conns: "10" + caddy.reverse_proxy.stream_timeout: 24h + caddy.reverse_proxy.stream_close_delay: 5s + # Volume mounts only matter in dev mode; in prod they're harmless + # (the prod stage doesn't reference /app — it serves /srv from caddy). + volumes: + - ./src:/app/src + - ./public:/app/public + - ./astro.config.mjs:/app/astro.config.mjs networks: caddy: