fastapi部署之单机蓝绿不停服更新
背景
容器化管理,使用podman-compose 实际也支持docker-compose,部署服务是fastapi+nginx进行切换转发
整体通过switch.sh脚本来切换nginx 转发到不同的容器来实现不同版本的更新部署,也就是一份代码,通过fastapi-green fastapi-blue实现流量切换
项目目录
(api) ➜ demo git:(main) ✗ tree
.
├── Dockerfile
├── README.md
├── api
│ ├── README.md
│ ├── __pycache__
│ │ ├── gunicorn_conf.cpython-39.pyc
│ │ ├── gunicorn_conf.cpython-39.pyc.281472865685392
│ │ ├── main.cpython-311.pyc
│ │ └── main.cpython-39.pyc
│ ├── gunicorn_conf.py
│ ├── main.py
│ ├── pyproject.toml
│ ├── requirements.txt
│ ├── start.sh
│ └── uv.lock
├── nginx
│ ├── current.conf
│ ├── nginx.conf
│ └── upstreams
│ ├── blue.conf
│ └── green.conf
├── podman-compose.yml
├── scripts
│ └── switch.sh
├── static
├── switch.log
└── switch.py
switch.py \ switch.log这2个文件和项目无关,主要是实现观测切换是否存在异常的文件和日志.
镜像文件
对同样的代码实现2个不同的容器进行管理
# `podman-compose.yml`
version: '3.8'
services:
fastapi-blue:
build:
context: .
dockerfile: Dockerfile
image: fastapi:blue-${VERSION_BLUE:-latest}
container_name: fastapi-blue
environment:
- APP_COLOR=blue
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
expose:
- "8000"
fastapi-green:
build:
context: .
dockerfile: Dockerfile
image: fastapi:green-${VERSION_GREEN:-latest}
container_name: fastapi-green
environment:
- APP_COLOR=green
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
expose:
- "8000"
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "8000:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro,Z
- ./nginx/upstreams:/etc/nginx/upstreams:ro,Z
- ./nginx/current.conf:/etc/nginx/conf.d/upstream.conf:ro,Z
networks:
- app-network
user: "root"
networks:
app-network:
driver: bridge
# podman-compose 推荐显式指定网络名称(可选)
name: app-network
Dockerfile使用了python:3.13-slim和分段构建,降低了包体大小也提升了构建速度.
# Build stage
FROM python:3.13-slim as builder
WORKDIR /home/api
COPY api/requirements.txt /home/api/
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.13-slim
WORKDIR /home/api
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /install /usr/local
COPY /api /home/api/
RUN chmod +x /home/api/start.sh
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/home/api
CMD ["/home/api/start.sh"]
日常操作
实际在这样部署后 ,需要基于git进行版本管理,在代码更新后需要实时进行更新,并能通过容器的tag来进行版本的切换.
首次使用
第一次模型给容器的tag版本号都是一样的,PODMAN_NO_PULL=1 VERSION_GREEN=v1.0.0 podman-compose up -d fastapi-blue fastapi-green nginx,这样即可启动容器,不过第一次可能会慢一点(具体看你的网络)
(api) ➜ demo git:(main) ✗ podman-compose ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7e6c0be5bb5c localhost/fastapi:blue-latest /home/api/start.s... 17 seconds ago Up 17 seconds (healthy) 8000/tcp fastapi-blue
fddefaa91ccf localhost/fastapi:green-v1.0.0 /home/api/start.s... 17 seconds ago Up 17 seconds (healthy) 8000/tcp fastapi-green
0e54e1c14179 docker.io/library/nginx:alpine nginx -g daemon o... 16 seconds ago Up 17 seconds (starting) 0.0.0.0:8000->80/tcp nginx
现在我们确认成功后,在尝试对,可以自己在代码写一个固定的版本号和接口,来实现是否更新成功.
@app.get("/")
def read_root():
COLOR = os.getenv("APP_COLOR", "unknown")
HOSTNAME = socket.gethostname()
return {
"message": "Hello from FastAPI",
"color": COLOR,
"hostname": HOSTNAME,
"version": "1.0.0"
}
请求后接口返回如下
{"message":"Hello from FastAPI","color":"green","hostname":"fddefaa91ccf","version":"1.0.0"}
跑到这里就代表我们的容器和初始化全部完成
流量切换
因为我们实际是跑了2个容器的,所以会涉及fastapi-green和fastapi-blue2个容器,通过scripts/switch.sh即可实现切换.
#!/bin/bash
set -e
TARGET=$1
if [[ "$TARGET" != "blue" && "$TARGET" != "green" ]]; then
echo "Usage: $0 [blue|green]"
exit 1
fi
echo "Switching traffic to $TARGET..."
echo "Checking health of fastapi-$TARGET..."
if podman exec "fastapi-$TARGET" curl -s -f http://localhost:8000/health > /dev/null; then
echo "Health check passed."
else
echo "Health check FAILED for fastapi-$TARGET. Aborting switch."
exit 1
fi
CONFIG_PATH="./nginx/current.conf"
SOURCE_CONFIG="./nginx/upstreams/$TARGET.conf"
if [ ! -f "$SOURCE_CONFIG" ]; then
echo "Error: Configuration file $SOURCE_CONFIG not found."
exit 1
fi
echo "Updating Nginx configuration..."
cp "$SOURCE_CONFIG" "$CONFIG_PATH"
echo "Reloading Nginx..."
podman exec nginx nginx -s reload
echo "Successfully switched to $TARGET."
执行如下命令后 在请求接口,可以看到流量从green容器切换到blue容器
(api) ➜ demo git:(main) ✗ bash scripts/switch.sh blue
Switching traffic to blue...
Checking health of fastapi-blue...
Health check passed.
Updating Nginx configuration...
Reloading Nginx...
2026/02/24 05:53:33 [notice] 33#33: signal process started
Successfully switched to blue.
请求后接口返回
{"message":"Hello from FastAPI","color":"blue","hostname":"fddefaa91ccf","version":"1.0.0"}
更新版本
这里会比较复杂,因为是单机部署,且没有接入jenkins、k8s等自动化工具,所以对镜像的拉取、版本更新仍需要手动操作
现在我们的容器流量在green上,我们可以先对代码修改,同时更新blue容器的环境
更新blue容器代码,假设已经完成了代码变更.
PODMAN_NO_PULL=1 VERSION_BLUE=v1.0.2 podman-compose up -d fastapi-blue
这里要注意VERSION_BLUE的版本号代表打包以后的镜像版本,
执行后确认打包成功后在执行流量切换后访问接口则能看到新的版本号已经返回
(api) ➜ demo git:(main) ✗ bash scripts/switch.sh blue
Switching traffic to blue...
Checking health of fastapi-blue...
Health check passed.
Updating Nginx configuration...
Reloading Nginx...
2026/02/24 05:58:42 [notice] 42#42: signal process started
{"message":"Hello from FastAPI","color":"blue","hostname":"1e7171b9b528","version":"1.0.2"}
按照上面的逻辑即可实现一个手动操作控制的不停服,通过单机实现的蓝绿更新服务.
接下来可以完善的点
+ 没有镜像会滚机制,还需要新增rollback.sh工具,实现代码出错,快速回滚的机制
+ 历史镜像堆积问题,每次更新后,历史镜像并没有清除.