Docker 实战部署:从本地镜像到云服务器,一篇走通 FastAPI + Celery + MySQL + Redis + Nginx
一、前言
最近在部署自己的一个实际项目时,我把原本散落在本地开发环境中的 FastAPI、Celery、MySQL、Redis 等服务逐步 Docker 化,并最终部署到了云服务器。
整个过程走下来,我发现 Docker 部署真正麻烦的地方并不是记住几个 docker 命令,而是理解下面这条完整链路:
本地开发
↓
Docker Build
↓
Docker Image
↓
镜像仓库
↓
云服务器 Pull
↓
Docker Compose
↓
FastAPI / Celery / MySQL / Redis
↓
Nginx
↓
域名
↓
HTTPS
尤其第一次真正把一个多服务项目部署到服务器时,很容易遇到这些问题:
- Docker 镜像到底是什么?
- 本地已经 Build 成功,怎么放到服务器?
- 要不要把源码传到服务器重新 Build?
- Docker Compose 到服务器以后应该怎么改?
- MySQL 和 Redis 为什么不能写 localhost?
- 为什么服务器执行 docker compose up –build 特别慢?
- Docker 服务运行在 8000 端口,怎么绑定自己的域名?
- 数据库容器重新创建以后,数据会不会全部消失?
- Docker 镜像仓库到底解决了什么问题?
本文就根据一次真实部署过程,把这些问题完整串起来。
本文项目使用的主要技术栈如下:
Backend
├── Python 3.12
├── FastAPI
├── Uvicorn
├── SQLAlchemy
├── Alembic
└── Celery
Infrastructure
├── MySQL 8.4
├── Redis 7.4
├── Docker
├── Docker Compose
└── Nginx
Registry
└── Huawei Cloud SWR
最终希望达到的效果非常简单:
本地开发完成后构建 Docker 镜像,将镜像推送到镜像仓库,服务器只需要 Pull 镜像并启动,不再手动安装 Python、项目依赖、Celery 等运行环境。
二、为什么这次决定使用 Docker?
如果不用 Docker,一个 Python 项目部署到服务器,大概需要经历:
安装 Python
↓
创建虚拟环境
↓
安装 pip 依赖
↓
配置 MySQL
↓
安装 Redis
↓
配置 FastAPI
↓
配置 Celery Worker
↓
配置 Celery Beat
↓
使用 systemd / Supervisor 守护进程
↓
配置 Nginx
如果只有一个 FastAPI 服务还好。
但我的项目实际上已经包含多个进程:
API
Worker
Dispatcher
Scheduler
Migration
MySQL
Redis
如果全部直接部署在 Linux 上,就需要分别考虑每个服务如何启动、停止、重启以及开机自启。
而 Docker Compose 可以把这些东西统一起来:
docker compose up -d
查看状态:
docker compose ps
查看日志:
docker compose logs -f
停止:
docker compose down
这也是我最终决定将整个项目 Docker 化的重要原因。
三、项目的 Docker Compose 架构
项目不是单容器结构,而是一个典型的多服务应用。
整体关系大致如下:
┌─────────────────┐
│ Nginx │
│ 80 / 443 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ FastAPI │
│ :8000 │
└────────┬────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ MySQL │ │ Redis │
│ :3306 │ │ :6379 │
└───────────────┘ └───────┬───────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Worker Dispatcher Scheduler
本地使用的 Compose 核心结构如下:
services:
api:
image: ${SERVER_IMAGE:–writing–assistant–server:latest}
build:
context: ..
dockerfile: server/Dockerfile
env_file: .env
command: uvicorn backend.main:app ––host 0.0.0.0 ––port 8000
depends_on:
migrate:
condition: service_completed_successfully
ports:
– "8000:8000"
volumes:
– server–storage:/data/storage
restart: unless–stopped
worker:
image: ${SERVER_IMAGE:–writing–assistant–server:latest}
env_file: .env
command: >
celery -A backend.workers.celery_app:celery_app
worker –loglevel=INFO –concurrency=1 -Q publish,celery
depends_on:
migrate:
condition: service_completed_successfully
redis:
condition: service_healthy
shm_size: 1gb
init: true
volumes:
– server–storage:/data/storage
restart: unless–stopped
dispatcher:
image: ${SERVER_IMAGE:–writing–assistant–server:latest}
env_file: .env
command: >
celery -A backend.workers.celery_app:celery_app
worker –loglevel=INFO –concurrency=1 -Q scheduler
depends_on:
migrate:
condition: service_completed_successfully
redis:
condition: service_healthy
restart: unless–stopped
scheduler:
image: ${SERVER_IMAGE:–writing–assistant–server:latest}
env_file: .env
command: >
celery -A backend.workers.celery_app:celery_app
beat –loglevel=INFO
depends_on:
migrate:
condition: service_completed_successfully
redis:
condition: service_healthy
volumes:
– server–storage:/data/storage
restart: unless–stopped
migrate:
image: ${SERVER_IMAGE:–writing–assistant–server:latest}
env_file: .env
command: alembic upgrade head
depends_on:
mysql:
condition: service_healthy
restart: "no"
mysql:
image: mysql:8.4
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
– mysql–data:/var/lib/mysql
healthcheck:
test:
[
"CMD-SHELL",
"mysqladmin ping -h localhost -uroot -p$$MYSQL_ROOT_PASSWORD || exit 1"
]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
restart: unless–stopped
redis:
image: redis:7.4–alpine
command: redis–server ––appendonly yes
volumes:
– redis–data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 12
restart: unless–stopped
volumes:
mysql-data:
redis-data:
server-storage:
这里有一个很重要的设计:
api
worker
dispatcher
scheduler
migrate
虽然是五个不同的服务,但实际上完全可以使用同一个业务 Docker Image。
区别只是每个 Container 启动时执行的 command 不一样。
例如:
writing-assistant-server
│
├── uvicorn → API
├── celery worker → Worker
├── celery worker -Q scheduler → Dispatcher
├── celery beat → Scheduler
└── alembic upgrade head → Migration
这也是 Docker 中非常常见的一种设计。
四、本地构建 Docker 镜像
本地执行:
docker compose build
或者:
docker compose up –build -d
构建完成以后:
docker images
可以看到类似:
REPOSITORY TAG IMAGE ID SIZE
writing-assistant-server latest 05f630b4a28e 3.23GB
mysql 8.4 0744ee5ef89c 1.1GB
redis 7.4-alpine 858f009f9709 58.2MB
其中:
writing-assistant-server:latest
就是我们自己的业务镜像。
这里需要注意:
Docker Image 和 Docker Container 并不是同一个概念。
可以简单理解为:
Dockerfile
↓
docker build
↓
Image
↓
docker run
↓
Container
Image 更像一个只读模板。
Container 则是 Image 的运行实例。
同一个 Image 完全可以启动多个 Container。
这也是为什么前面的:
API
Worker
Dispatcher
Scheduler
Migration
可以共用 writing-assistant-server。
五、本地镜像怎么传到服务器?
这里主要有三种方案。
5.1 方案一:使用 Docker Registry
这是我最终采用的方式。
流程:
Mac / 开发机
↓
docker build
↓
Docker Image
↓
docker push
↓
Huawei Cloud SWR
↓
docker pull
↓
Production Server
例如镜像仓库地址:
swr.cn-east-3.myhuaweicloud.com/example/writing-assistant-server
服务器可以直接:
docker pull \\
swr.cn-east-3.myhuaweicloud.com/example/writing-assistant-server:latest
生产环境更推荐使用明确版本:
docker pull \\
swr.cn-east-3.myhuaweicloud.com/example/writing-assistant-server:v1.0.0
5.2 方案二:docker save + SCP
Docker 镜像并不是必须上传 Registry。
可以直接导出:
docker save writing-assistant-server:latest \\
| gzip > writing-assistant-server.tar.gz
然后:
scp writing-assistant-server.tar.gz \\
root@SERVER_IP:/opt/writing-assistant/
服务器导入:
gunzip -c writing-assistant-server.tar.gz | docker load
然后:
docker images
即可看到:
writing-assistant-server latest
这种方式比较适合:
- 临时部署
- 内网服务器
- 无镜像仓库环境
- 偶尔发布
但如果频繁发布,Registry 会舒服很多。
5.3 方案三:服务器直接 Build
还有一种最直接的方式:
git clone ...
cd project
docker compose up –build -d
也就是:
Git Repository
↓
Production Server
↓
Docker Build
↓
Docker Image
↓
Container
虽然能用,但这次部署过程中我发现:
对正式服务器来说,这通常不是最理想的方式。
原因后面会详细讲。
六、服务器 Compose 和本地 Compose 不应该完全一样
本地开发时需要:
build:
context: ..
dockerfile: server/Dockerfile
因为本地需要从源码 Build。
但是服务器如果已经从 Registry 获取构建好的 Image,就没有必要再 Build。
因此生产 Compose 更推荐:
services:
api:
image: ${SERVER_IMAGE}
env_file: .env
command: uvicorn backend.main:app ––host 0.0.0.0 ––port 8000
.env:
SERVER_IMAGE=swr.cn-east-3.myhuaweicloud.com/example/writing-assistant-server:v1.0.0
然后:
docker compose pull
docker compose up -d
这两个命令非常关键。
正确的生产部署链路应该是:
开发机 / CI
│
│ Build
▼
Docker Image
│
│ Push
▼
Registry
│
│ Pull
▼
Production
│
│ Run
▼
Container
而不是:
Production
↓
Clone Source
↓
Download Dependencies
↓
Build
↓
Run
换句话说:
Build 和 Run 最好分开。
七、为什么我在服务器执行 Docker Build 慢得离谱?
这次实际部署时,我直接在服务器执行了:
docker compose up –build -d
结果发现 Build 非常慢。
日志中出现:
[5/8] RUN pip install …
772.5s
一个步骤就执行了 772 秒。
进一步看:
4.5/4.5 MB 17.9 kB/s 0:04:06
一个只有 4.5MB 左右的 Python 包,竟然下载了 4 分钟。
这时候问题其实已经非常明显:
不是 Docker Build 本身慢,而是 pip 下载 Python Dependencies 太慢。
整个过程其实是:
docker build
↓
python:3.12-bookworm
↓
pip install
↓
PyPI
↓
网络速度极慢
↓
整个 Docker Build 被阻塞
这也是为什么:
Docker Build ≠ 单纯编译代码
Dockerfile 中所有:
RUN …
都会真实执行。
如果里面有:
RUN pip install …
就需要真实访问 PyPI 下载依赖。
八、为什么生产服务器最好不要每次重新 Build?
这次经历以后,这个问题就非常直观了。
假设服务器每次发布都:
docker compose up –build -d
那么服务器需要承担:
拉基础镜像
下载系统依赖
下载 Python 依赖
构建镜像
占用 CPU
占用 RAM
占用 Disk
如果服务器网络访问 PyPI 又比较慢,部署一次可能十几分钟甚至几十分钟。
而如果提前 Build:
开发机
↓
Build Once
↓
Registry
↓
Server Pull
服务器只负责:
下载 Image
启动 Container
这也是容器镜像非常重要的价值:
把应用程序和运行环境一起打包成一个可分发的部署产物。
九、Docker Build Cache 也非常重要
Docker 并不是每次都从头 Build。
例如 Dockerfile:
COPY server/pyproject.toml /workspace/server/pyproject.toml
RUN pip install …
只要:
pyproject.toml
没有发生变化,Docker 就可能直接复用之前的 Layer:
CACHED
流程相当于:
pyproject.toml 没变
↓
依赖没有变化
↓
pip install Layer 继续使用
↓
只重新 COPY 新代码
但是如果修改:
pyproject.toml
这一层 Cache 就会失效:
pyproject.toml changed
↓
pip install cache invalid
↓
重新安装全部 dependencies
因此 Dockerfile 的 Layer 顺序实际上也会直接影响构建效率。
十、Docker Compose 内部为什么不能乱写 localhost?
这是第一次部署 Docker 多服务项目非常容易踩的坑。
假设 FastAPI 的数据库配置写:
MYSQL_HOST=localhost
在 Docker Compose 中往往是错的。
因为:
FastAPI Container
里面的:
localhost
代表的是:
FastAPI Container 自己
而不是 MySQL Container。
正确结构:
FastAPI Container
│
│ mysql:3306
▼
MySQL Container
因此应该:
MYSQL_HOST=mysql
MYSQL_PORT=3306
Redis 同理:
REDIS_HOST=redis
REDIS_PORT=6379
因为 Compose 中:
services:
mysql:
…
redis:
…
服务名本身就可以作为内部 DNS Hostname。
因此:
mysql:3306
redis:6379
就是容器之间的通信地址。
十一、depends_on 不等于服务真正可用了
例如:
depends_on:
mysql:
condition: service_healthy
为什么需要:
service_healthy
而不是单纯“先启动 MySQL”?
因为:
Container Started
并不意味着:
MySQL Ready
MySQL 容器启动后还需要初始化数据库、用户、权限等。
因此应该设置 Health Check:
healthcheck:
test:
[
"CMD-SHELL",
"mysqladmin ping -h localhost -uroot -p$$MYSQL_ROOT_PASSWORD || exit 1"
]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
这样整个启动链路可以变成:
MySQL Start
↓
MySQL Health Check
↓
Healthy
↓
Alembic Migration
↓
Migration Success
↓
FastAPI / Worker / Scheduler Start
比所有服务同时启动稳定很多。
十二、为什么 Migration 单独做一个 Container?
我的 Compose 中还有一个:
migrate:
image: ${SERVER_IMAGE}
command: alembic upgrade head
它专门执行:
alembic upgrade head
成功后直接退出。
因此看到:
migrate Exited (0)
并不是错误。
反而意味着:
Migration completed successfully
真正有问题的是:
Exited (1)
这时候可以:
docker compose logs migrate
查看数据库 Migration 为什么失败。
这种设计还有一个好处:
应用启动和数据库结构升级之间存在明确顺序。
不会出现:
FastAPI 已经启动
↓
开始接受请求
↓
数据库表还没升级
↓
SQL Error
十三、Volume:容器删了为什么数据还在?
数据库绝对不能直接把重要数据存在 Container 可写层里。
因此 Compose 中使用:
volumes:
– mysql–data:/var/lib/mysql
Redis:
volumes:
– redis–data:/data
业务文件:
volumes:
– server–storage:/data/storage
底部声明:
volumes:
mysql-data:
redis-data:
server-storage:
整个关系:
Container
│
│ Mount
▼
Docker Volume
所以:
Container
可以被删除、更新、重新创建
Volume
继续保存数据
这就是为什么:
docker compose pull
docker compose up -d
重新创建业务 Container 时,数据库不会因此自动清空。
但是一定要注意:
docker compose down -v
其中:
-v
代表同时删除相关 Volume。
生产环境执行这个命令之前一定要确认自己到底在做什么。
同时:
Volume 不是 Backup。
生产数据库仍然需要:
mysqldump
↓
Backup File
↓
Object Storage / Other Server
否则服务器磁盘损坏以后,Volume 一样救不了数据。
十四、FastAPI 运行在 8000,怎么绑定域名?
Docker 启动以后:
ports:
– "8000:8000"
意味着:
Server :8000
↓
Container :8000
理论上可以:
http://SERVER_IP:8000
直接访问。
但是生产环境一般不建议这么做。
更推荐:
Internet
↓
Domain
↓
Nginx :80 / :443
↓
127.0.0.1:8000
↓
Docker
↓
FastAPI :8000
因此 Compose 可以改成:
ports:
– "127.0.0.1:8000:8000"
这里非常关键。
它意味着:
127.0.0.1:8000
只监听服务器本机。
外部用户无法直接访问:
SERVER_IP:8000
只能通过 Nginx。
十五、配置 Nginx 反向代理
假设域名:
api.example.com
FastAPI:
127.0.0.1:8000
Nginx 可以:
server {
listen 80;
listen [::]:80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
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 300s;
proxy_read_timeout 300s;
}
}
检查:
nginx -t
如果输出:
syntax is ok
test is successful
重新加载:
systemctl reload nginx
最终:
http://api.example.com
↓
Nginx
↓
127.0.0.1:8000
↓
Docker
↓
FastAPI
十六、为什么 MySQL 和 Redis 不应该暴露公网?
Compose 中我没有:
ports:
– "3306:3306"
也没有:
ports:
– "6379:6379"
这是故意的。
因为 FastAPI、Celery、MySQL、Redis 都处于 Compose Network 中。
它们可以直接:
FastAPI → mysql:3306
FastAPI → redis:6379
Worker → redis:6379
因此根本不需要:
Internet
↓
3306
Internet
↓
6379
公网真正需要暴露的通常只有:
80
443
甚至 API 的:
8000
都只监听:
127.0.0.1
这样整体暴露面会小很多。
十七、再进一步:配置 HTTPS
Nginx 反向代理完成以后,还可以使用 Let’s Encrypt + Certbot。
Ubuntu:
sudo apt install certbot python3-certbot-nginx -y
申请:
sudo certbot –nginx -d api.example.com
最终访问:
https://api.example.com
整个生产链路就变成:
Internet
│
│ HTTPS :443
▼
┌─────────────┐
│ Nginx │
└──────┬──────┘
│
127.0.0.1:8000
│
▼
┌─────────────┐
│ FastAPI │
└──────┬──────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ MySQL │ │ Redis │
└─────────┘ └────┬────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
Worker Dispatcher Scheduler
十八、以后发布新版本应该怎么做?
假设:
v1.0.0
已经在线。
开发完成:
v1.0.1
在开发机或 CI 构建:
docker build -t \\
swr.cn-east-3.myhuaweicloud.com/example/writing-assistant-server:v1.0.1 \\
.
Push:
docker push \\
swr.cn-east-3.myhuaweicloud.com/example/writing-assistant-server:v1.0.1
服务器修改:
SERVER_IMAGE=swr.cn-east-3.myhuaweicloud.com/example/writing-assistant-server:v1.0.1
然后:
docker compose pull
docker compose up -d
完成更新。
如果:
v1.0.1
发现严重 Bug,可以改回:
SERVER_IMAGE=swr.cn-east-3.myhuaweicloud.com/example/writing-assistant-server:v1.0.0
重新:
docker compose up -d
这也是为什么生产环境我更推荐:
v1.0.0
v1.0.1
v1.1.0
而不是永远:
latest
明确版本号会让部署记录和回滚清晰很多。
十九、Docker 部署到底有什么优势?
经过这次完整部署以后,我认为 Docker 对这种项目最大的价值不是“看起来专业”,而是下面几点。
19.1 环境一致
开发环境已经运行成功的 Image:
Python
Python Dependencies
System Dependencies
Application
整体搬到服务器。
减少:
本地能跑
服务器不能跑
这种经典问题。
19.2 多服务管理简单
原来:
FastAPI
Celery Worker
Celery Beat
Redis
MySQL
可能需要多个 systemd / Supervisor 配置。
Docker Compose 后统一:
docker compose up -d
19.3 部署产物标准化
以前交付:
Source Code
README
Python Version
requirements
Linux Dependency
Startup Script
现在交付:
Docker Image
部署目标变得更加明确。
19.4 回滚方便
v1.0.0
↓
v1.0.1
↓
出现问题
↓
v1.0.0
不需要重新修改服务器代码。
19.5 更容易做 CI/CD
以后完全可以:
Git Push
↓
GitHub Actions / GitLab CI
↓
Docker Build
↓
Docker Push
↓
Deployment
开发完成以后自动完成镜像构建。
二十、Docker 也不是没有缺点
这次部署过程中也能明显感受到一些问题。
首先是磁盘。
例如业务镜像:
3.23GB
MySQL:
1.1GB
随着版本越来越多:
v1.0.0
v1.0.1
v1.0.2
v1.1.0
服务器可能留下很多旧 Image。
可以查看:
docker system df
清理不用的 Image:
docker image prune
其次是排错复杂度。
以前:
Application
↓
Linux
现在:
Application
↓
Container
↓
Docker Network
↓
Volume
↓
Docker Engine
↓
Linux
因此需要理解:
Image
Container
Network
Volume
Port Mapping
Health Check
Compose
这些概念。
但对于多服务项目来说,这部分学习成本通常是值得的。
二十一、这次部署以后,我对 Docker 的理解发生了什么变化?
以前学习 Docker 时,很容易把重点放在:
docker build
docker run
docker ps
docker exec
docker logs
这些命令本身。
真正做完一次完整部署以后,我认为 Docker 最重要的其实不是这些命令,而是:
把应用程序构建成一个标准化、可重复部署、可版本化的运行产物。
完整的软件交付过程应该逐渐变成:
Development
│
▼
Source
│
▼
Docker Build
│
▼
Docker Image
│
▼
Registry
│
▼
Production
│
▼
Docker Compose
│
┌─────────┼─────────┐
▼ ▼ ▼
API Worker Scheduler
│ │
└─────────┬──────────┘
▼
MySQL / Redis
│
▼
Nginx
│
▼
Domain / HTTPS
这才是 Docker 真正在项目部署中的价值。
二十二、下一步:为什么我开始想自己做一个 DevOps 平台?
当这套流程真正跑通以后,我又发现一个新的问题。
虽然 Docker 已经方便很多,但每部署一个项目,我还是需要:
ssh root@server
docker login ...
docker pull ...
nano docker-compose.yml
docker compose up -d
docker compose ps
docker compose logs -f
nano /etc/nginx/...
nginx -t
systemctl reload nginx
certbot ...
一个项目还好。
如果以后有:
Server A
Server B
Server C
Project A
Project B
Project C
Project D
Project E
这些操作就会开始重复。
于是我产生了一个新的想法:
给自己开发一个轻量级 DevOps + Server Management 平台。
平台不需要一开始就在所有服务器安装 Agent。
第一阶段完全可以:
DevOps Platform
│
│ SSH
┌────────────┼────────────┐
▼ ▼ ▼
Server A Server B Server C
│ │ │
Docker Docker Docker
只需要服务器支持:
Linux
SSH
Docker
平台就可以完成:
服务器资源监控
Docker Container 管理
Docker Image 管理
Docker Compose 部署
项目管理
环境变量管理
域名管理
Nginx 配置
HTTPS
日志查看
版本发布
版本回滚
最终希望达到:
创建应用
应用:
Writing Assistant
镜像:
writing-assistant-server:v1.0.0
服务器:
Production-01
端口:
8000
域名:
api.example.com
HTTPS:
开启
环境变量:
********
[部署]
点击部署以后:
检查服务器 Success
检查 Docker Success
Pull Image Success
Run Migration Success
Start Container Success
Health Check Success
Create Nginx Config Success
Reload Nginx Success
Configure HTTPS Success
Deployment Successful
从:
“我会使用 Docker 部署项目。”
进一步走向:
“我把自己的整个部署流程平台化。”
这也是我接下来准备继续实践的方向。
二十三、总结
这次实际部署以后,整个思路已经非常清晰。
开发阶段:
Source Code
↓
Docker Build
↓
Local Test
发布阶段:
Docker Image
↓
Registry
↓
Version Management
生产阶段:
docker compose pull
↓
docker compose up -d
↓
Health Check
流量入口:
Domain
↓
HTTPS
↓
Nginx
↓
127.0.0.1:8000
↓
Docker
↓
FastAPI
内部服务:
FastAPI
├── mysql:3306
└── redis:6379
Celery
└── redis:6379
数据:
Container
↓
Docker Volume
↓
Persistent Data
↓
Regular Backup
真正理解这些关系以后,再看 Docker 就不会只是:
docker run
docker ps
docker logs
几个零散命令了。
它真正解决的是软件工程中一个非常实际的问题:
如何把一个在开发环境运行正常的复杂应用,以可重复、可管理、可版本化的方式交付到生产环境。
而当部署项目越来越多以后,下一个值得解决的问题,也就自然从“怎么部署”变成了:
怎么把部署本身自动化、标准化、平台化。
这也是我下一阶段准备继续折腾的方向。


