chore(deploy): serve SPA without nginx and slim docker image

This commit is contained in:
shiyu
2025-12-09 14:21:20 +08:00
parent 055c240079
commit 16283dea09
4 changed files with 44 additions and 56 deletions
+9 -6
View File
@@ -14,23 +14,26 @@ FROM python:3.13-slim
WORKDIR /app WORKDIR /app
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends nginx git ffmpeg \ && apt-get install -y --no-install-recommends ffmpeg curl ca-certificates \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN pip install uv RUN pip install uv
COPY pyproject.toml uv.lock ./ COPY pyproject.toml uv.lock ./
RUN uv pip install --system . gunicorn RUN uv pip install --system . gunicorn \
&& rm -rf /root/.cache
RUN git clone https://github.com/DrizzleTime/FoxelUpgrade /app/migrate RUN curl -L https://github.com/DrizzleTime/FoxelUpgrade/archive/refs/heads/main.tar.gz -o /tmp/migrate.tgz \
&& mkdir -p /app/migrate \
&& tar -xzf /tmp/migrate.tgz --strip-components=1 -C /app/migrate \
&& rm -rf /tmp/migrate.tgz
COPY --from=frontend-builder /app/web/dist /app/web/dist COPY --from=frontend-builder /app/web/dist /app/web/dist
COPY . . COPY . .
COPY nginx.conf /etc/nginx/nginx.conf
RUN mkdir -p data/db data/mount && \ RUN mkdir -p data/db data/mount && \
chmod 777 data/db data/mount chmod 777 data/db data/mount && \
rm -rf /var/log/apt /var/cache/apt/archives
EXPOSE 80 EXPOSE 80
+1 -2
View File
@@ -1,5 +1,4 @@
#!/bin/bash #!/bin/bash
set -e set -e
python migrate/run.py python migrate/run.py
nginx -g 'daemon off;' & exec gunicorn -k uvicorn.workers.UvicornWorker -w 1 -b 0.0.0.0:80 main:app
exec gunicorn -k uvicorn.workers.UvicornWorker -w 1 -b 0.0.0.0:8000 main:app
+34 -3
View File
@@ -1,11 +1,15 @@
import os import os
from pathlib import Path
from contextlib import asynccontextmanager
from domain.config.service import ConfigService, VERSION from domain.config.service import ConfigService, VERSION
from domain.adapters.registry import runtime_registry from domain.adapters.registry import runtime_registry
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from db.session import close_db, init_db from db.session import close_db, init_db
from api.routers import include_routers from api.routers import include_routers
from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from middleware.exception_handler import ( from middleware.exception_handler import (
global_exception_handler, global_exception_handler,
@@ -20,6 +24,31 @@ from domain.tasks.task_queue import task_queue_service
load_dotenv() load_dotenv()
class SPAStaticFiles(StaticFiles):
async def get_response(self, path, scope):
response = await super().get_response(path, scope)
if response.status_code == 404:
return await super().get_response("index.html", scope)
return response
INDEX_FILE = Path("web/dist/index.html")
SPA_EXCLUDE_PREFIXES = ("/api", "/docs", "/openapi.json", "/webdav", "/s3")
async def spa_fallback_middleware(request: Request, call_next):
response = await call_next(request)
if (
response.status_code == 404
and request.method == "GET"
and "text/html" in request.headers.get("accept", "")
and not request.url.path.startswith(SPA_EXCLUDE_PREFIXES)
and INDEX_FILE.exists()
):
return FileResponse(INDEX_FILE)
return response
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
os.makedirs("data/db", exist_ok=True) os.makedirs("data/db", exist_ok=True)
@@ -40,6 +69,7 @@ def create_app() -> FastAPI:
description="A highly extensible private cloud storage solution for individuals and teams", description="A highly extensible private cloud storage solution for individuals and teams",
lifespan=lifespan, lifespan=lifespan,
) )
app.middleware("http")(spa_fallback_middleware)
include_routers(app) include_routers(app)
app.add_exception_handler(HTTPException, http_exception_handler) app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler)
@@ -56,6 +86,7 @@ app.add_middleware(
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
app.mount("/", SPAStaticFiles(directory="web/dist", html=True, check_dir=False), name="static")
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
-45
View File
@@ -1,45 +0,0 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
client_max_body_size 4G;
server {
listen 80;
server_name _;
location ~ ^/(api|webdav|s3|docs|openapi\.json$) {
proxy_pass http://127.0.0.1:8000;
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 / {
root /app/web/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
}
}