diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d94b90c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +dist +dev-dist +.git +.gitignore +.vite +*.log +.DS_Store +.vscode +.idea +Dockerfile +docker-compose.yml +README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6be922e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:1 + +# ---- Build stage ------------------------------------------------------- +FROM node:22-alpine AS build +WORKDIR /app + +# Install dependencies against the lockfile first (better layer caching). +COPY package.json package-lock.json ./ +RUN npm ci + +# Build the static site. `prebuild` fetches a PokéAPI snapshot into +# src/data/snapshot.json — if that file is already present and fresh it is +# reused, otherwise this stage needs network access to pokeapi.co. +# Serving under a sub-path? Pass --build-arg BASE_PATH=/subdir/. +COPY . . +ARG BASE_PATH=/ +ENV BASE_PATH=${BASE_PATH} +RUN npm run build + +# ---- Runtime stage --------------------------------------------------- +FROM nginx:1.27-alpine AS runtime +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +# The base image's CMD already runs: nginx -g 'daemon off;' diff --git a/README.md b/README.md index b3960c1..f254fde 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,24 @@ npm run preview # serve the production build (needed to exercise the servic Deploying under a sub-path (e.g. GitHub Pages project site): `BASE_PATH=/repo-name/ npm run build`. +## Docker + +Multi-stage build (Node → nginx) that serves the static `dist/`: + +```bash +docker compose up --build # → http://localhost:8080 +# or without compose: +docker build -t pokedex-pwa . +docker run --rm -p 8080:80 pokedex-pwa +``` + +The build stage runs `npm run build`, whose `prebuild` step fetches the +PokéAPI snapshot — so `docker build` needs network access to `pokeapi.co` +unless a fresh `src/data/snapshot.json` is already present (it gets copied in +and reused). Serving under a sub-path: `docker build --build-arg BASE_PATH=/dex/ .`. +`nginx.conf` sets the SPA fallback and long-cache headers for `/assets/*` +while keeping `index.html` / `sw.js` uncached. + ## Project layout ``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2b664d9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + web: + build: + context: . + args: + BASE_PATH: / + image: pokedex-pwa + ports: + - "8080:80" + restart: unless-stopped diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..a3035bd --- /dev/null +++ b/nginx.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Hash-routed SPA: unknown paths fall back to the app shell. + location / { + try_files $uri $uri/ /index.html; + } + + # Content-hashed build output — safe to cache forever. + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Entry point and service worker must always be revalidated. + location = /index.html { + add_header Cache-Control "no-cache"; + } + location = /sw.js { + add_header Cache-Control "no-cache"; + } + location = /manifest.webmanifest { + default_type application/manifest+json; + add_header Cache-Control "no-cache"; + } + + gzip on; + gzip_comp_level 5; + gzip_min_length 1024; + gzip_types text/css application/javascript application/json + image/svg+xml application/manifest+json; +}