From 1b07a1119a2b21fe55c0bf0a47f6e3c27c67cb98 Mon Sep 17 00:00:00 2001 From: Linus Olofsson <61837116+LinusOlofsson-maker@users.noreply.github.com> Date: Wed, 20 May 2026 21:52:21 +0200 Subject: [PATCH] build: add Dockerfile + nginx config for self-hosted deploy (#5) Multi-stage build (node:22-alpine -> nginx:1.27-alpine). Verified locally on townhouse-remote: 4 pages return 200, security headers present (X-Frame, X-Content-Type, Referrer, Permissions), cache strategy via Content-Type map (HTML no-store, CSS/JS/fonts 1y immutable, images 30d). Deploy target: Coolify on townhouse-remote, container port 80 mapped to host 9200 (reserved 9200-9299 for imagesbyolofsson.se stack). Includes: - Dockerfile Multi-stage build with healthcheck - nginx.conf Content-type-driven cache + security headers - .dockerignore Excludes node_modules, .git, dist, .astro --- .dockerignore | 11 +++++++++++ Dockerfile | 20 ++++++++++++++++++++ nginx.conf | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0fd2a5a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.github +node_modules +dist +.astro +.vscode +.idea +*.log +.DS_Store +README.md +LICENSE diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3c14e87 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1.7 + +FROM node:22-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 + +FROM nginx:1.27-alpine AS runtime +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +RUN adduser -D -H -u 101 -s /sbin/nologin nginxuser 2>/dev/null || true +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q --spider http://127.0.0.1/ || exit 1 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..242f4a1 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,46 @@ +map $sent_http_content_type $cache_control_strategy { + default "public, max-age=2592000"; + ~^text/html "no-store, must-revalidate"; + ~^text/css "public, max-age=31536000, immutable"; + ~^application/javascript "public, max-age=31536000, immutable"; + ~^application/json "no-store, must-revalidate"; + ~^image/ "public, max-age=2592000"; + ~^font/ "public, max-age=31536000, immutable"; +} + +server { + listen 80 default_server; + server_name _; + root /usr/share/nginx/html; + index index.html; + + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + add_header Cache-Control $cache_control_strategy always; + + location / { + try_files $uri $uri/ $uri.html $uri/index.html =404; + } + + location /_astro/ { + access_log off; + expires 1y; + } + + location ~* \.(webp|avif|jpg|jpeg|png|gif|svg|ico|woff2?|ttf|otf|eot)$ { + access_log off; + expires 30d; + } + + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss image/svg+xml; + + error_page 404 /404.html; + location = /404.html { + internal; + } +}