commit 5ff20429d6d5fbd533372d9389025aa7d9d26383 Author: KYUNGMO TAK Date: Sat Aug 15 19:50:54 2026 +0900 First commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f578e77 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Documentation only (network_mode: host reads the listen port from nginx.conf). +ERROR_PAGE_BIND=127.0.0.1 +ERROR_PAGE_PORT=4098 diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb6088b --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# error-page + +`*.takits.me`가 컨테이너 다운으로 502/503/504가 날 때, DSM 기본 에러창 대신 쓰는 공통 nginx. + +DSM 리버스 프록시가 **앱 포트에 직접** 붙으면, 그 포트가 닫혀 있을 때 나스가 에러 페이지를 그립니다. 이 컨테이너는 항상 `127.0.0.1:4098`에서 떠 있고, `Host`로 실제 앱에 넘긴 뒤 업스트림이 죽었을 때만 공통 페이지를 보여 줍니다. + +``` +브라우저 → DSM RP (HTTPS) → 127.0.0.1:4098 (error-page) + └─ Host가 takits.me 이면 127.0.0.1:4889 +``` + +앱의 404/500은 가로채지 않습니다. + +## 배포 + +NAS 예: `/volume1/09_container_manager/@error-page` + +```bash +docker compose up -d +``` + +확인: + +```bash +curl -sI http://127.0.0.1:4098/healthz +curl -sI -H "Host: unknown.takits.me" http://127.0.0.1:4098/ +``` + +두 번째가 `502`와 커스텀 HTML이면 정상입니다. + +## DSM 리버스 프록시 + +1. `upstreams.map`에 호스트 → **지금 DSM에 적어 둔 목적지 포트**를 넣습니다. +2. 해당 호스트의 리버스 프록시 목적지를 `127.0.0.1:4098`로 바꿉니다. (앱 포트로 두면 나스 에러창이 그대로입니다.) +3. WebSocket이 필요한 호스트는 DSM에서 WS를 그대로 켭니다. +4. SSL은 지금처럼 DSM이 종료합니다. + +한 줄 추가 후: + +```bash +docker exec error-page nginx -s reload +``` + +와일드카드 `*.takits.me` → `127.0.0.1:4098` 한 규칙으로 모아도 됩니다. 맵에 없는 호스트는 공통 에러 페이지가 뜹니다. + +## 파일 + +| 파일 | 역할 | +|------|------| +| `upstreams.map` | 호스트별 앱 포트 | +| `html/index.html` | 502/503/504 페이지 | +| `nginx.conf` | 프록시 + 에러 가로채기 | diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f9bafa3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +name: error-page + +services: + error-page: + image: nginx:alpine + container_name: error-page + restart: unless-stopped + # Reach 127.0.0.1-bound app ports on the NAS (DSM reverse proxy style). + network_mode: host + environment: + TZ: Asia/Seoul + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./upstreams.map:/etc/nginx/upstreams.map:ro + - ./html:/usr/share/nginx/html:ro + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4098/healthz"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..1bcdf4b --- /dev/null +++ b/html/index.html @@ -0,0 +1,115 @@ + + + + + + + __STATUS__ + + + +
+ + +

__STATUS__

+

__ERROR_TITLE__

+

+ We're working to resolve the issue.
+ Please try again shortly. +

+
+ + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..1d44822 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,78 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type text/html; + sendfile on; + server_tokens off; + charset utf-8; + + map $http_upgrade $connection_upgrade { + default upgrade; + "" close; + } + + map $status $error_title { + default "Service Unavailable"; + 502 "Bad Gateway"; + 503 "Service Unavailable"; + 504 "Gateway Timeout"; + } + + include /etc/nginx/upstreams.map; + + server { + listen 127.0.0.1:4098; + server_name _; + + root /usr/share/nginx/html; + absolute_redirect off; + + location = /healthz { + access_log off; + default_type text/plain; + return 200 "ok\n"; + } + + error_page 502 503 504 /__error/index.html; + + location = /__error/index.html { + internal; + sub_filter_once off; + sub_filter_types text/html; + sub_filter "__STATUS__" $status; + sub_filter "__ERROR_TITLE__" $error_title; + sub_filter "__HOST__" $host; + add_header Cache-Control "no-store" always; + add_header X-Content-Type-Options "nosniff" always; + } + + location / { + if ($app_port = 0) { + return 502; + } + + proxy_pass http://127.0.0.1:$app_port; + 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 Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_connect_timeout 3s; + proxy_send_timeout 3600s; + proxy_read_timeout 3600s; + + # Only replace DSM-style "upstream down" pages. App 404/500 stay as-is. + proxy_intercept_errors on; + } + } +} diff --git a/upstreams.map b/upstreams.map new file mode 100644 index 0000000..18750dc --- /dev/null +++ b/upstreams.map @@ -0,0 +1,18 @@ +# Host → published port on the NAS (the destination already used in DSM reverse proxy). +# 0 = unknown host → custom error page. +# +# After adding a line, reload: docker exec error-page nginx -s reload +# Then point that host's DSM reverse proxy to 127.0.0.1:4098 (not the app port). + +map $host $app_port { + default 0; + + takits.me 4889; + www.takits.me 4889; + + fas.takits.me 4885; + ops.fas.takits.me 4887; + + # dev-fas.takits.me 80xx; + # android-fas.takits.me 6080; +}