Upload files to "/"
This commit is contained in:
parent
9d9e5ad44a
commit
33b6b43d0a
44
Dockerfile
Normal file
44
Dockerfile
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
ffmpeg \
|
||||||
|
tini \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN python -m venv /opt/venv
|
||||||
|
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
|
||||||
|
RUN /opt/venv/bin/pip install --upgrade pip setuptools wheel \
|
||||||
|
&& /opt/venv/bin/pip install --no-cache-dir -r /app/requirements.txt
|
||||||
|
|
||||||
|
COPY app.py network.py /app/
|
||||||
|
COPY static /app/static
|
||||||
|
|
||||||
|
RUN groupadd --gid 10002 app \
|
||||||
|
&& useradd --uid 10002 --gid app --create-home \
|
||||||
|
--home-dir /app --shell /usr/sbin/nologin app \
|
||||||
|
&& mkdir -p /app/data /tmp \
|
||||||
|
&& chown -R 10002:10002 /app /opt/venv \
|
||||||
|
&& chmod 1777 /tmp
|
||||||
|
|
||||||
|
USER 10002:10002
|
||||||
|
|
||||||
|
EXPOSE 8003
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD curl --fail --silent --show-error http://127.0.0.1:8003/healthz || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tini","--"]
|
||||||
|
|
||||||
|
CMD ["uvicorn","app:app","--host","0.0.0.0","--port","8003","--workers","1","--loop","uvloop","--http","httptools","--proxy-headers","--forwarded-allow-ips","*","--timeout-keep-alive","30","--timeout-graceful-shutdown","30","--limit-concurrency","256","--backlog","2048"]
|
||||||
163
docker-compose.yml
Normal file
163
docker-compose.yml
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
s1ne:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: s1ne-s-1
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
environment:
|
||||||
|
PYTHONUNBUFFERED: "1"
|
||||||
|
PYTHONDONTWRITEBYTECODE: "1"
|
||||||
|
APP_ENV: ${APP_ENV:-production}
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
||||||
|
SQLITE_CACHE_PATH: ${SQLITE_CACHE_PATH:-/app/data/cache.sqlite3}
|
||||||
|
AUDIO_CACHE_TTL: ${AUDIO_CACHE_TTL:-600}
|
||||||
|
SEARCH_CACHE_TTL: ${SEARCH_CACHE_TTL:-600}
|
||||||
|
SOURCE_METADATA_CACHE_TTL: ${SOURCE_METADATA_CACHE_TTL:-1800}
|
||||||
|
RECOMMENDATION_CACHE_TTL: ${RECOMMENDATION_CACHE_TTL:-900}
|
||||||
|
NEGATIVE_CACHE_TTL: ${NEGATIVE_CACHE_TTL:-90}
|
||||||
|
AUDIO_NEGATIVE_CACHE_TTL: ${AUDIO_NEGATIVE_CACHE_TTL:-90}
|
||||||
|
CACHE_CLEANUP_INTERVAL_SECONDS: ${CACHE_CLEANUP_INTERVAL_SECONDS:-300}
|
||||||
|
CACHE_CLEANUP_BATCH_SIZE: ${CACHE_CLEANUP_BATCH_SIZE:-1000}
|
||||||
|
YTDLP_CONCURRENCY: ${YTDLP_CONCURRENCY:-5}
|
||||||
|
SEARCH_YTDLP_CONCURRENCY: ${SEARCH_YTDLP_CONCURRENCY:-3}
|
||||||
|
AUDIO_YTDLP_CONCURRENCY: ${AUDIO_YTDLP_CONCURRENCY:-2}
|
||||||
|
RECOMMENDATION_YTDLP_CONCURRENCY: ${RECOMMENDATION_YTDLP_CONCURRENCY:-3}
|
||||||
|
YTDLP_PLAYER_CLIENTS: ${YTDLP_PLAYER_CLIENTS:-web,android,mweb,ios}
|
||||||
|
TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS}
|
||||||
|
PROXY_FILE: ${PROXY_FILE:-/app/proxies.txt}
|
||||||
|
PROXY_USER: ${PROXY_USER:-}
|
||||||
|
PROXY_PASS: ${PROXY_PASS:-}
|
||||||
|
PROXY_RETRY_ATTEMPTS: ${PROXY_RETRY_ATTEMPTS:-3}
|
||||||
|
MEDIA_TOKEN_SECRET: ${MEDIA_TOKEN_SECRET}
|
||||||
|
MEDIA_TOKEN_BIND_ANONYMOUS_ID: ${MEDIA_TOKEN_BIND_ANONYMOUS_ID:-true}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- yt_music_data:/app/data
|
||||||
|
- ${PROXY_FILE_HOST:-./proxies.txt}:/app/proxies.txt:ro
|
||||||
|
|
||||||
|
expose:
|
||||||
|
- "8003"
|
||||||
|
|
||||||
|
command:
|
||||||
|
[
|
||||||
|
"uvicorn",
|
||||||
|
"app:app",
|
||||||
|
"--host", "0.0.0.0",
|
||||||
|
"--port", "8003",
|
||||||
|
"--workers", "1",
|
||||||
|
"--loop", "uvloop",
|
||||||
|
"--http", "httptools",
|
||||||
|
"--proxy-headers",
|
||||||
|
"--forwarded-allow-ips", "${TRUSTED_PROXY_CIDRS}",
|
||||||
|
"--timeout-keep-alive", "30",
|
||||||
|
"--timeout-graceful-shutdown", "30",
|
||||||
|
"--limit-concurrency", "256",
|
||||||
|
"--backlog", "2048"
|
||||||
|
]
|
||||||
|
networks:
|
||||||
|
- yt_music_proxy
|
||||||
|
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=256m,mode=1777
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
pids_limit: 256
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"curl",
|
||||||
|
"--fail",
|
||||||
|
"--silent",
|
||||||
|
"--show-error",
|
||||||
|
"http://127.0.0.1:8003/healthz"
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 20s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
# No Caddy labels here.
|
||||||
|
# s1ne is now internal-only; Nginx is its only HTTP caller.
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: s1ne-nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
s1ne:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- yt_music_proxy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Full main config, not /etc/nginx/conf.d/default.conf.
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./static:/usr/share/nginx/html:ro
|
||||||
|
tmpfs:
|
||||||
|
- /var/cache/nginx:size=64m
|
||||||
|
- /var/run:size=8m
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
pids_limit: 128
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"wget -q -O /dev/null http://127.0.0.1/healthz || exit 1"
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
labels:
|
||||||
|
# Caddy routes the public domain to Nginx port 80.
|
||||||
|
caddy: ${APP_DOMAIN}
|
||||||
|
caddy_ingress_network: yt_music_proxy
|
||||||
|
caddy.encode: zstd gzip
|
||||||
|
caddy.request_body.max_size: 256KB
|
||||||
|
caddy.header.-Server: ""
|
||||||
|
caddy.header.X-Content-Type-Options: "nosniff"
|
||||||
|
caddy.header.X-Frame-Options: "DENY"
|
||||||
|
caddy.header.Referrer-Policy: "strict-origin-when-cross-origin"
|
||||||
|
caddy.header.Permissions-Policy: "camera=(), microphone=(), geolocation=()"
|
||||||
|
caddy.header.Strict-Transport-Security: "max-age=31536000"
|
||||||
|
caddy.reverse_proxy: "{{upstreams 80}}"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
yt_music_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
yt_music_proxy:
|
||||||
|
external: true
|
||||||
102
network.py
Normal file
102
network.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
logger = logging.getLogger("minimal-yt.network")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_trusted_proxy_networks(
|
||||||
|
value: str,
|
||||||
|
) -> tuple[ipaddress._BaseNetwork, ...]:
|
||||||
|
networks: list[ipaddress._BaseNetwork] = []
|
||||||
|
|
||||||
|
for raw_value in (value or "").split(","):
|
||||||
|
candidate = raw_value.strip()
|
||||||
|
if not candidate:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
networks.append(ipaddress.ip_network(candidate, strict=False))
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(
|
||||||
|
"Ignoring invalid TRUSTED_PROXY_CIDRS entry: %r",
|
||||||
|
candidate,
|
||||||
|
)
|
||||||
|
|
||||||
|
return tuple(networks)
|
||||||
|
|
||||||
|
|
||||||
|
TRUSTED_PROXY_NETWORKS = _parse_trusted_proxy_networks(
|
||||||
|
os.getenv("TRUSTED_PROXY_CIDRS", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_ip(value: str) -> Optional[ipaddress._BaseAddress]:
|
||||||
|
"""
|
||||||
|
Parse a bare IP, [IPv6], [IPv6]:port, or IPv4:port.
|
||||||
|
Hostnames are intentionally not accepted.
|
||||||
|
"""
|
||||||
|
value = str(value or "").strip()
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if value.startswith("["):
|
||||||
|
closing_bracket = value.find("]")
|
||||||
|
if closing_bracket == -1:
|
||||||
|
return None
|
||||||
|
value = value[1:closing_bracket]
|
||||||
|
elif value.count(":") == 1:
|
||||||
|
host, possible_port = value.rsplit(":", 1)
|
||||||
|
if possible_port.isdigit():
|
||||||
|
value = host
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ipaddress.ip_address(value)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_trusted_proxy(address: ipaddress._BaseAddress) -> bool:
|
||||||
|
return any(
|
||||||
|
address.version == network.version and address in network
|
||||||
|
for network in TRUSTED_PROXY_NETWORKS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_ip(request: Request) -> str:
|
||||||
|
"""
|
||||||
|
Return the effective client IP.
|
||||||
|
|
||||||
|
Forwarded headers are trusted only when the direct TCP peer belongs to
|
||||||
|
TRUSTED_PROXY_CIDRS. X-Forwarded-For is traversed right-to-left while
|
||||||
|
skipping trusted proxy hops.
|
||||||
|
"""
|
||||||
|
peer_value = request.client.host if request.client else ""
|
||||||
|
peer_address = _parse_ip(peer_value)
|
||||||
|
|
||||||
|
if peer_address is None:
|
||||||
|
return peer_value or "unknown"
|
||||||
|
|
||||||
|
if not _is_trusted_proxy(peer_address):
|
||||||
|
return str(peer_address)
|
||||||
|
|
||||||
|
forwarded_for = request.headers.get("x-forwarded-for", "")
|
||||||
|
if forwarded_for:
|
||||||
|
addresses = [
|
||||||
|
address
|
||||||
|
for raw_value in forwarded_for.split(",")
|
||||||
|
if (address := _parse_ip(raw_value)) is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
for address in reversed(addresses):
|
||||||
|
if not _is_trusted_proxy(address):
|
||||||
|
return str(address)
|
||||||
|
|
||||||
|
real_ip = _parse_ip(request.headers.get("x-real-ip", ""))
|
||||||
|
if real_ip is not None and not _is_trusted_proxy(real_ip):
|
||||||
|
return str(real_ip)
|
||||||
|
|
||||||
|
return str(peer_address)
|
||||||
276
nginx.conf
Normal file
276
nginx.conf
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
# ./nginx.conf
|
||||||
|
# Mount as: ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
#
|
||||||
|
# Request path:
|
||||||
|
# Internet -> Caddy (TLS) -> Nginx -> FastAPI service "s1ne:8003"
|
||||||
|
#
|
||||||
|
# Nginx directly serves ./static mounted at /usr/share/nginx/html.
|
||||||
|
# FastAPI handles /api/, /media/, /share/, /healthz, and /readyz.
|
||||||
|
#
|
||||||
|
# A separately mounted protected directory is exposed at /files/.
|
||||||
|
# Example container mount: ./protected:/protected:ro
|
||||||
|
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
|
||||||
|
keepalive_timeout 65;
|
||||||
|
keepalive_requests 1000;
|
||||||
|
|
||||||
|
client_max_body_size 256k;
|
||||||
|
client_header_timeout 15s;
|
||||||
|
client_body_timeout 15s;
|
||||||
|
send_timeout 60s;
|
||||||
|
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# Caddy is the only trusted upstream proxy.
|
||||||
|
# Must match TRUSTED_PROXY_CIDRS used by Uvicorn/FastAPI.
|
||||||
|
set_real_ip_from 172.30.20.0/24;
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
real_ip_recursive on;
|
||||||
|
|
||||||
|
# Limits by real client IP.
|
||||||
|
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=20r/s;
|
||||||
|
limit_req_zone $binary_remote_addr zone=search_limit:10m rate=2r/s;
|
||||||
|
limit_req_zone $binary_remote_addr zone=audio_limit:10m rate=3r/s;
|
||||||
|
limit_conn_zone $binary_remote_addr zone=per_ip_conn:10m;
|
||||||
|
|
||||||
|
upstream fastapi {
|
||||||
|
server s1ne:8003;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
autoindex off;
|
||||||
|
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
|
||||||
|
limit_conn per_ip_conn 20;
|
||||||
|
|
||||||
|
# Block accidental secret/source exposure from the public static mount.
|
||||||
|
location ~ /\.(?!well-known) {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:bak|conf|dist|fla|in[ci]|log|orig|psd|sh|sql|swp|tmp)$ {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Public SPA entry page.
|
||||||
|
location = / {
|
||||||
|
try_files /index.html =404;
|
||||||
|
add_header Cache-Control "no-cache" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Public static assets.
|
||||||
|
# /static/app.js -> /usr/share/nginx/html/app.js
|
||||||
|
location ^~ /static/ {
|
||||||
|
alias /usr/share/nginx/html/;
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
expires 1h;
|
||||||
|
add_header Cache-Control "public, max-age=3600" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Long-cache versioned JavaScript asset.
|
||||||
|
location = /static/app--3.js {
|
||||||
|
alias /usr/share/nginx/html/app--3.js;
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, max-age=2592000, immutable" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional public robots file.
|
||||||
|
location = /robots.txt {
|
||||||
|
try_files /robots.txt =404;
|
||||||
|
default_type text/plain;
|
||||||
|
add_header Cache-Control "public, max-age=86400" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Protected files from a separate mounted directory.
|
||||||
|
#
|
||||||
|
# URL: /files/report.pdf
|
||||||
|
# Container: /protected/report.pdf
|
||||||
|
location = /files {
|
||||||
|
return 301 /files/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /files/ {
|
||||||
|
auth_basic "Private media";
|
||||||
|
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||||
|
|
||||||
|
alias /protected/;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# FastAPI validates share-token syntax and returns SPA documents.
|
||||||
|
location ^~ /share/ {
|
||||||
|
proxy_pass http://fastapi;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $realip_remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Expensive yt-dlp endpoints.
|
||||||
|
location ~ ^/api/(search|recommendations)$ {
|
||||||
|
limit_req zone=search_limit burst=15 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://fastapi;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $realip_remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Audio streaming, preserving Range/seek support.
|
||||||
|
location ^~ /media/ {
|
||||||
|
limit_req zone=audio_limit burst=30 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://fastapi;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $realip_remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
proxy_set_header Range $http_range;
|
||||||
|
proxy_set_header If-Range $http_if_range;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
send_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Audio download streaming.
|
||||||
|
location ^~ /api/download/ {
|
||||||
|
limit_req zone=audio_limit burst=20 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://fastapi;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $realip_remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
proxy_set_header Range $http_range;
|
||||||
|
proxy_set_header If-Range $http_if_range;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
send_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# FastAPI health endpoints.
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
proxy_pass http://fastapi/healthz;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $realip_remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_read_timeout 15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /readyz {
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
proxy_pass http://fastapi/readyz;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $realip_remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_read_timeout 15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remaining FastAPI endpoints.
|
||||||
|
location /api/ {
|
||||||
|
limit_req zone=general_limit burst=60 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://fastapi;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $realip_remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default deny: only explicitly configured paths are exposed.
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
fastapi
|
||||||
|
uvloop
|
||||||
|
httptools
|
||||||
|
uvicorn[standard]
|
||||||
|
yt-dlp
|
||||||
|
httpx
|
||||||
|
aiosqlite
|
||||||
|
pytest
|
||||||
|
pytest-asyncio
|
||||||
|
curl_cffi
|
||||||
Loading…
x
Reference in New Issue
Block a user