Merge pull request #536 from zivkovicp/feature/docker-support

feat: add optional Docker setup for local development
This commit is contained in:
Shu Yao 2026-02-10 12:32:16 +08:00 committed by GitHub
commit b8b069373d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 292 additions and 27 deletions

27
.dockerignore Normal file
View File

@ -0,0 +1,27 @@
.git
.gitignore
.gitattributes
*.pyc
.DS_Store
Thumbs.db
.idea/
.vscode/
__pycache__/
.venv/
env/
venv/
.uv-cache
.env
.env.*
node_modules/
WareHouse/
data/
temp/
logs
/frontend
README*
compose.yml
Dockerfile

10
.env.docker Normal file
View File

@ -0,0 +1,10 @@
BACKEND_BIND=0.0.0.0
FRONTEND_HOST=0.0.0.0
FRONTEND_PORT=5173
# Frontend points to the backend service name and exposed port
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

2
.gitignore vendored
View File

@ -26,4 +26,4 @@ logs/
node_modules/
data/
temp/
WareHouse/
WareHouse/

63
Dockerfile Normal file
View File

@ -0,0 +1,63 @@
# ---- Builder: install deps with compilers and uv ----
FROM python:3.12-slim AS builder
ARG DEBIAN_FRONTEND=noninteractive
WORKDIR /app
# System deps required to build Python packages
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config \
build-essential \
python3-dev \
libcairo2-dev \
&& rm -rf /var/lib/apt/lists/*
# Install uv just for dependency resolution/install
RUN pip install --no-cache-dir uv
# Install the project virtualenv outside /app so bind-mounts don't hide it
ENV UV_PROJECT_ENVIRONMENT=/opt/venv
# Copy dependency files first to maximize cache
COPY pyproject.toml ./
# reproducible builds:
COPY uv.lock ./
# Create the project virtualenv and install deps
RUN uv sync --no-cache --frozen
# ---- Runtime: minimal image with only runtime libs + app ----
FROM python:3.12-slim AS runtime
ARG DEBIAN_FRONTEND=noninteractive
ARG BACKEND_BIND=0.0.0.0
WORKDIR /app
# Install only runtime system libraries (no compilers)
# Keep libcairo if your deps need it; remove if unnecessary
RUN apt-get update && apt-get install -y --no-install-recommends \
libcairo2 \
&& rm -rf /var/lib/apt/lists/*
# Copy the prebuilt virtualenv from the builder
COPY --from=builder /opt/venv /opt/venv
# Copy the rest of the application code
COPY . .
# Use the venv Python by default and keep Python output unbuffered.
# Bake default bind/port into the image; can be overridden at runtime.
ENV PATH="/opt/venv/bin:${PATH}" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
BACKEND_BIND=${BACKEND_BIND}
# Drop privileges
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
# EXPOSE is informational; compose controls published ports
EXPOSE 6400
# Command to run the backend server, parameterized by env
CMD ["sh", "-c", "python server_main.py --port 6400 --host ${BACKEND_BIND:-0.0.0.0}"]

View File

@ -118,7 +118,7 @@ ChatDev 已从一个专门的软件开发多智能体系统演变为一个全面
cd frontend && npm install
```
### ⚡️ 运行应用
### ⚡️ 运行应用(本地)
#### 使用 Makefile推荐
@ -171,6 +171,25 @@ make dev
检查所有 YAML 文件的语法与 schema 错误。
### 🐳 使用 Docker 运行
你也可以通过 Docker Compose 运行整个应用。该方式可简化依赖管理,并提供一致的运行环境。
1. **前置条件**
* 已安装 [Docker](https://docs.docker.com/get-docker/) 和 [Docker Compose](https://docs.docker.com/compose/install/)。
* 请确保在项目根目录中存在用于配置 API Key 的 `.env` 文件。
2. **构建并运行**
```bash
# 在项目根目录执行
docker compose up --build
```
3. **访问地址**
* **后端**`http://localhost:6400`
* **前端**`http://localhost:5173`
> 服务在异常退出后会自动重启,本地文件的修改会同步映射到容器中,便于实时开发。
### 🔑 配置
* **环境变量**:在项目根目录创建一个 `.env` 文件。

View File

@ -131,7 +131,6 @@ See our paper in [Multi-Agent Collaboration via Evolving Orchestration](https://
### ⚡️ Run the Application
#### Using Makefile (Recommended)
**Start both Backend and Frontent**:
@ -183,6 +182,24 @@ make dev
```
Checks all YAML files for syntax and schema errors.
### 🐳 Run with Docker
Alternatively, you can run the entire application using Docker Compose. This method simplifies dependency management and provides a consistent environment.
1. **Prerequisites**:
* [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) installed.
* Ensure you have a `.env` file in the project root for your API keys.
2. **Build and Run**:
```bash
# From the project root
docker compose up --build
```
3. **Access**:
* **Backend**: `http://localhost:6400`
* **Frontend**: `http://localhost:5173`
> The services will automatically restart if they crash, and local file changes will be reflected inside the containers for live development.
---

33
compose.yml Normal file
View File

@ -0,0 +1,33 @@
services:
backend:
build:
context: .
dockerfile: Dockerfile
target: runtime
container_name: chatdev_backend
volumes:
- .:/app
ports:
- "6400:6400"
env_file:
- .env
- .env.docker
restart: unless-stopped
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
target: dev
container_name: chatdev_frontend
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- "${FRONTEND_PORT:-5173}:5173"
env_file:
- .env
- .env.docker
depends_on:
- backend
restart: unless-stopped

24
frontend/.dockerignore Normal file
View File

@ -0,0 +1,24 @@
.git
.gitignore
.gitattributes
*.pyc
.DS_Store
Thumbs.db
.idea/
.vscode/
__pycache__/
.venv/
env/
venv/
.uv-cache
.env
.env.*
node_modules/
temp/
logs
README*
compose.yml
Dockerfile

15
frontend/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
# ---- Dependencies: install node_modules once (cached) ----
FROM node:24-alpine AS deps
WORKDIR /app
COPY package*.json ./
# Prefer reproducible installs; fall back if no lockfile
RUN npm ci --no-audit --no-fund || npm install --no-audit --no-fund
# ---- Dev runtime: hot-reload server ----
FROM node:24-alpine AS dev
WORKDIR /app
ENV NODE_ENV=development
COPY --from=deps /app/node_modules /app/node_modules
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]

View File

@ -969,10 +969,24 @@ const establishWebSocketConnection = () => {
return
}
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
const wsProtocol = baseUrl.startsWith('https') ? 'wss:' : 'ws:'
const urlObj = new URL(baseUrl)
const wsUrl = `${wsProtocol}//${urlObj.host}/ws`
const apiBase = import.meta.env.VITE_API_BASE_URL || ''
// Defaults: same-origin (works with Vite dev proxy)
const defaultScheme = window.location.protocol === 'https:' ? 'wss:' : '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)
ws = socket

View File

@ -1363,10 +1363,24 @@ const establishWebSocketConnection = () => {
return
}
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
const wsProtocol = baseUrl.startsWith('https') ? 'wss:' : 'ws:'
const urlObj = new URL(baseUrl)
const wsUrl = `${wsProtocol}//${urlObj.host}/ws`
const apiBase = import.meta.env.VITE_API_BASE_URL || ''
// Defaults: same-origin (works with Vite dev proxy)
const defaultScheme = window.location.protocol === 'https:' ? 'wss:' : '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)
ws = socket

View File

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

View File

@ -4,15 +4,21 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
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 {
plugins: [vue()],
server: {
host: true,
proxy: {
'/api': {
target: target,
changeOrigin: true,
},
'/ws': {
target: target,
ws: true,
changeOrigin: true,
}
}
}

View File

@ -1,7 +1,6 @@
"""Application bootstrap helpers for the FastAPI server."""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from server import state
from server.config_schema_router import router as config_schema_router
@ -13,14 +12,6 @@ from utils.middleware import add_middleware
def init_app(app: FastAPI) -> None:
"""Apply shared middleware, routers, and global state to ``app``."""
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
add_exception_handlers(app)
add_middleware(app)

View File

@ -2,10 +2,12 @@
import uuid
from typing import Callable, Awaitable
from fastapi import Request, HTTPException
from fastapi import Request, HTTPException, FastAPI
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
import time
import re
import os
from utils.structured_logger import get_server_logger, LogType
from utils.exceptions import SecurityError
@ -85,11 +87,42 @@ async def rate_limit_middleware(request: Request, call_next: Callable):
return response
def add_middleware(app):
def add_cors_middleware(app: FastAPI) -> None:
"""Configure and attach CORS middleware."""
# Dev defaults; override via CORS_ALLOW_ORIGINS (comma-separated)
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+)?$"
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,
)
def add_middleware(app: FastAPI):
"""Add all middleware to the FastAPI application."""
# Add middleware in the appropriate order
# Attach CORS first to handle preflight requests and allow origins.
add_cors_middleware(app)
# Add other middleware
app.middleware("http")(correlation_id_middleware)
app.middleware("http")(security_middleware)
# app.middleware("http")(rate_limit_middleware) # Enable if needed
return app
return app