Production deployment: SSR frontend, Caddy path routing

Update frontend Dockerfile prod stage from Caddy static to Node.js
running Astro SSR server. Configure caddy-docker-proxy labels to
route /api/* to backend and everything else to frontend on the same
domain. Support empty PUBLIC_API_URL for same-origin API calls.
This commit is contained in:
Ryan Malloy 2026-02-13 03:37:14 -07:00
parent 5a2c5730c0
commit 4600a7b0a9
3 changed files with 23 additions and 19 deletions

View File

@ -9,6 +9,10 @@ services:
networks: networks:
- default - default
- caddy - caddy
labels:
caddy: "${SPICEBOOK_DOMAIN:-spicebook.localhost}"
caddy.@api.path: "/api/* /health /docs /openapi.json /redoc"
caddy.reverse_proxy_0: "@api {{upstreams 8000}}"
frontend: frontend:
build: build:
@ -22,7 +26,7 @@ services:
- caddy - caddy
labels: labels:
caddy: "${SPICEBOOK_DOMAIN:-spicebook.localhost}" caddy: "${SPICEBOOK_DOMAIN:-spicebook.localhost}"
caddy.reverse_proxy: "{{upstreams 4321}}" caddy.reverse_proxy_1: "{{upstreams 4321}}"
networks: networks:
caddy: caddy:

View File

@ -26,23 +26,22 @@ RUN npm run build
# ── Prod stage ─────────────────────────────────────────────────────── # ── Prod stage ───────────────────────────────────────────────────────
FROM caddy:2-alpine AS prod FROM node:20-slim AS prod
COPY --from=build /app/dist /srv WORKDIR /app
# Simple Caddyfile for serving the static Astro build # Copy the built Astro SSR server
RUN echo ':4321 {\n\ COPY --from=build /app/dist ./dist
root * /srv\n\ COPY --from=build /app/node_modules ./node_modules
encode gzip\n\ COPY --from=build /app/package.json ./
try_files {path} {path}/index.html /index.html\n\
file_server\n\ # Run as non-root
header {\n\ RUN useradd --create-home --shell /bin/bash astro
X-Content-Type-Options nosniff\n\ USER astro
X-Frame-Options DENY\n\
Referrer-Policy strict-origin-when-cross-origin\n\ ENV HOST=0.0.0.0
}\n\ ENV PORT=4321
}' > /etc/caddy/Caddyfile
EXPOSE 4321 EXPOSE 4321
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"] CMD ["node", "dist/server/entry.mjs"]

View File

@ -8,9 +8,10 @@ import type {
} from './types'; } from './types';
const API_BASE = (() => { const API_BASE = (() => {
// In SSR context, import.meta.env may not have PUBLIC_ vars populated the same way. // PUBLIC_API_URL controls where API requests go:
// Prefer the PUBLIC_ env var, fall back to localhost for dev. // - Dev (local): 'http://localhost:8099'
if (typeof import.meta !== 'undefined' && import.meta.env?.PUBLIC_API_URL) { // - Production: '' (empty = same origin, Caddy routes /api/* to backend)
if (typeof import.meta !== 'undefined' && import.meta.env?.PUBLIC_API_URL != null) {
return import.meta.env.PUBLIC_API_URL; return import.meta.env.PUBLIC_API_URL;
} }
return 'http://localhost:8099'; return 'http://localhost:8099';