diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cb840d9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# Dependencies and build output (rebuilt inside image) +node_modules +dist + +# Version control +.git +.gitignore +.gitattributes + +# Documentation +doc/ +*.md +**/*.md + +# Editor and OS junk +.vscode +.idea +.directory +.DS_Store +Thumbs.db + +# Environment files +.env +.env.* + +# Docker files themselves +Dockerfile +.dockerignore +docker-compose*.yml +stack.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..95db580 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1.7 + +# ---- Stage 1: Build ---- +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy manifests first for cache efficiency +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy source and build +COPY . . +RUN npm run build + +# ---- Stage 2: Runtime ---- +FROM nginx:alpine + +# Replace default config with SPA-aware one +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built static assets +COPY --from=builder /app/dist /usr/share/nginx/html + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +EXPOSE 80 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..2ad6e3c --- /dev/null +++ b/nginx.conf @@ -0,0 +1,39 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_vary on; + gzip_min_length 256; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/xml + image/svg+xml; + + # Vite emits hashed filenames in /assets/ — cache aggressively + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # index.html must NOT be cached — it points at the current bundle hash + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + expires off; + } + + # SPA fallback (defensive — your app has no router today, but harmless) + location / { + try_files $uri $uri/ /index.html; + } + + server_tokens off; +} diff --git a/stack.yml b/stack.yml new file mode 100644 index 0000000..43a4945 --- /dev/null +++ b/stack.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + ptk: + image: git.bennu.duckdns.org/jshackney/pilot-toolkit-web:latest + networks: + - cluster-net + deploy: + replicas: 2 + update_config: + parallelism: 1 + order: start-first + failure_action: rollback + delay: 10s + rollback_config: + parallelism: 1 + order: stop-first + restart_policy: + condition: on-failure + max_attempts: 3 + labels: + - traefik.enable=true + - traefik.http.routers.ptk.rule=Host(`ptk.bennu.duckdns.org`) + - traefik.http.routers.ptk.entrypoints=websecure + - traefik.http.routers.ptk.tls.certresolver=letsencrypt + - traefik.http.services.ptk.loadbalancer.server.port=80 + - traefik.docker.network=cluster-net + - traefik.http.routers.ptk.middlewares=crowdsec@docker + resources: + limits: + memory: 64M + reservations: + memory: 16M + +networks: + cluster-net: + external: true