diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fb64ffe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +node_modules +dist +dist-ssr +.vite +.git +.github +.idea +.vscode +.claude +*.log +npm-debug.log* +.DS_Store +Dockerfile +.dockerignore +README.md +LICENSE diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c103ba9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# ---- Build stage ---- +FROM node:22-alpine AS build +WORKDIR /app + +# Install dependencies against the lockfile for reproducible builds +COPY package.json package-lock.json ./ +RUN npm ci + +# Build the static site (tsc -b && vite build -> /app/dist) +COPY . . +RUN npm run build + +# ---- Serve stage ---- +FROM nginx:1.27-alpine AS runtime + +# SPA-aware nginx config (history fallback + asset caching) +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Static output from the build stage +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://localhost/ >/dev/null 2>&1 || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..25d98ec --- /dev/null +++ b/nginx.conf @@ -0,0 +1,29 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip text assets + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + + # Hashed build assets are immutable -> cache aggressively + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # SPA history fallback: unknown routes serve index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Never cache the HTML entrypoint so new deploys are picked up immediately + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } +}