Files
picpeak/frontend/Dockerfile
T
Paul Nothaft efa6b4a205 fix(brand-title): runtime substitution so GHCR-image users can override (#521 follow-up)
@Rekoo-PS confirmed the prior #521 fix landed on beta but reported the
preview still shows the default "PicPeak" title — their brand is
"arkan-studio". Root cause: that fix used Vite's build-time
%VITE_DEFAULT_TITLE% substitution. Self-hosters running the pre-built
ghcr.io/the-luap/picpeak/frontend image can't override at build time
without rebuilding, so they were stuck with whatever the upstream
build baked in.

Pivot to runtime substitution: the frontend container now reads
BRAND_TITLE / BRAND_DESCRIPTION env vars on startup and envsubsts
them into index.html. Change the values in .env, restart the frontend
service, done — no rebuild required.

Mechanics:
  - frontend/index.html: tokens are now ${BRAND_TITLE} /
    ${BRAND_DESCRIPTION} (shell expansion syntax, passes through Vite
    unchanged into the built dist).
  - frontend/Dockerfile: install gettext (provides envsubst), snapshot
    /usr/share/nginx/html/index.html → index.html.tpl at build, install
    docker-entrypoint.sh, wire ENTRYPOINT to it. The .tpl is the
    immutable source — every container start re-renders index.html
    from .tpl, so restarts pick up new env values cleanly (no
    accidental "first-boot env stuck forever" trap).
  - frontend/docker-entrypoint.sh: applies defaults if env unset,
    runs envsubst (locked to BRAND_TITLE + BRAND_DESCRIPTION
    explicitly so /assets/*.js template literals aren't touched if
    anyone ever extends substitution to the bundle), execs nginx.
  - frontend/vite.config.ts: drop the htmlTitleDefaults plugin — no
    longer needed since substitution is fully runtime.
  - frontend/.env.example + .env.production.example: drop the
    VITE_DEFAULT_* docs (the vars no longer have effect).
  - docker-compose.yml + docker-compose.production.yml: pass
    BRAND_TITLE / BRAND_DESCRIPTION env into the frontend service
    with sensible defaults so unconfigured installs work unchanged.
  - .env.example: add BRAND_TITLE / BRAND_DESCRIPTION with comment
    pointing at the social-preview use case.

Verified end-to-end against the built image:
  - BRAND_TITLE="Arkan Studio" BRAND_DESCRIPTION="Wedding photographs
    by Arkan Studio" → index.html serves <title>Arkan Studio</title>
    + og:title="Arkan Studio" + og:description correctly substituted.
  - .tpl preserves ${...} tokens so the next restart can re-substitute.
  - Bundle assets unaffected.
  - Defaults applied when env unset → <title>PicPeak</title>.

Docs PR in picpeak-docs describes the two new env vars under
"Social link preview fallback" in the environment-variables reference.

Refs: #521
2026-05-21 10:00:21 +02:00

83 lines
2.5 KiB
Docker

# Build stage
FROM node:20-alpine AS builder
# Add build arguments
ARG CACHEBUST=1
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
# Add labels for GitHub Container Registry
LABEL org.opencontainers.image.source="https://github.com/the-luap/picpeak"
LABEL org.opencontainers.image.description="PicPeak Frontend Application"
LABEL org.opencontainers.image.licenses="MIT"
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --legacy-peer-deps
# Copy source files
COPY . .
# Build the application
RUN npm run build
# Production stage (Alpine 3.23 with OpenSSL 3.5.5, patched libexpat)
FROM nginx:1.28-alpine
# Upgrade all packages to fix security vulnerabilities (OpenSSL, libexpat, BusyBox CVEs)
RUN apk upgrade --no-cache
# Install runtime dependencies. `gettext` provides envsubst, used by
# docker-entrypoint.sh for the BRAND_TITLE / BRAND_DESCRIPTION runtime
# substitution into index.html (#521 — runtime fix for self-hosters
# on the pre-built GHCR image who can't override at build time).
RUN apk add --no-cache curl gettext
# Remove default nginx config
RUN rm -rf /etc/nginx/conf.d/*
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built application from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Snapshot index.html as a template so the entrypoint always renders
# from a known-good source — not from its own previous substitution.
# Container restarts can change BRAND_TITLE freely; the rendered file
# is recomputed from the .tpl each time.
RUN mv /usr/share/nginx/html/index.html /usr/share/nginx/html/index.html.tpl
# Runtime entrypoint that envsubsts the template and execs nginx
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Set permissions (nginx user already exists in nginx:alpine)
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1
# Switch to non-root user
USER nginx
# Start nginx via the entrypoint so each container start re-renders
# index.html from the template against the current BRAND_TITLE /
# BRAND_DESCRIPTION env vars (defaults applied when unset).
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]