Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
letsencrypt/
|
||||||
|
.idea/
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Proxy Reminder: Race Management Upstream Resilience
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`mnmsoft_proxy` currently fails hard at nginx startup if the upstream hostname
|
||||||
|
`race-management-web` is not resolvable at config-parse time.
|
||||||
|
|
||||||
|
Observed failure:
|
||||||
|
|
||||||
|
```text
|
||||||
|
host not found in upstream "race-management-web" in /etc/nginx/conf.d/default.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
This caused the proxy container to restart in a loop whenever the
|
||||||
|
`race-management-web` container was down or not yet attached to the shared
|
||||||
|
Docker network.
|
||||||
|
|
||||||
|
## What Was Already Fixed
|
||||||
|
|
||||||
|
In `/opt/docker/apps/race-management/docker-compose.yml`:
|
||||||
|
|
||||||
|
- `db` now uses `restart: unless-stopped`
|
||||||
|
- `web` now uses `restart: unless-stopped`
|
||||||
|
|
||||||
|
That reduces the chance of the backend staying down, but the proxy should still
|
||||||
|
be made tolerant of temporary upstream absence.
|
||||||
|
|
||||||
|
## What Needs To Be Done In Proxy
|
||||||
|
|
||||||
|
Relevant proxy files:
|
||||||
|
|
||||||
|
- `/opt/docker/apps/proxy/generated/default.conf`
|
||||||
|
- possibly `/opt/docker/apps/proxy/nginx/nginx.conf`
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Avoid nginx resolving `race-management-web` only once at startup.
|
||||||
|
Instead, make nginx resolve through Docker DNS at request time or with a
|
||||||
|
short-lived resolver cache.
|
||||||
|
|
||||||
|
### Recommended Approach
|
||||||
|
|
||||||
|
Use Docker's internal DNS resolver and proxy through a variable.
|
||||||
|
|
||||||
|
Typical pattern:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
resolver 127.0.0.11 valid=10s;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
set $race_management_upstream http://race-management-web:8000;
|
||||||
|
proxy_pass $race_management_upstream;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
With a static upstream like:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
proxy_pass http://race-management-web:8000;
|
||||||
|
```
|
||||||
|
|
||||||
|
nginx may try to resolve the hostname during startup/config load. If the
|
||||||
|
container is absent at that exact moment, nginx exits and the proxy restarts.
|
||||||
|
|
||||||
|
Using `resolver 127.0.0.11` plus a variable makes nginx rely on Docker DNS more
|
||||||
|
safely at runtime.
|
||||||
|
|
||||||
|
## Also Clean Up
|
||||||
|
|
||||||
|
The proxy logs also show this warning:
|
||||||
|
|
||||||
|
```text
|
||||||
|
the "listen ... http2" directive is deprecated
|
||||||
|
```
|
||||||
|
|
||||||
|
If present, replace patterns like:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
listen 443 ssl http2;
|
||||||
|
```
|
||||||
|
|
||||||
|
with the newer form appropriate for the installed nginx version.
|
||||||
|
|
||||||
|
## After Editing
|
||||||
|
|
||||||
|
Reload/recreate the proxy stack and verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/docker/apps/proxy
|
||||||
|
docker compose up -d
|
||||||
|
docker logs --tail 100 mnmsoft_proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result:
|
||||||
|
|
||||||
|
- proxy stays up even if `race-management-web` is briefly unavailable during startup
|
||||||
|
- no more `host not found in upstream "race-management-web"` fatal error
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# HTTPS Reverse Proxy
|
||||||
|
|
||||||
|
This directory hosts the single public HTTPS entry point for apps under `/opt/docker/apps`.
|
||||||
|
|
||||||
|
## Route config
|
||||||
|
|
||||||
|
Edit [config/proxy.yaml](/opt/docker/apps/proxy/config/proxy.yaml) and add routes like:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server_name: mnmsoft.hopto.org
|
||||||
|
certificate_name: mnmsoft.hopto.org
|
||||||
|
|
||||||
|
routes:
|
||||||
|
- project_dir: /opt/docker/apps/turnir2024
|
||||||
|
path_suffix: odzaci-open-2026
|
||||||
|
port: 32834
|
||||||
|
upstream_scheme: https
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
- `project_dir`: informational only, kept in the generated config and landing page.
|
||||||
|
- `path_suffix`: public path after the domain. `odzaci-open-2026` becomes `https://mnmsoft.hopto.org/odzaci-open-2026/`.
|
||||||
|
- `port`: port exposed on the Docker host by the target app.
|
||||||
|
- `upstream_scheme`: optional, defaults to `http`. Use `https` if the backend app still terminates TLS itself.
|
||||||
|
- `upstream_host`: optional, defaults to `host.docker.internal`.
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/render.sh
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
After changing routes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/render.sh
|
||||||
|
docker compose restart proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
## TLS certificates
|
||||||
|
|
||||||
|
Issue a Let's Encrypt certificate after the proxy is reachable on public port `80`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/certbot-init.sh mnmsoft.hopto.org you@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Renew manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/certbot-renew.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical cron entry:
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 3 * * * cd /opt/docker/apps/proxy && ./scripts/certbot-renew.sh >/var/log/proxy-certbot-renew.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
The proxy serves certificates from a stable path:
|
||||||
|
|
||||||
|
```text
|
||||||
|
./letsencrypt/live/current/fullchain.pem
|
||||||
|
./letsencrypt/live/current/privkey.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
Certbot may create versioned lineages such as `mnmsoft.hopto.org-0001`; the helper scripts automatically repoint `live/current/` to the latest real lineage after issuance or renewal.
|
||||||
|
|
||||||
|
## Important behavior
|
||||||
|
|
||||||
|
This proxy strips the configured prefix before forwarding. Example:
|
||||||
|
|
||||||
|
- public request: `/odzaci-open-2026/api/matches`
|
||||||
|
- upstream request: `/api/matches`
|
||||||
|
|
||||||
|
It also sends `X-Forwarded-Prefix: /odzaci-open-2026`.
|
||||||
|
|
||||||
|
Apps that generate absolute URLs from `/` instead of respecting the forwarded prefix may still need app-level configuration changes.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
server_name: mnmsoft.hopto.org
|
||||||
|
certificate_name: current
|
||||||
|
|
||||||
|
routes:
|
||||||
|
# - project_dir: /opt/docker/apps/turnir2024
|
||||||
|
# path_suffix: odzaci-open-2026
|
||||||
|
# port: 32834
|
||||||
|
# upstream_scheme: http
|
||||||
|
- project_dir: /opt/docker/apps/biogena-pdf-generator
|
||||||
|
path_suffix: biogena-pdf-generator
|
||||||
|
port: 8092
|
||||||
|
upstream_scheme: http
|
||||||
|
- project_dir: /opt/docker/apps/pdf-generator-demo
|
||||||
|
path_suffix: pdf-generator-demo
|
||||||
|
port: 8093
|
||||||
|
upstream_scheme: http
|
||||||
|
# - project_dir: /opt/docker/apps/digiped
|
||||||
|
# path_suffix: digiped
|
||||||
|
# port: 32420
|
||||||
|
# upstream_scheme: http
|
||||||
|
# - project_dir: /opt/docker/apps/file-server
|
||||||
|
# path_suffix: file-server
|
||||||
|
# port: 32500
|
||||||
|
# upstream_scheme: http
|
||||||
|
# - project_dir: /opt/docker/apps/invoice-control
|
||||||
|
# path_suffix: invoice-control
|
||||||
|
# port: 32443
|
||||||
|
# upstream_scheme: http
|
||||||
|
# - project_dir: /opt/docker/apps/jkpu-pog
|
||||||
|
# path_suffix: jkpu-pog
|
||||||
|
# port: 32444
|
||||||
|
# upstream_scheme: http
|
||||||
|
- project_dir: /opt/docker/apps/my-git
|
||||||
|
path_suffix: git
|
||||||
|
port: 3000
|
||||||
|
upstream_scheme: http
|
||||||
|
- project_dir: /opt/docker/apps/jellyfin
|
||||||
|
path_suffix: jellyfin
|
||||||
|
port: 8096
|
||||||
|
upstream_scheme: http
|
||||||
|
# - project_dir: /opt/docker/apps/race-management
|
||||||
|
# path_suffix: odzacka-humanitarna-petica
|
||||||
|
# port: 8000
|
||||||
|
# upstream_scheme: http
|
||||||
|
# upstream_host: race-management-web
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
services:
|
||||||
|
proxy:
|
||||||
|
image: nginx:1.29-alpine
|
||||||
|
container_name: mnmsoft_proxy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/.htpasswd:/etc/nginx/.htpasswd:ro
|
||||||
|
- ./generated/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- ./generated/index.html:/usr/share/nginx/html/index.html:ro
|
||||||
|
- ./nginx/html:/var/www/certbot:rw
|
||||||
|
- ./letsencrypt:/etc/letsencrypt:ro
|
||||||
|
certbot:
|
||||||
|
image: certbot/certbot:latest
|
||||||
|
profiles:
|
||||||
|
- certbot
|
||||||
|
volumes:
|
||||||
|
- ./nginx/html:/var/www/certbot:rw
|
||||||
|
- ./letsencrypt:/etc/letsencrypt:rw
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name mnmsoft.hopto.org;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name mnmsoft.hopto.org;
|
||||||
|
|
||||||
|
client_max_body_size 256M;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/current/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/current/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
add_header X-Frame-Options DENY;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# /opt/docker/apps/biogena-pdf-generator
|
||||||
|
location = /biogena-pdf-generator {
|
||||||
|
return 301 /biogena-pdf-generator/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /biogena-pdf-generator/ {
|
||||||
|
proxy_pass http://host.docker.internal:8092/;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /biogena-pdf-generator;
|
||||||
|
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# /opt/docker/apps/pdf-generator-demo
|
||||||
|
location = /pdf-generator-demo {
|
||||||
|
return 301 /pdf-generator-demo/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /pdf-generator-demo/ {
|
||||||
|
proxy_pass http://host.docker.internal:8093/;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /pdf-generator-demo;
|
||||||
|
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# /opt/docker/apps/my-git
|
||||||
|
location = /git {
|
||||||
|
return 301 /git/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /git/ {
|
||||||
|
proxy_pass http://host.docker.internal:3000/;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /git;
|
||||||
|
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# /opt/docker/apps/jellyfin
|
||||||
|
location = /jellyfin {
|
||||||
|
return 301 /jellyfin/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /jellyfin/ {
|
||||||
|
proxy_pass http://host.docker.internal:8096;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /jellyfin;
|
||||||
|
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
auth_basic "Restricted";
|
||||||
|
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>mnmsoft.hopto.org proxy</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
background: #f4f1ea;
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(192, 132, 84, 0.18), transparent 35%),
|
||||||
|
linear-gradient(135deg, #f7f3ec, #efe5d5);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
width: min(760px, calc(100vw - 2rem));
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1px solid rgba(31, 41, 51, 0.08);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 24px 60px rgba(31, 41, 51, 0.12);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: clamp(2rem, 5vw, 3rem);
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin: 0.8rem 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #8f3b1b;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
color: #52606d;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: #f3ede3;
|
||||||
|
padding: 0.15rem 0.35rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>mnmsoft.hopto.org</h1>
|
||||||
|
<p>Configured reverse-proxy routes:</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/biogena-pdf-generator/">/biogena-pdf-generator/</a> <span>/opt/docker/apps/biogena-pdf-generator</span></li>
|
||||||
|
<li><a href="/pdf-generator-demo/">/pdf-generator-demo/</a> <span>/opt/docker/apps/pdf-generator-demo</span></li>
|
||||||
|
<li><a href="/git/">/git/</a> <span>/opt/docker/apps/my-git</span></li>
|
||||||
|
<li><a href="/jellyfin/">/jellyfin/</a> <span>/opt/docker/apps/jellyfin</span></li>
|
||||||
|
</ul>
|
||||||
|
<p>Edit <code>config/proxy.yaml</code>, regenerate, then restart the proxy container.</p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
miroslav.vlajnic:$apr1$AXbpQ.v4$S1e9cDhJmjhG9e/8WNWh00
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
Binary file not shown.
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Usage: $0 <domain> <email>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
DOMAIN="$1"
|
||||||
|
EMAIL="$2"
|
||||||
|
LE_LIVE_DIR="${PROJECT_DIR}/letsencrypt/live"
|
||||||
|
BOOTSTRAP_DIR="${LE_LIVE_DIR}/current"
|
||||||
|
LE_RENEWAL_DIR="${PROJECT_DIR}/letsencrypt/renewal"
|
||||||
|
LE_RENEWAL_CONF="${LE_RENEWAL_DIR}/${DOMAIN}.conf"
|
||||||
|
|
||||||
|
cd "${PROJECT_DIR}"
|
||||||
|
|
||||||
|
mkdir -p "${BOOTSTRAP_DIR}" "${LE_RENEWAL_DIR}"
|
||||||
|
|
||||||
|
if [[ ! -f "${BOOTSTRAP_DIR}/fullchain.pem" || ! -f "${BOOTSTRAP_DIR}/privkey.pem" ]]; then
|
||||||
|
openssl req -x509 -nodes -newkey rsa:2048 -days 1 \
|
||||||
|
-keyout "${BOOTSTRAP_DIR}/privkey.pem" \
|
||||||
|
-out "${BOOTSTRAP_DIR}/fullchain.pem" \
|
||||||
|
-subj "/CN=${DOMAIN}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose up -d proxy
|
||||||
|
|
||||||
|
docker compose --profile certbot run --rm certbot certonly \
|
||||||
|
--webroot \
|
||||||
|
--webroot-path /var/www/certbot \
|
||||||
|
--domain "${DOMAIN}" \
|
||||||
|
--email "${EMAIL}" \
|
||||||
|
--agree-tos \
|
||||||
|
--no-eff-email
|
||||||
|
|
||||||
|
rm -f "${BOOTSTRAP_DIR}/fullchain.pem" "${BOOTSTRAP_DIR}/privkey.pem"
|
||||||
|
"${PROJECT_DIR}/scripts/update-active-cert-links.sh" "${DOMAIN}"
|
||||||
|
docker compose exec proxy nginx -s reload
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
DOMAIN="${1:-mnmsoft.hopto.org}"
|
||||||
|
|
||||||
|
cd "${PROJECT_DIR}"
|
||||||
|
|
||||||
|
docker compose --profile certbot run --rm certbot renew
|
||||||
|
"${PROJECT_DIR}/scripts/update-active-cert-links.sh" "${DOMAIN}"
|
||||||
|
docker compose exec proxy nginx -s reload
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
python3 "${PROJECT_DIR}/scripts/render_config.py"
|
||||||
@@ -0,0 +1,497 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import html
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
CONFIG_PATH = ROOT / "config" / "proxy.yaml"
|
||||||
|
GENERATED_DIR = ROOT / "generated"
|
||||||
|
NGINX_OUTPUT_PATH = GENERATED_DIR / "default.conf"
|
||||||
|
INDEX_OUTPUT_PATH = GENERATED_DIR / "index.html"
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_scalar(raw: str):
|
||||||
|
value = raw.strip()
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
if value[0] == value[-1] and value[0] in {"'", '"'}:
|
||||||
|
return value[1:-1]
|
||||||
|
if re.fullmatch(r"-?\d+", value):
|
||||||
|
return int(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def parse_config(path: Path) -> dict:
|
||||||
|
config: dict[str, object] = {}
|
||||||
|
routes: list[dict[str, object]] = []
|
||||||
|
current_route: dict[str, object] | None = None
|
||||||
|
in_routes = False
|
||||||
|
|
||||||
|
for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||||
|
line = raw_line.split("#", 1)[0].rstrip()
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
indent = len(line) - len(line.lstrip(" "))
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
if indent == 0:
|
||||||
|
current_route = None
|
||||||
|
if stripped == "routes:":
|
||||||
|
in_routes = True
|
||||||
|
continue
|
||||||
|
in_routes = False
|
||||||
|
if ":" not in stripped:
|
||||||
|
raise ConfigError(f"{path}:{line_number}: expected key: value pair")
|
||||||
|
key, value = stripped.split(":", 1)
|
||||||
|
config[key.strip()] = parse_scalar(value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_routes:
|
||||||
|
raise ConfigError(f"{path}:{line_number}: unexpected indentation outside routes")
|
||||||
|
|
||||||
|
if indent == 2 and stripped.startswith("- "):
|
||||||
|
payload = stripped[2:].strip()
|
||||||
|
current_route = {}
|
||||||
|
routes.append(current_route)
|
||||||
|
if payload:
|
||||||
|
if ":" not in payload:
|
||||||
|
raise ConfigError(f"{path}:{line_number}: expected key: value after '-'")
|
||||||
|
key, value = payload.split(":", 1)
|
||||||
|
current_route[key.strip()] = parse_scalar(value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if indent == 4 and current_route is not None:
|
||||||
|
if ":" not in stripped:
|
||||||
|
raise ConfigError(f"{path}:{line_number}: expected key: value in route entry")
|
||||||
|
key, value = stripped.split(":", 1)
|
||||||
|
current_route[key.strip()] = parse_scalar(value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise ConfigError(f"{path}:{line_number}: unsupported YAML structure")
|
||||||
|
|
||||||
|
config["routes"] = routes
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config(config: dict) -> dict:
|
||||||
|
server_name = config.get("server_name")
|
||||||
|
certificate_name = config.get("certificate_name")
|
||||||
|
routes = config.get("routes")
|
||||||
|
|
||||||
|
if not isinstance(server_name, str) or not server_name:
|
||||||
|
raise ConfigError("server_name must be a non-empty string")
|
||||||
|
if not isinstance(certificate_name, str) or not certificate_name:
|
||||||
|
raise ConfigError("certificate_name must be a non-empty string")
|
||||||
|
if not isinstance(routes, list) or not routes:
|
||||||
|
raise ConfigError("routes must contain at least one route")
|
||||||
|
|
||||||
|
normalized_routes = []
|
||||||
|
seen_suffixes = set()
|
||||||
|
for index, route in enumerate(routes, start=1):
|
||||||
|
if not isinstance(route, dict):
|
||||||
|
raise ConfigError(f"route #{index} must be a mapping")
|
||||||
|
|
||||||
|
project_dir = route.get("project_dir")
|
||||||
|
path_suffix = route.get("path_suffix")
|
||||||
|
port = route.get("port")
|
||||||
|
upstream_scheme = route.get("upstream_scheme", "http")
|
||||||
|
upstream_host = route.get("upstream_host", "host.docker.internal")
|
||||||
|
|
||||||
|
if not isinstance(project_dir, str) or not project_dir:
|
||||||
|
raise ConfigError(f"route #{index}: project_dir must be a non-empty string")
|
||||||
|
if not isinstance(path_suffix, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]*", path_suffix):
|
||||||
|
raise ConfigError(
|
||||||
|
f"route #{index}: path_suffix must match [A-Za-z0-9][A-Za-z0-9._/-]*"
|
||||||
|
)
|
||||||
|
if path_suffix.startswith("/") or path_suffix.endswith("/"):
|
||||||
|
raise ConfigError(f"route #{index}: path_suffix must not start or end with '/'")
|
||||||
|
if path_suffix in seen_suffixes:
|
||||||
|
raise ConfigError(f"route #{index}: duplicate path_suffix '{path_suffix}'")
|
||||||
|
seen_suffixes.add(path_suffix)
|
||||||
|
if not isinstance(port, int) or not (1 <= port <= 65535):
|
||||||
|
raise ConfigError(f"route #{index}: port must be an integer between 1 and 65535")
|
||||||
|
if upstream_scheme not in {"http", "https"}:
|
||||||
|
raise ConfigError(f"route #{index}: upstream_scheme must be http or https")
|
||||||
|
if not isinstance(upstream_host, str) or not upstream_host:
|
||||||
|
raise ConfigError(f"route #{index}: upstream_host must be a non-empty string")
|
||||||
|
|
||||||
|
normalized_routes.append(
|
||||||
|
{
|
||||||
|
"project_dir": project_dir,
|
||||||
|
"path_suffix": path_suffix,
|
||||||
|
"port": port,
|
||||||
|
"upstream_scheme": upstream_scheme,
|
||||||
|
"upstream_host": upstream_host,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"server_name": server_name,
|
||||||
|
"certificate_name": certificate_name,
|
||||||
|
"routes": normalized_routes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_docker_host(upstream_host: str) -> bool:
|
||||||
|
return upstream_host != "host.docker.internal"
|
||||||
|
|
||||||
|
|
||||||
|
def _upstream_var(suffix: str) -> str:
|
||||||
|
return f"upstream_{suffix.replace('-', '_')}"
|
||||||
|
|
||||||
|
|
||||||
|
def _proxy_pass_directive(upstream_host: str, port: int, upstream_scheme: str, suffix: str, upstream_path: str = "/") -> str:
|
||||||
|
target = f"{upstream_scheme}://{upstream_host}:{port}{upstream_path}"
|
||||||
|
if _is_docker_host(upstream_host):
|
||||||
|
var = _upstream_var(suffix)
|
||||||
|
return f" set ${var} {target};\n proxy_pass ${var};"
|
||||||
|
return f" proxy_pass {target};"
|
||||||
|
|
||||||
|
|
||||||
|
def _render_static_location(host: str, port: int, suffix: str) -> str:
|
||||||
|
target = f"http://{host}:{port}/static/"
|
||||||
|
if _is_docker_host(host):
|
||||||
|
var = f"$upstream_{suffix.replace('-', '_')}_static"
|
||||||
|
return f" set {var} {target};\n proxy_pass {var};"
|
||||||
|
return f" proxy_pass {target};"
|
||||||
|
|
||||||
|
|
||||||
|
def render_location(route: dict[str, object]) -> str:
|
||||||
|
suffix = str(route["path_suffix"])
|
||||||
|
upstream_scheme = str(route["upstream_scheme"])
|
||||||
|
upstream_host = str(route["upstream_host"])
|
||||||
|
port = int(route["port"])
|
||||||
|
project_dir = str(route["project_dir"])
|
||||||
|
upstream_path = "/"
|
||||||
|
if suffix == "jellyfin":
|
||||||
|
upstream_path = ""
|
||||||
|
ssl_directives = ""
|
||||||
|
if upstream_scheme == "https":
|
||||||
|
ssl_directives = "\n proxy_ssl_server_name on;\n proxy_ssl_verify off;"
|
||||||
|
proxy_pass_block = _proxy_pass_directive(upstream_host, port, upstream_scheme, suffix, upstream_path)
|
||||||
|
|
||||||
|
return f""" # {project_dir}
|
||||||
|
location = /{suffix} {{
|
||||||
|
return 301 /{suffix}/;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location /{suffix}/ {{
|
||||||
|
{proxy_pass_block}
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /{suffix};
|
||||||
|
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;{ssl_directives}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_nginx(config: dict) -> str:
|
||||||
|
server_name = config["server_name"]
|
||||||
|
certificate_name = config["certificate_name"]
|
||||||
|
routes: list[dict] = config["routes"]
|
||||||
|
|
||||||
|
route_blocks = "\n".join(render_location(route) for route in routes)
|
||||||
|
|
||||||
|
has_docker_upstream = any(_is_docker_host(str(route["upstream_host"])) for route in routes)
|
||||||
|
resolver_block = ""
|
||||||
|
if has_docker_upstream:
|
||||||
|
resolver_block = "\n resolver 127.0.0.11 valid=10s ipv6=off;\n"
|
||||||
|
|
||||||
|
extra_locations = ""
|
||||||
|
race_management_route = next((route for route in routes if route["project_dir"] == "/opt/docker/apps/race-management"), None)
|
||||||
|
if race_management_route is not None:
|
||||||
|
race_management_host = str(race_management_route["upstream_host"])
|
||||||
|
race_management_port = int(race_management_route["port"])
|
||||||
|
race_management_suffix = str(race_management_route["path_suffix"])
|
||||||
|
static_proxy_line = _render_static_location(race_management_host, race_management_port, race_management_suffix)
|
||||||
|
extra_locations += f"""
|
||||||
|
location /{race_management_suffix}/static/ {{
|
||||||
|
{static_proxy_line}
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
digiped_route = next((route for route in routes if route["path_suffix"] == "digiped"), None)
|
||||||
|
if digiped_route is not None:
|
||||||
|
digiped_host = str(digiped_route["upstream_host"])
|
||||||
|
digiped_port = int(digiped_route["port"])
|
||||||
|
extra_locations += f"""
|
||||||
|
# digiped uses Angular's Vite dev server, which still emits a few root-level requests.
|
||||||
|
location /@vite/ {{
|
||||||
|
proxy_pass http://{digiped_host}:{digiped_port}/@vite/;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location /@fs/ {{
|
||||||
|
proxy_pass http://{digiped_host}:{digiped_port}/@fs/;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location ~ ^/(main|polyfills|styles)\\.js$ {{
|
||||||
|
proxy_pass http://{digiped_host}:{digiped_port}$request_uri;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location = /styles.css {{
|
||||||
|
proxy_pass http://{digiped_host}:{digiped_port}/styles.css;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location ~ ^/chunk-[A-Z0-9]+\\.js$ {{
|
||||||
|
proxy_pass http://{digiped_host}:{digiped_port}$request_uri;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location = /favicon.ico {{
|
||||||
|
proxy_pass http://{digiped_host}:{digiped_port}/favicon.ico;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location = /digiped.png {{
|
||||||
|
proxy_pass http://{digiped_host}:{digiped_port}/digiped.png;
|
||||||
|
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_set_header X-Forwarded-Port 443;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_buffering off;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
return f"""map $http_upgrade $connection_upgrade {{
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}}
|
||||||
|
|
||||||
|
server {{
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name {server_name};
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {{
|
||||||
|
root /var/www/certbot;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
server {{
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name {server_name};
|
||||||
|
|
||||||
|
client_max_body_size 256M;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/{certificate_name}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/{certificate_name}/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;{resolver_block}
|
||||||
|
add_header X-Frame-Options DENY;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {{
|
||||||
|
root /var/www/certbot;
|
||||||
|
}}
|
||||||
|
|
||||||
|
{extra_locations}
|
||||||
|
{route_blocks}
|
||||||
|
location = / {{
|
||||||
|
auth_basic "Restricted";
|
||||||
|
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files /index.html =404;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
return 404;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_index(config: dict) -> str:
|
||||||
|
server_name = html.escape(str(config["server_name"]))
|
||||||
|
items = "\n".join(
|
||||||
|
(
|
||||||
|
f' <li><a href="/{html.escape(str(route["path_suffix"]))}/">'
|
||||||
|
f'/{html.escape(str(route["path_suffix"]))}/</a>'
|
||||||
|
f' <span>{html.escape(str(route["project_dir"]))}</span></li>'
|
||||||
|
)
|
||||||
|
for route in config["routes"]
|
||||||
|
)
|
||||||
|
return f"""<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{server_name} proxy</title>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
color-scheme: light;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
background: #f4f1ea;
|
||||||
|
color: #1f2933;
|
||||||
|
}}
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(192, 132, 84, 0.18), transparent 35%),
|
||||||
|
linear-gradient(135deg, #f7f3ec, #efe5d5);
|
||||||
|
}}
|
||||||
|
main {{
|
||||||
|
width: min(760px, calc(100vw - 2rem));
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1px solid rgba(31, 41, 51, 0.08);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 24px 60px rgba(31, 41, 51, 0.12);
|
||||||
|
}}
|
||||||
|
h1 {{
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: clamp(2rem, 5vw, 3rem);
|
||||||
|
}}
|
||||||
|
ul {{
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}}
|
||||||
|
li {{
|
||||||
|
margin: 0.8rem 0;
|
||||||
|
}}
|
||||||
|
a {{
|
||||||
|
color: #8f3b1b;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}}
|
||||||
|
span {{
|
||||||
|
color: #52606d;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}}
|
||||||
|
code {{
|
||||||
|
background: #f3ede3;
|
||||||
|
padding: 0.15rem 0.35rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>{server_name}</h1>
|
||||||
|
<p>Configured reverse-proxy routes:</p>
|
||||||
|
<ul>
|
||||||
|
{items}
|
||||||
|
</ul>
|
||||||
|
<p>Edit <code>config/proxy.yaml</code>, regenerate, then restart the proxy container.</p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
config = validate_config(parse_config(CONFIG_PATH))
|
||||||
|
except (OSError, ConfigError) as exc:
|
||||||
|
print(f"error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
NGINX_OUTPUT_PATH.write_text(render_nginx(config), encoding="utf-8")
|
||||||
|
INDEX_OUTPUT_PATH.write_text(render_index(config), encoding="utf-8")
|
||||||
|
print(f"Rendered {NGINX_OUTPUT_PATH}")
|
||||||
|
print(f"Rendered {INDEX_OUTPUT_PATH}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "Usage: $0 <domain>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
DOMAIN="$1"
|
||||||
|
LIVE_DIR="${PROJECT_DIR}/letsencrypt/live"
|
||||||
|
ACTIVE_DIR="${LIVE_DIR}/current"
|
||||||
|
|
||||||
|
latest_lineage="$(find "${LIVE_DIR}" -maxdepth 1 -mindepth 1 -type d -name "${DOMAIN}*" | sort | tail -n 1)"
|
||||||
|
|
||||||
|
if [[ -z "${latest_lineage}" ]]; then
|
||||||
|
echo "No certificate lineage found for ${DOMAIN}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${ACTIVE_DIR}"
|
||||||
|
latest_lineage_name="$(basename "${latest_lineage}")"
|
||||||
|
ln -sfn "../${latest_lineage_name}/fullchain.pem" "${ACTIVE_DIR}/fullchain.pem"
|
||||||
|
ln -sfn "../${latest_lineage_name}/privkey.pem" "${ACTIVE_DIR}/privkey.pem"
|
||||||
|
|
||||||
|
printf 'Active certificate path updated to %s\n' "${latest_lineage}"
|
||||||
Reference in New Issue
Block a user