mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-29 16:17:25 +09:00
Stock nginx (1.18-1.24 on Debian/Ubuntu) has no `.mjs` entry in mime.types, so the runtime-extension vendor shims the import map points at came back as application/octet-stream. Browsers refuse to execute an ES module with that type, so every runtime-loaded extension (WordPress, Git, Mail, Fail2ban, ...) failed on fresh installs with "Failed to fetch dynamically imported module: blob:..." (Chrome) or "'application/octet-stream' is not a valid JavaScript MIME type" (Safari). Add a location block to the shipped vhosts (and the container frontend config) that forces text/javascript for /serverkit-vendor/*.mjs. The regex is anchored to that folder: a bare `\.mjs$` would also capture the /api/v1/plugins/<slug>/assets/dist/index.mjs route and 404 every bundle. update.sh already re-copies these vhosts, so existing installs pick it up on their next update. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWc84WDsWR7M55Mn7qtpie
206 lines
12 KiB
Plaintext
206 lines
12 KiB
Plaintext
# ServerKit - Main Application
|
|
# /etc/nginx/sites-available/serverkit.conf
|
|
#
|
|
# This is the default server block for ServerKit.
|
|
# It works both WITH and WITHOUT Cloudflare in front of the server.
|
|
#
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# CLOUDFLARE USERS (recommended for zero-downtime certs + DDoS protection)
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# 1. In Cloudflare dashboard → SSL/TLS → Overview:
|
|
# Set Encryption mode to "Full (strict)". NEVER use "Flexible".
|
|
# 2. Create an Origin CA certificate:
|
|
# SSL/TLS → Origin Server → Create Certificate → choose RSA.
|
|
# 3. Save the files on this server, for example:
|
|
# /etc/nginx/ssl/cloudflare-origin.pem (certificate)
|
|
# /etc/nginx/ssl/cloudflare-origin.key (private key)
|
|
# 4. Uncomment the Cloudflare Origin CA paths below.
|
|
# 5. Recommended Cloudflare dashboard toggles for painless UX:
|
|
# - SSL/TLS → Edge Certificates → Always Use HTTPS: ON
|
|
# - SSL/TLS → Edge Certificates → Automatic HTTPS Rewrites: ON
|
|
# - SSL/TLS → Edge Certificates → HSTS: ON
|
|
# (max-age 63072000, Include subdomains: ON, Preload: ON, No-Sniff: ON)
|
|
# - SSL/TLS → Edge Certificates → Minimum TLS Version: TLS 1.2
|
|
# 6. Add a CAA DNS record allowing Cloudflare's certificate authorities, e.g.:
|
|
# example.com. CAA 0 issue "letsencrypt.org"
|
|
# example.com. CAA 0 issue "pki.goog"
|
|
# example.com. CAA 0 issue "digicert.com"
|
|
# example.com. CAA 0 issuewild "letsencrypt.org"
|
|
# example.com. CAA 0 iodef "mailto:security@example.com"
|
|
#
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# STANDALONE USERS (no Cloudflare)
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# 1. Obtain Let's Encrypt certificates, e.g.:
|
|
# certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com
|
|
# 2. Set the Let's Encrypt paths below.
|
|
# 3. Add a CAA record for your chosen CA, e.g.:
|
|
# example.com. CAA 0 issue "letsencrypt.org"
|
|
# example.com. CAA 0 issuewild "letsencrypt.org"
|
|
#
|
|
# Optional: place a canonical-domain redirect block in
|
|
# /etc/nginx/serverkit-canonical-domain.conf (managed by install.sh / settings UI).
|
|
|
|
server {
|
|
listen 80 default_server;
|
|
server_name _;
|
|
|
|
# Allow Let's Encrypt validation over plain HTTP
|
|
location /.well-known/acme-challenge/ {
|
|
root /var/www/certbot;
|
|
}
|
|
|
|
# Redirect all other HTTP traffic to HTTPS
|
|
location / {
|
|
return 301 https://$host$request_uri;
|
|
}
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2 default_server;
|
|
server_name _;
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# TLS certificates - pick ONE option and comment the other out
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
# Option A: Let's Encrypt (standalone mode)
|
|
ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;
|
|
|
|
# Option B: Cloudflare Origin CA (use with Cloudflare proxy + Full (strict))
|
|
# ssl_certificate /etc/nginx/ssl/cloudflare-origin.pem;
|
|
# ssl_certificate_key /etc/nginx/ssl/cloudflare-origin.key;
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Modern TLS configuration
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
|
ssl_prefer_server_ciphers off;
|
|
ssl_session_timeout 1d;
|
|
ssl_session_cache shared:SSL:50m;
|
|
ssl_session_tickets off;
|
|
|
|
# OCSP stapling (optional; improves handshake privacy/performance)
|
|
# For Let's Encrypt, uncomment all three lines and set the chain path.
|
|
# Not applicable to Cloudflare Origin CA certificates.
|
|
# ssl_stapling on;
|
|
# ssl_stapling_verify on;
|
|
# ssl_trusted_certificate /etc/letsencrypt/live/YOUR_DOMAIN/chain.pem;
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Security headers
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
|
add_header X-Frame-Options "DENY" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
|
# Keep the panel out of search indexes (scanners ignore it — real hiding is
|
|
# access control; this just stops accidental Google/Bing indexing).
|
|
add_header X-Robots-Tag "noindex, nofollow, noarchive" always;
|
|
|
|
# Content Security Policy
|
|
# Note: 'unsafe-inline'/'unsafe-eval' are kept because many SPAs need them.
|
|
# Once your build is verified to work without inline scripts, remove them.
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' ws: wss:; frame-ancestors 'none'; base-uri 'self'; form-action 'self' https://github.com;" always;
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# ServerKit frontend — served directly by host nginx from the built SPA
|
|
# bundle (no container). The path tracks the /opt/serverkit symlink, so a
|
|
# blue/green update swaps the served assets atomically. The installer rewrites
|
|
# this root when SERVERKIT_DIR is customised.
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
root /opt/serverkit/frontend/dist;
|
|
index index.html;
|
|
|
|
# Long-cache the content-hashed build assets; never cache the HTML entrypoint.
|
|
location /assets/ {
|
|
try_files $uri =404;
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable";
|
|
}
|
|
|
|
# Runtime-extension vendor shims (/serverkit-vendor/*.mjs). The import map in
|
|
# index.html resolves react / react-dom / react-router-dom / i18next /
|
|
# serverkit-sdk to these, and the browser refuses to run an ES module served
|
|
# with a non-JavaScript MIME type. Stock nginx (1.18-1.24, Debian/Ubuntu
|
|
# mime.types) has no `.mjs` entry and falls back to application/octet-stream,
|
|
# which surfaces as "Failed to fetch dynamically imported module: blob:..."
|
|
# (Chrome) or "'application/octet-stream' is not a valid JavaScript MIME
|
|
# type" (Safari) on EVERY runtime-loaded extension. Force the type here
|
|
# instead of editing the system mime.types. Not hashed, so revalidate.
|
|
# Anchored to the shim folder: a bare `\.mjs$` regex would also capture
|
|
# /api/v1/plugins/<slug>/assets/dist/index.mjs (regex locations beat the
|
|
# /api prefix block) and 404 every extension bundle instead.
|
|
location ~* ^/serverkit-vendor/.+\.mjs$ {
|
|
types { }
|
|
default_type text/javascript;
|
|
try_files $uri =404;
|
|
expires -1;
|
|
}
|
|
|
|
location / {
|
|
# expires -1 (not add_header): emits Cache-Control: no-cache without
|
|
# discarding the server-level security headers, and forces every load
|
|
# to revalidate the entrypoint — a cached index.html references hashed
|
|
# assets that no longer exist after the next blue/green switch.
|
|
expires -1;
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Proxy to ServerKit backend API
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
location /api {
|
|
proxy_pass http://127.0.0.1:5000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-Host $host;
|
|
proxy_connect_timeout 60s;
|
|
proxy_send_timeout 60s;
|
|
proxy_read_timeout 60s;
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# WebSocket support for Socket.IO
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
location /socket.io {
|
|
proxy_pass http://127.0.0.1:5000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-Host $host;
|
|
proxy_connect_timeout 7d;
|
|
proxy_send_timeout 7d;
|
|
proxy_read_timeout 7d;
|
|
}
|
|
|
|
# Include app location blocks (WordPress, Gitea, private URLs, etc.)
|
|
# These are location-only snippets, NOT separate server blocks.
|
|
include /etc/nginx/serverkit-locations/*.conf;
|
|
|
|
# Deny access to hidden/sensitive files
|
|
location ~ /\. {
|
|
deny all;
|
|
}
|
|
|
|
location ~* \.(env|git|gitignore|dockerignore)$ {
|
|
deny all;
|
|
}
|
|
}
|
|
|
|
# Optional canonical-domain redirect block. Generated by install.sh when a
|
|
# canonical domain is supplied; can also be created manually in
|
|
# /etc/nginx/serverkit-conf.d/*.conf.
|
|
include /etc/nginx/serverkit-conf.d/*.conf;
|