作者:Ethan Zhang | 发布时间:2026-07-30 | 标签:
DockerReactFlaskPostgreSQL全栈
传统开发中,前后端、数据库往往需要分别安装配置环境,团队协作时极易出现“在我机器上能跑”的问题。Docker Compose 通过一个 YAML 文件定义多容器应用,实现:
本文将带你亲手搭建一个生产级模板,掌握容器化全栈开发的精髓。
fullstack-app/
├── backend/ # Flask 后端
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── app.py
│ └── models.py
├── frontend/ # React 前端
│ ├── Dockerfile
│ ├── package.json
│ ├── vite.config.js
│ ├── index.html
│ └── src/
│ ├── App.jsx
│ ├── main.jsx
│ └── api.js
├── docker-compose.yml
└── .env # 环境变量(不提交)backend/requirements.txt
Flask==2.3.3
Flask-CORS==4.0.0
psycopg2-binary==2.9.9
python-dotenv==1.0.0backend/models.py(数据库模型)
import os
import psycopg2
from psycopg2.extras import RealDictCursor
def get_db_connection():
conn = psycopg2.connect(
host=os.environ.get('DB_HOST', 'db'),
database=os.environ.get('DB_NAME', 'postgres'),
user=os.environ.get('DB_USER', 'postgres'),
password=os.environ.get('DB_PASSWORD', 'postgres'),
port=5432
)
return conn
def init_db():
conn = get_db_connection()
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS items (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
cur.close()
conn.close()
def get_all_items():
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute('SELECT * FROM items ORDER BY id')
items = cur.fetchall()
cur.close()
conn.close()
return items
def add_item(name, description):
conn = get_db_connection()
cur = conn.cursor()
cur.execute(
'INSERT INTO items (name, description) VALUES (%s, %s) RETURNING id',
(name, description)
)
item_id = cur.fetchone()[0]
conn.commit()
cur.close()
conn.close()
return item_idbackend/app.py(主入口)
import os
from flask import Flask, request, jsonify
from flask_cors import CORS
from models import init_db, get_all_items, add_item
app = Flask(__name__)
CORS(app) # 允许前端跨域
@app.route('/health', methods=['GET'])
def health():
return jsonify({'status': 'ok'})
@app.route('/api/items', methods=['GET'])
def list_items():
items = get_all_items()
return jsonify(items)
@app.route('/api/items', methods=['POST'])
def create_item():
data = request.get_json()
if not data or 'name' not in data:
return jsonify({'error': 'Name is required'}), 400
name = data['name']
description = data.get('description', '')
item_id = add_item(name, description)
return jsonify({'id': item_id, 'name': name, 'description': description}), 201
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=5000, debug=True)backend/Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_ENV=development
EXPOSE 5000
CMD ["python", "app.py"]使用 Vite 快速创建 React 项目(TypeScript 可选,本文用 JSX)。
frontend/package.json(核心依赖)
{
"name": "frontend",
"version": "0.0.0",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.6.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.0",
"vite": "^4.5.0"
}
}frontend/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://backend:5000',
changeOrigin: true,
}
}
}
})frontend/src/api.js(API 调用)
import axios from 'axios';
const api = axios.create({
baseURL: '/api', // 开发时通过 Vite 代理转发到后端
headers: { 'Content-Type': 'application/json' }
});
export const getItems = () => api.get('/items');
export const createItem = (name, description) => api.post('/items', { name, description });frontend/src/App.jsx
import React, { useState, useEffect } from 'react';
import { getItems, createItem } from './api';
function App() {
const [items, setItems] = useState([]);
const [name, setName] = useState('');
const [desc, setDesc] = useState('');
const loadItems = async () => {
try {
const res = await getItems();
setItems(res.data);
} catch (err) {
console.error('加载数据失败', err);
}
};
useEffect(() => {
loadItems();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
if (!name.trim()) return;
await createItem(name, desc);
setName('');
setDesc('');
loadItems();
};
return (
<div style={{ padding: '2rem', fontFamily: 'sans-serif' }}>
<h1>📦 物品列表</h1>
<form onSubmit={handleSubmit} style={{ marginBottom: '1.5rem' }}>
<input
type="text"
placeholder="名称"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ marginRight: '0.5rem' }}
/>
<input
type="text"
placeholder="描述"
value={desc}
onChange={(e) => setDesc(e.target.value)}
style={{ marginRight: '0.5rem' }}
/>
<button type="submit">添加</button>
</form>
<ul>
{items.map(item => (
<li key={item.id}>
<strong>{item.name}</strong> - {item.description || '无描述'}
<span style={{ marginLeft: '1rem', color: '#888', fontSize: '0.8rem' }}>
{new Date(item.created_at).toLocaleString()}
</span>
</li>
))}
</ul>
</div>
);
}
export default App;frontend/src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);frontend/index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>全栈演示</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>frontend/Dockerfile
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]注意:为了开发时方便热重载,我们也可以直接使用
node镜像运行npm run dev,但生产环境建议使用 Nginx 托管静态文件。本文采用开发模式,即直接运行 dev 服务,便于实时修改。
修改 frontend/Dockerfile 用于开发(我们采用开发模式,所以用下面这个):
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]直接使用官方镜像 postgres:15-alpine,无需额外 Dockerfile。我们只需在 docker-compose.yml 中配置环境变量和持久化卷。
docker-compose.yml
version: '3.8'
services:
db:
image: postgres:15-alpine
container_name: fullstack-db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
backend:
build: ./backend
container_name: fullstack-backend
environment:
DB_HOST: db
DB_NAME: postgres
DB_USER: postgres
DB_PASSWORD: postgres
ports:
- "5000:5000"
depends_on:
db:
condition: service_healthy
networks:
- app-network
volumes:
- ./backend:/app # 开发时挂载代码,实现热更新
command: python app.py
frontend:
build: ./frontend
container_name: fullstack-frontend
ports:
- "3000:3000"
depends_on:
- backend
networks:
- app-network
volumes:
- ./frontend:/app
- /app/node_modules # 避免覆盖容器内的 node_modules
environment:
- VITE_API_URL=http://backend:5000 # 传递给 Vite 环境变量(可选)
command: npm run dev
volumes:
pgdata:
networks:
app-network:
driver: bridge关键点说明:
depends_on 确保数据库先启动,且 condition: service_healthy 保证数据库就绪后再启动后端。volumes 挂载源码,修改代码后容器内自动更新(Flask 的 debug 模式、Vite 的 HMR 都支持)。app-network 让容器间通过服务名(如 db、backend)互相访问。在项目根目录执行:
docker-compose up --build首次启动会构建镜像并下载依赖,耐心等待。看到以下日志表示成功:
db 健康检查通过backend 输出 Running on http://0.0.0.0:5000frontend 输出 VITE v4.5.0 ready in xxx mscurl http://localhost:5000/health 返回 {"status":"ok"}在网页中添加几个物品,数据会存入 PostgreSQL,刷新页面数据保留。
进入数据库容器:
docker exec -it fullstack-db psql -U postgres查询数据:
SELECT * FROM items;将敏感信息(如数据库密码)放在 .env 文件中,并在 compose 中引用:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}同时将 .env 加入 .gitignore。
gunicorn 替代 flask run。集成 ELK 或 Prometheus,使用 docker-compose 的 logging 驱动。
配合 GitHub Actions 或 GitLab CI,实现自动化构建和部署。
本文通过一个完整的全栈示例,展示了 Docker Compose 在开发环境中的强大能力。你将以下技能收入囊中:
这套模板可以直接作为你下一个项目的起点,无论你是独立开发者还是团队协作,容器化都将极大降低环境配置成本。未来你可以继续集成 Redis、消息队列或微服务网关,让架构更加健壮。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。