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

View File

@ -26,23 +26,22 @@ RUN npm run build
# ── 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
RUN echo ':4321 {\n\
root * /srv\n\
encode gzip\n\
try_files {path} {path}/index.html /index.html\n\
file_server\n\
header {\n\
X-Content-Type-Options nosniff\n\
X-Frame-Options DENY\n\
Referrer-Policy strict-origin-when-cross-origin\n\
}\n\
}' > /etc/caddy/Caddyfile
# Copy the built Astro SSR server
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
# Run as non-root
RUN useradd --create-home --shell /bin/bash astro
USER astro
ENV HOST=0.0.0.0
ENV PORT=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';
const API_BASE = (() => {
// In SSR context, import.meta.env may not have PUBLIC_ vars populated the same way.
// Prefer the PUBLIC_ env var, fall back to localhost for dev.
if (typeof import.meta !== 'undefined' && import.meta.env?.PUBLIC_API_URL) {
// PUBLIC_API_URL controls where API requests go:
// - Dev (local): 'http://localhost:8099'
// - 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 'http://localhost:8099';