# syntax=docker/dockerfile:1.7 # Build stage — produces the static SPA bundle in /app/build. FROM node:24-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type=cache,target=/root/.npm npm ci COPY . . RUN npm run build # Runtime stage — a minimal Caddy that serves the SPA and falls back to # index.html for client-side routing. The main reverse-proxy Caddy in # the compose stack proxies `/` to this container. FROM caddy:2-alpine AS runtime WORKDIR /srv COPY --from=builder /app/build /srv # SvelteKit was built with paths.base='/admin', so the bundle's URLs # already include the /admin/ prefix. The outer Caddy forwards # /admin/* here verbatim; we strip the prefix and serve from /srv # with index.html as the SPA fallback. Bare / 404s (the outer Caddy # already sends user routes to picloud, so we shouldn't see them here). RUN printf '{\n\tauto_https off\n\tadmin off\n}\n:80 {\n\thandle_path /admin* {\n\t\troot * /srv\n\t\ttry_files {path} /index.html\n\t\tfile_server\n\t\tencode zstd gzip\n\t}\n\thandle {\n\t\trespond 404\n\t}\n\tlog {\n\t\toutput stdout\n\t\tformat console\n\t}\n}\n' > /etc/caddy/Caddyfile EXPOSE 80 HEALTHCHECK --interval=10s --timeout=2s --retries=3 \ CMD wget -qO- http://127.0.0.1/admin/ >/dev/null 2>&1 || exit 1