template tutoring-admin-dashboard v1
This commit is contained in:
+159
@@ -0,0 +1,159 @@
|
||||
# One origin, two halves: static frontend at /, the API proxied behind it.
|
||||
#
|
||||
# This is what makes a generated app a single container on a single port. The
|
||||
# browser sees one origin, so there is no CORS to configure, no second hostname
|
||||
# to route, and no preflight on every request. The backend never learns it is
|
||||
# behind anything.
|
||||
#
|
||||
# This is a COMPLETE nginx.conf, not a conf.d snippet, because the websocket
|
||||
# `Connection` header needs a `map` and a map is only legal at http level. A
|
||||
# drop-in under conf.d cannot declare one, and the usual workaround — passing
|
||||
# the client's Connection header straight through — is wrong for a proxy that
|
||||
# also keeps upstream connections alive.
|
||||
|
||||
worker_processes auto;
|
||||
error_log /dev/stderr warn;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logs go to the container's streams. A generated app that logs to a file
|
||||
# inside its own filesystem is logging into a volume nobody reads and
|
||||
# filling a disk nobody is watching.
|
||||
access_log /dev/stdout;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
server_tokens off;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css application/json application/javascript
|
||||
text/xml application/xml image/svg+xml;
|
||||
|
||||
# "upgrade" when the client is opening a websocket, "close" otherwise.
|
||||
# Forwarding the client's own Connection header instead would tear down the
|
||||
# keepalive to the upstream on every ordinary request.
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
upstream app_backend {
|
||||
# The application server, bound to loopback inside this container. It is
|
||||
# NOT published: the only way in is through this nginx, which is what
|
||||
# makes "the exposed port is 8000" true rather than conventional.
|
||||
server 127.0.0.1:8001;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8000;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# An upload limit belongs here rather than in the app: nginx refuses an
|
||||
# oversized body before it is buffered, where the app would read it
|
||||
# first and then complain.
|
||||
client_max_body_size 25m;
|
||||
|
||||
# --- the API -----------------------------------------------------
|
||||
# Longest-prefix wins in nginx, so this is matched before the SPA
|
||||
# fallback regardless of the order the blocks appear in.
|
||||
location /api {
|
||||
# Whether the prefix is STRIPPED before the backend sees it depends
|
||||
# on the framework, so the recipe supplies the rewrite (or none):
|
||||
#
|
||||
# Django keeps /api/... and strips it itself via
|
||||
# FORCE_SCRIPT_NAME, which is also what makes its
|
||||
# redirects and admin URLs carry the prefix.
|
||||
# FastAPI needs it stripped. uvicorn's --root-path does NOT
|
||||
# strip; it only affects URL generation.
|
||||
#
|
||||
# An explicit `rewrite`, not `proxy_pass http://app_backend/`. The
|
||||
# trailing-slash form looks equivalent and is not: with a prefix
|
||||
# location that has no trailing slash, nginx replaces the matched
|
||||
# "/api" with "/" and the backend receives "//ping" — a double
|
||||
# slash that matches no route, so every endpoint 404s while nginx,
|
||||
# the frontend and the healthcheck all report perfectly fine.
|
||||
|
||||
proxy_pass http://app_backend;
|
||||
proxy_http_version 1.1;
|
||||
# FastAPI recipes: a 404 with the prefix kept is retried with it
|
||||
# stripped (@api_stripped below), so an app that mounts at the root
|
||||
# and one that mounts under /api both answer. Django: empty.
|
||||
proxy_intercept_errors on; error_page 404 = @api_stripped;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# Without this a Django or FastAPI app behind TLS builds http://
|
||||
# URLs in redirects and password-reset links, and the browser
|
||||
# refuses them as mixed content — a failure that only shows up once
|
||||
# there is TLS in front, which is to say in production.
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
# Upgrade is hop-by-hop, so it has to be forwarded explicitly or a
|
||||
# websocket handshake degrades to a plain request that the app
|
||||
# answers 200 with no socket, and the client waits forever.
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
proxy_read_timeout 120s;
|
||||
proxy_send_timeout 120s;
|
||||
# Streaming responses — server-sent events, a slow export — must
|
||||
# not be held in a buffer until they finish, which is exactly what
|
||||
# makes them look like a hang.
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# --- the frontend -------------------------------------------------
|
||||
|
||||
# The retry target for proxy_intercept_errors on; error_page 404 = @api_stripped;: the same upstream, prefix
|
||||
# stripped. Internal only — a client cannot request it by name.
|
||||
location @api_stripped {
|
||||
rewrite ^/api/?(.*)$ /$1 break;
|
||||
proxy_pass http://app_backend;
|
||||
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;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# index.html must NOT be cached: it names the hashed asset files, so a
|
||||
# cached copy keeps pointing at the previous deploy's bundles.
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# nginx's own liveness, answered without waking the app. The container
|
||||
# healthcheck uses this, so a backend that is slow to start reads as
|
||||
# starting rather than as a dead container.
|
||||
location = /_nginx_health {
|
||||
access_log off;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user