fix: resolve runtime issues in Docker environment

This commit is contained in:
Petar Zivkovic 2026-02-07 00:56:47 +01:00
parent d60bb77bf0
commit 83a7c36f3b
12 changed files with 155 additions and 14 deletions

View File

@ -20,7 +20,6 @@ WareHouse/
data/ data/
temp/ temp/
logs logs
.aider*
/frontend /frontend
README* README*

View File

@ -5,3 +5,6 @@ FRONTEND_PORT=5173
# Frontend points to the backend service name and exposed port # Frontend points to the backend service name and exposed port
VITE_API_BASE_URL=http://backend:6400 VITE_API_BASE_URL=http://backend:6400
# Explicit CORS origins for Docker-based dev (comma-separated)
CORS_ALLOW_ORIGINS=http://localhost:5173,http://127.0.0.1:5173

View File

@ -190,6 +190,23 @@ make dev
> 服务在异常退出后会自动重启,本地文件的修改会同步映射到容器中,便于实时开发。 > 服务在异常退出后会自动重启,本地文件的修改会同步映射到容器中,便于实时开发。
#### 生产环境Docker + nginx
- 构建并运行:
```bash
docker compose -f compose.yml -f compose.prod.yml up -d --build
```
- 访问:
- 前端http://localhost:8080
- 后端:不对外暴露,由 nginx 通过 /api 与 /ws 转发
- 停止:
```bash
docker compose -f compose.yml -f compose.prod.yml down
```
- 说明:
- .env 保存密钥API Keys.env.docker 保存容器相关配置。
- nginx 将 /api 与 /ws 代理到 backend:6400见 frontend/nginx.conf
### 🔑 配置 ### 🔑 配置
* **环境变量**:在项目根目录创建一个 `.env` 文件。 * **环境变量**:在项目根目录创建一个 `.env` 文件。

44
compose.prod.yml Normal file
View File

@ -0,0 +1,44 @@
services:
backend:
build:
context: .
dockerfile: Dockerfile
target: runtime
container_name: chatdev_backend
env_file:
- .env
- .env.docker
expose:
- "6400"
restart: unless-stopped
networks:
- appnet
# Optional healthcheck (adjust path if different)
# healthcheck:
# test: ["CMD-SHELL", "curl -fsS http://localhost:6400/api/health || exit 1"]
# interval: 10s
# timeout: 3s
# retries: 5
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
target: prod
container_name: chatdev_frontend
ports:
- "8080:80"
depends_on:
- backend
restart: unless-stopped
networks:
- appnet
# Optional healthcheck
# healthcheck:
# test: ["CMD-SHELL", "wget -q -O - http://localhost/ >/dev/null 2>&1 || exit 1"]
# interval: 10s
# timeout: 3s
# retries: 5
networks:
appnet: {}

View File

@ -18,7 +18,6 @@ venv/
node_modules/ node_modules/
temp/ temp/
logs logs
.aider*
README* README*
compose.yml compose.yml

2
frontend/.gitignore vendored
View File

@ -7,7 +7,7 @@ yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules/ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local

View File

@ -27,7 +27,7 @@ RUN npm run build
# ---- Prod runtime: serve static dist with nginx ---- # ---- Prod runtime: serve static dist with nginx ----
FROM nginx:alpine AS prod FROM nginx:alpine AS prod
# For SPA deep links you might add a custom nginx.conf with index.html fallback. # For SPA deep links you might add a custom nginx.conf with index.html fallback.
# COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

32
frontend/nginx.conf Normal file
View File

@ -0,0 +1,32 @@
server {
listen 80;
server_name _;
# Serve built assets
root /usr/share/nginx/html;
index index.html;
# Proxy API
location /api/ {
proxy_pass http://backend:6400/api/;
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 WebSocket
location /ws {
proxy_pass http://backend:6400/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@ -1363,10 +1363,24 @@ const establishWebSocketConnection = () => {
return return
} }
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000' const apiBase = import.meta.env.VITE_API_BASE_URL || ''
const wsProtocol = baseUrl.startsWith('https') ? 'wss:' : 'ws:' // Defaults: same-origin (works with Vite dev proxy)
const urlObj = new URL(baseUrl) const defaultScheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${wsProtocol}//${urlObj.host}/ws` let scheme = defaultScheme
let host = window.location.host
// In production, prefer explicit API base if provided
if (!import.meta.env.DEV && apiBase) {
try {
const api = new URL(apiBase, window.location.origin)
scheme = api.protocol === 'https:' ? 'wss:' : 'ws:'
host = api.host
} catch {
// keep defaults
}
}
const wsUrl = `${scheme}//${host}/ws`
const socket = new WebSocket(wsUrl) const socket = new WebSocket(wsUrl)
ws = socket ws = socket

View File

@ -1,7 +1,6 @@
import yaml from 'js-yaml' import yaml from 'js-yaml'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000' const apiUrl = (path) => path
const apiUrl = (path) => `${API_BASE_URL}${path}`
const addYamlSuffix = (filename) => { const addYamlSuffix = (filename) => {
const trimmed = (filename || '').trim() const trimmed = (filename || '').trim()

View File

@ -4,15 +4,21 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '') const env = loadEnv(mode, process.cwd(), '')
const target = env.VITE_API_BASE_URL || 'http://localhost:8000' const target = env.VITE_API_BASE_URL || 'http://localhost:6400'
return { return {
plugins: [vue()], plugins: [vue()],
server: { server: {
host: true,
proxy: { proxy: {
'/api': { '/api': {
target: target, target: target,
changeOrigin: true, changeOrigin: true,
},
'/ws': {
target: target,
ws: true,
changeOrigin: true,
} }
} }
} }

View File

@ -2,10 +2,12 @@
import uuid import uuid
from typing import Callable, Awaitable from typing import Callable, Awaitable
from fastapi import Request, HTTPException from fastapi import Request, HTTPException, FastAPI
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
import time import time
import re import re
import os
from utils.structured_logger import get_server_logger, LogType from utils.structured_logger import get_server_logger, LogType
from utils.exceptions import SecurityError from utils.exceptions import SecurityError
@ -85,9 +87,35 @@ async def rate_limit_middleware(request: Request, call_next: Callable):
return response return response
def add_middleware(app): def add_middleware(app: FastAPI):
"""Add all middleware to the FastAPI application.""" """Add all middleware to the FastAPI application."""
# Add middleware in the appropriate order # CORS (dev defaults; override via CORS_ALLOW_ORIGINS comma-separated list)
default_origins = [
"http://localhost:5173",
"http://127.0.0.1:5173",
]
env_origins = os.getenv("CORS_ALLOW_ORIGINS")
if env_origins:
origins = [o.strip() for o in env_origins.split(",") if o.strip()]
origin_regex = None
else:
origins = default_origins
# Helpful in dev: allow localhost/127.0.0.1 on any port
origin_regex = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$"
# Add CORS middleware first to handle preflight requests and allow origins.
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origin_regex=origin_regex,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Correlation-ID"],
max_age=600,
)
# Add other middleware
app.middleware("http")(correlation_id_middleware) app.middleware("http")(correlation_id_middleware)
app.middleware("http")(security_middleware) app.middleware("http")(security_middleware)
# app.middleware("http")(rate_limit_middleware) # Enable if needed # app.middleware("http")(rate_limit_middleware) # Enable if needed