📚前言
👀回顾,系统学习docker系列已发布内容:
【docker基础】第三课:镜像管理与Dockerfile基础
【docker基础】第五课:Docker网络详解-CSDN博客
【docker基础】第六课:Web应用与数据库容器部署-CSDN博客
【docker基础】 第七课:Docker Compose 多容器实战-CSDN博客
【docker基础】 第八周:容器监控与应用更新策略-CSDN博客
【docker基础】第九周:Docker安全与镜像优化-CSDN博客
【docker基础】Docker第十周:CI/CD集成-CSDN博客
🔗相关文档:
【docker基础】Ubuntu 安装 Docker 超详细小白教程
📒本课学习目标:
- 项目架构设计:前端 + 后端 + 数据库 + 缓存
- 多服务开发:Node.js API + Vue.js 前端
- 配置文件:MySQL、Redis、Nginx
- 环境配置:开发环境和生产环境
- 资源限制:CPU、内存限制
- 监控日志:日志轮转、健康检查
- 部署流程:构建、推送、部署
- 性能优化:网络、存储、缓存优化
🌍Docker第十二周:综合实战项目
欢迎来到 Docker 第十二周的学习!本周我们将进行一个完整的实战项目,从代码开发到容器化部署,完成一个真实的生产级应用部署流程。
第一章:项目概述
1.1 项目架构
我们将部署一个完整的博客系统,包含以下组件:
┌─────────────────────────────────────────────────────────────┐
│ 用户请求 │
│ http://example.com │
└───────────────────────────┬─────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────┐
│ Nginx 反向代理 │
│ (负载均衡 + SSL) │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
↓ ↓ ↓
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Web Server 1 │ │ Web Server 2 │ │ Web Server 3 │
│ (Nginx) │ │ (Nginx) │ │ (Nginx) │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└───────────────────┼───────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────┐
│ 应用服务器 (Node.js) │
│ API 服务集群 │
└───────────────────────────┬─────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────┐
│ Redis 缓存服务器 │
│ (会话 + 热点数据) │
└─────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────┐
│ MySQL 数据库 │
│ (主从复制) │
└─────────────────────────────────────────────────────────────┘
1.2 技术栈
| 组件 | 技术 | 说明 |
|---|---|---|
| 前端 | Vue.js | 单页应用 |
| 后端 | Node.js + Express | REST API |
| 数据库 | MySQL 5.7 | 关系型数据库 |
| 缓存 | Redis | 会话和缓存 |
| Web服务器 | Nginx | 反向代理 |
| 容器编排 | Docker Compose | 本地开发 |
| CI/CD | GitHub Actions | 自动部署 |
第二章:项目结构
2.1 目录结构
blog-app/
├── frontend/ # 前端代码
│ ├── src/
│ ├── public/
│ ├── Dockerfile
│ └── package.json
├── backend/ # 后端代码
│ ├── src/
│ ├── Dockerfile
│ └── package.json
├── nginx/ # Nginx 配置
│ ├── nginx.conf
│ └── conf.d/
│ └── blog.conf
├── mysql/ # MySQL 配置
│ └── init.sql
├── redis/ # Redis 配置
│ └── redis.conf
├── monitoring/ # 监控配置
│ └── prometheus.yml
├── docker-compose.yml # 开发环境
├── docker-compose.prod.yml # 生产环境
└── .env # 环境变量
2.2 创建项目目录
mkdir -p ~/blog-app/{frontend,backend,nginx/conf.d,mysql,redis,monitoring}
cd ~/blog-app
第三章:后端服务开发
3.1 创建后端项目
backend/package.json:
{
"name": "blog-api",
"version": "1.0.0",
"description": "Blog API Server",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
},
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.6.0",
"redis": "^4.6.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
3.2 后端代码
backend/src/index.js:
const express = require('express');
const cors = require('cors');
const mysql = require('mysql2/promise');
const redis = require('redis');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(express.json());
// MySQL 连接池
const db = mysql.createPool({
host: process.env.DB_HOST || 'mysql',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'blog',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Redis 客户端
const redisClient = redis.createClient({
url: `redis://${process.env.REDIS_HOST || 'redis'}:${process.env.REDIS_PORT || 6379}`
});
redisClient.on('error', (err) => console.log('Redis Client Error', err));
redisClient.connect().then(() => console.log('Redis Connected'));
// 健康检查
app.get('/health', async (req, res) => {
try {
await db.query('SELECT 1');
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
} catch (error) {
res.status(500).json({ status: 'unhealthy', error: error.message });
}
});
// 获取文章列表(带缓存)
app.get('/api/articles', async (req, res) => {
try {
// 尝试从缓存获取
const cached = await redisClient.get('articles:list');
if (cached) {
return res.json(JSON.parse(cached));
}
// 从数据库获取
const [rows] = await db.query('SELECT * FROM articles ORDER BY created_at DESC');
// 存入缓存(60秒)
await redisClient.setEx('articles:list', 60, JSON.stringify(rows));
res.json(rows);
} catch (error) {
console.error('Error fetching articles:', error);
res.status(500).json({ error: 'Failed to fetch articles' });
}
});
// 获取单篇文章
app.get('/api/articles/:id', async (req, res) => {
try {
const { id } = req.params;
// 尝试从缓存获取
const cached = await redisClient.get(`articles:${id}`);
if (cached) {
return res.json(JSON.parse(cached));
}
// 从数据库获取
const [rows] = await db.query('SELECT * FROM articles WHERE id = ?', [id]);
if (rows.length === 0) {
return res.status(404).json({ error: 'Article not found' });
}
// 存入缓存
await redisClient.setEx(`articles:${id}`, 300, JSON.stringify(rows[0]));
res.json(rows[0]);
} catch (error) {
console.error('Error fetching article:', error);
res.status(500).json({ error: 'Failed to fetch article' });
}
});
// 创建文章
app.post('/api/articles', async (req, res) => {
try {
const { title, content, author } = req.body;
const [result] = await db.query(
'INSERT INTO articles (title, content, author) VALUES (?, ?, ?)',
[title, content, author]
);
// 清除列表缓存
await redisClient.del('articles:list');
res.status(201).json({ id: result.insertId, message: 'Article created' });
} catch (error) {
console.error('Error creating article:', error);
res.status(500).json({ error: 'Failed to create article' });
}
});
// 启动服务器
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});
3.3 后端 Dockerfile
backend/Dockerfile:
# 使用 Node.js Alpine 镜像(减小体积)
FROM node:18-alpine
# 创建应用目录
WORKDIR /app
# 复制 package 文件
COPY package*.json ./
# 安装依赖(使用缓存层)
RUN npm ci --only=production
# 复制应用代码
COPY src/ ./src/
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:3000/health || exit 1
# 启动命令
CMD ["node", "src/index.js"]
第四章:前端服务开发
4.1 前端代码
frontend/public/index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客系统</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
header { background: #333; color: white; padding: 20px; text-align: center; }
header h1 { margin-bottom: 10px; }
nav a { color: white; margin: 0 15px; text-decoration: none; }
.article-list { margin-top: 30px; }
.article-card { background: white; padding: 20px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
.article-card h2 { color: #333; margin-bottom: 10px; }
.article-meta { color: #666; font-size: 14px; margin-bottom: 10px; }
.article-content { line-height: 1.6; color: #444; }
.status { padding: 10px; background: #4CAF50; color: white; text-align: center; margin-bottom: 20px; border-radius: 4px; }
</style>
</head>
<body>
<header>
<h1>博客系统</h1>
<nav>
<a href="#" onclick="loadArticles()">首页</a>
<a href="#" onclick="showForm()">发布文章</a>
</nav>
</header>
<div class="container">
<div id="status" class="status" style="display: none;"></div>
<div id="content"></div>
</div>
<script>
const API_BASE = window.location.port === '8080' ? 'http://localhost:3000' : '/api';
async function loadArticles() {
const status = document.getElementById('status');
const content = document.getElementById('content');
try {
status.style.display = 'block';
status.textContent = '加载中...';
status.style.background = '#2196F3';
const response = await fetch(`${API_BASE}/articles`);
const articles = await response.json();
status.style.background = '#4CAF50';
status.textContent = `共 ${articles.length} 篇文章`;
content.innerHTML = articles.length === 0
? '<p>暂无文章</p>'
: articles.map(article => `
<div class="article-card">
<h2>${article.title}</h2>
<div class="article-meta">作者: ${article.author} | ${new Date(article.created_at).toLocaleString()}</div>
<div class="article-content">${article.content}</div>
</div>
`).join('');
} catch (error) {
status.style.background = '#f44336';
status.textContent = '加载失败: ' + error.message;
}
setTimeout(() => { status.style.display = 'none'; }, 3000);
}
function showForm() {
document.getElementById('content').innerHTML = `
<div class="article-card">
<h2>发布新文章</h2>
<form onsubmit="submitArticle(event)">
<p style="margin-bottom: 10px;">
<input type="text" id="title" placeholder="标题" required style="width: 100%; padding: 10px;">
</p>
<p style="margin-bottom: 10px;">
<input type="text" id="author" placeholder="作者" required style="width: 100%; padding: 10px;">
</p>
<p style="margin-bottom: 10px;">
<textarea id="content" placeholder="内容" rows="5" required style="width: 100%; padding: 10px;"></textarea>
</p>
<p>
<button type="submit" style="padding: 10px 30px; background: #333; color: white; border: none; cursor: pointer;">发布</button>
</p>
</form>
</div>
`;
}
async function submitArticle(event) {
event.preventDefault();
const title = document.getElementById('title').value;
const author = document.getElementById('author').value;
const content = document.getElementById('content').value;
try {
const response = await fetch(`${API_BASE}/articles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, author, content })
});
if (response.ok) {
alert('发布成功!');
loadArticles();
}
} catch (error) {
alert('发布失败: ' + error.message);
}
}
window.onload = loadArticles;
</script>
</body>
</html>
4.2 前端 Dockerfile
frontend/Dockerfile:
# 多阶段构建
# 阶段1:构建
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段2:运行
FROM nginx:alpine
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制 Nginx 配置
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
frontend/nginx.conf:
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}
第五章:配置文件
5.1 MySQL 初始化脚本
mysql/init.sql:
-- 创建数据库
CREATE DATABASE IF NOT EXISTS blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE blog;
-- 创建文章表
CREATE TABLE IF NOT EXISTS articles (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
author VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入测试数据
INSERT INTO articles (title, content, author) VALUES
('欢迎使用博客系统', '这是我们的第一篇文章!', '管理员'),
('Docker 入门指南', '本文介绍 Docker 的基本概念和使用方法...', '技术编辑'),
('容器化最佳实践', '本文分享容器化部署的最佳实践...', 'DevOps工程师');
5.2 Redis 配置
redis/redis.conf:
# 基本配置
port 6379
bind 0.0.0.0
protected-mode no
# 持久化配置
save 900 1
save 300 10
save 60 10000
# 内存配置
maxmemory 256mb
maxmemory-policy allkeys-lru
# 日志配置
loglevel notice
5.3 Nginx 配置
nginx/conf.d/blog.conf:
upstream backend {
server backend1:3000;
server backend2:3000;
server backend3:3000;
keepalive 32;
}
server {
listen 80;
server_name blog.example.com;
# 前端静态文件
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# API 代理
location /api {
proxy_pass http://backend;
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_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
第六章:开发环境配置
6.1 docker-compose.yml
docker-compose.yml:
version: '3.8'
services:
# MySQL 数据库
mysql:
image: mysql:5.7
container_name: blog-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: blog
volumes:
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- blog-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
# Redis 缓存
redis:
image: redis:7-alpine
container_name: blog-redis
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
- redis_data:/data
ports:
- "6379:6379"
networks:
- blog-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
# 后端服务1
backend1:
build: ./backend
container_name: blog-backend-1
environment:
NODE_ENV: development
DB_HOST: mysql
DB_USER: root
DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
DB_NAME: blog
REDIS_HOST: redis
PORT: 3000
volumes:
- ./backend/src:/app/src
networks:
- blog-network
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
# 后端服务2
backend2:
build: ./backend
container_name: blog-backend-2
environment:
NODE_ENV: development
DB_HOST: mysql
DB_USER: root
DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
DB_NAME: blog
REDIS_HOST: redis
PORT: 3000
volumes:
- ./backend/src:/app/src
networks:
- blog-network
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
# 前端服务
frontend:
image: nginx:alpine
container_name: blog-frontend
volumes:
- ./frontend/public:/usr/share/nginx/html
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
ports:
- "8080:80"
networks:
- blog-network
depends_on:
- backend1
- backend2
networks:
blog-network:
driver: bridge
volumes:
mysql_data:
redis_data:
6.2 .env 文件
.env:
# 数据库
MYSQL_ROOT_PASSWORD=your_secure_password_here
# 其他配置
NODE_ENV=development
TZ=Asia/Shanghai
第七章:生产环境配置
7.1 docker-compose.prod.yml
docker-compose.prod.yml:
version: '3.8'
services:
mysql:
image: mysql:5.7
container_name: blog-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: blog
volumes:
- mysql_data:/var/lib/mysql
ports:
- "127.0.0.1:3306:3306"
networks:
- blog-network
restart: always
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: blog-redis
command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
ports:
- "127.0.0.1:6379:6379"
networks:
- blog-network
restart: always
backend:
image: ${DOCKER_REGISTRY}/blog-backend:${VERSION}
container_name: blog-backend
environment:
NODE_ENV: production
DB_HOST: mysql
DB_USER: root
DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
DB_NAME: blog
REDIS_HOST: redis
REDIS_PASSWORD: ${REDIS_PASSWORD}
PORT: 3000
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
networks:
- blog-network
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
restart: always
nginx:
image: nginx:alpine
container_name: blog-nginx
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- nginx_logs:/var/log/nginx
ports:
- "80:80"
- "443:443"
networks:
- blog-network
depends_on:
- backend
restart: always
networks:
blog-network:
driver: bridge
volumes:
mysql_data:
redis_data:
nginx_logs:
7.2 资源限制配置
在生产环境中,我们使用 deploy.resources 进行资源限制:
deploy:
replicas: 3
resources:
limits:
cpus: '0.5' # 每个容器最多使用 0.5 个 CPU
memory: 512M # 每个容器最多使用 512MB 内存
reservations:
cpus: '0.25' # 预留资源
memory: 256M
第八章:监控与日志
8.1 Prometheus 配置
monitoring/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'blog-backend'
static_configs:
- targets: ['backend:3000']
labels:
app: 'blog-api'
- job_name: 'nginx'
static_configs:
- targets: ['nginx:80']
8.2 日志管理
配置日志轮转:
services:
backend:
image: ${DOCKER_REGISTRY}/blog-backend:${VERSION}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
第九章:部署流程
9.1 开发环境启动
# 1. 克隆代码
git clone https://github.com/yourusername/blog-app.git
cd blog-app
# 2. 创建 .env 文件
cp .env.example .env
# 编辑 .env 文件,设置密码
# 3. 启动开发环境
docker-compose up -d
# 4. 查看服务状态
docker-compose ps
# 5. 查看日志
docker-compose logs -f
# 6. 访问应用
# 前端: http://localhost:8080
# API: http://localhost:8080/api/articles
9.2 生产环境部署
# 1. 构建镜像
docker-compose -f docker-compose.prod.yml build
# 2. 标记镜像版本
docker tag blog-backend:latest ${DOCKER_REGISTRY}/blog-backend:${VERSION}
docker tag blog-backend:latest ${DOCKER_REGISTRY}/blog-backend:latest
# 3. 推送镜像
docker push ${DOCKER_REGISTRY}/blog-backend:${VERSION}
docker push ${DOCKER_REGISTRY}/blog-backend:latest
# 4. 部署到服务器
ssh user@server
cd /opt/blog-app
docker-compose -f docker-compose.prod.yml pull
docker-compose -f docker-compose.prod.yml up -d
# 5. 查看服务状态
docker-compose -f docker-compose.prod.yml ps
docker-compose -f docker-compose.prod.yml logs -f
9.3 更新与回滚
# 更新服务
docker-compose -f docker-compose.prod.yml up -d --no-deps backend
# 回滚到上一版本
docker-compose -f docker-compose.prod.yml rollback backend
# 查看更新历史
docker-compose -f docker-compose.prod.yml ps
第十章:性能优化
10.1 网络优化
-
使用
keepalive连接池 -
配置合理的缓冲区大小
-
启用压缩(gzip)
Nginx 配置
gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 1000;
10.2 存储优化
services:
mysql:
image: mysql:5.7
command: --default-authentication-plugin=mysql_native_password --innodb-buffer-pool-size=256M
10.3 缓存优化
-
合理设置 Redis 缓存时间
-
使用缓存预热
-
监控缓存命中率
查看 Redis 统计信息
docker exec blog-redis redis-cli info stats
本周总结
本周我们完成了一个完整的实战项目:
- 项目架构设计:前端 + 后端 + 数据库 + 缓存
- 多服务开发:Node.js API + Vue.js 前端
- 配置文件:MySQL、Redis、Nginx
- 环境配置:开发环境和生产环境
- 资源限制:CPU、内存限制
- 监控日志:日志轮转、健康检查
- 部署流程:构建、推送、部署
- 性能优化:网络、存储、缓存优化
练习作业:
- 完成整个项目的部署
- 配置健康检查和监控
- 实现蓝绿部署