IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
从第 1 篇搭建环境到现在,我们一路学完了镜像、容器、Dockerfile、数据卷、网络。但说实话------前面的知识都是"散装"的。你学会了怎么单独操作容器,但还没有真正把一整个应用"端到端"地容器化过。
这篇就是来补这一课的。我们将把贯穿全系列的 Flask + Redis 计数器应用 从头到尾完整容器化,并且不依赖 Docker Compose ------用纯 Docker 命令完成网络创建、数据卷挂载、多容器启动和验证。这不仅是对前 9 篇的系统性综合实战,更是为接下来进入 Docker Compose 以及 Kubernetes 编排世界打下最坚实的基础。当你手动完成一次完整的容器化部署后,你才能真正理解 Compose 的 docker-compose.yml 里每一行在背后帮你做了什么,也才能真正理解 Kubernetes 的 Service 和 Deployment 在解决什么痛点。
一、回顾:我们走到了哪里?
在动工之前,快速回顾一下前 9 篇积累的核心能力:
现在,是时候把它们全部串联起来了。
二、项目全景:我们要交付什么?
我们最终交付的是一个可运行的多组件应用栈:
bash
┌─────────────────────────────────────────────────────┐
│ 宿主机 (localhost) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Flask 容器 │──────▶│ Redis 容器 │ │
│ │ (端口 5000) │ DNS │ (端口 6379) │ │
│ │ │ 解析 │ │ │
│ │ Volume: │ │ Volume: │ │
│ │ flask-logs │ │ redis-data │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ ┌──────▼───────────────────────▼───────────┐ │
│ │ 自定义网络: app-net │ │
│ │ 内置 DNS: 127.0.0.11 │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
2.1 项目结构
先看一眼最终的项目目录------一个标准的 Docker 化项目通常就包含这些文件:
bash
flask-redis-counter/
├── app.py # Flask 应用主程序
├── requirements.txt # Python 依赖清单
├── Dockerfile # 生产级多阶段构建
├── .dockerignore # 构建排除文件
└── start.sh # 一键启动脚本(本节新增)
2.2 应用代码回顾
以下是我们打磨了两个版本后的最终代码。新增了 /health 健康检查端点(配合 HEALTHCHECK 指令使用,第 6 篇已详解)和 /logs 日志查看端点(方便验证 Volume 持久化效果)。
bash
import time
import os
import redis
from flask import Flask
app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379, decode_responses=True)
def get_hit_count():
retries = 5
while True:
try:
return cache.incr('hits')
except redis.exceptions.ConnectionError as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)
@app.route('/')
def hello():
count = get_hit_count()
return f'Hello World! I have been seen {count} times.\n'
@app.route('/health')
def health():
"""K8s 探针就靠这个端点"""
return {'status': 'ok'}
@app.route('/logs')
def view_logs():
"""查看访问日志(验证 Volume 持久化)"""
log_dir = '/app/logs'
if not os.path.exists(log_dir):
return {'error': 'logs directory not found'}, 404
files = os.listdir(log_dir)
return {'files': files, 'count': len(files)}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
requirements.txt:
bash
flask==3.1.1
redis==6.4.0
2.3 Dockerfile(沿用第 5 篇优化版)
bash
# syntax=docker/dockerfile:1
# ============================================================
# Flask + Redis 计数器应用 ------ 生产级多阶段 Dockerfile
# 系列贯穿案例 v2.0
# ============================================================
# ---- 阶段 1:Builder ----
FROM python:3.12-slim AS builder
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc python3-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# ---- 阶段 2:Runtime ----
FROM python:3.12-slim
LABEL maintainer="IT策士" \
description="Flask + Redis 计数器应用(贯穿案例 v2.0)" \
version="2.0"
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
RUN groupadd -r appuser && \
useradd -r -m -u 1000 -g appuser appuser
WORKDIR /app
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt && \
rm -rf /wheels requirements.txt
# 创建日志目录
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
CMD ["python", "app.py"]
变更说明 :相比第 5 篇,此处在 pip install 之后新增了 mkdir -p /app/logs 并调整权限,确保日志目录存在且可写。这是因为后续我们会将 flask-logs Volume 挂载到此路径,如果目录不存在,Docker 会自动创建但权限为 root,导致 appuser 无法写入。
2.4 .dockerignore
bash
__pycache__
*.pyc
*.pyo
*.log
.env
.git
.gitignore
*.md
.vscode
.idea
venv
.venv
*.tar
*.gz
Dockerfile
.dockerignore
三、Step by Step:手动启动全套应用
Step 1:构建镜像
bash
cd flask-redis-counter
docker build -t flask-redis-counter:2.0 .
输出关键行:
bash
[+] Building 42.3s (17/17) FINISHED
=> [builder 1/4] FROM python:3.12-slim 0.0s
=> [builder 2/4] WORKDIR /build 0.1s
=> [builder 3/4] RUN apt-get update && ... 14.2s
=> [builder 4/4] RUN pip wheel --no-cache-dir ... 9.5s
=> [runtime 1/9] FROM python:3.12-slim 0.0s
=> [runtime 2/9] RUN groupadd -r appuser && ... 0.4s
=> [runtime 3/9] WORKDIR /app 0.0s
=> [runtime 4/9] COPY --from=builder /wheels /wheels 0.2s
=> [runtime 5/9] COPY requirements.txt . 0.1s
=> [runtime 6/9] RUN pip install --no-index ... 3.8s
=> [runtime 7/9] RUN mkdir -p /app/logs && chown ... 0.3s
=> [runtime 8/9] COPY --chown=appuser:appuser . . 0.1s
=> [runtime 9/9] USER appuser 0.0s
=> exporting to image 2.1s
=> => naming to docker.io/library/flask-redis-counter:2.0 0.0s
bash
# 确认镜像
docker images flask-redis-counter
# REPOSITORY TAG IMAGE ID SIZE
# flask-redis-counter 2.0 b2c3d4e5f6a7 138MB
Step 2:创建自定义网络
bash
docker network create app-net
Step 3:创建命名卷(数据持久化)
bash
docker volume create redis-data
docker volume create flask-logs
Step 4:启动 Redis 容器
bash
docker run -d \
--name redis \
--network app-net \
--restart=unless-stopped \
-v redis-data:/data \
redis:alpine redis-server --appendonly yes
参数回顾:
Step 5:启动 Flask 容器
bash
docker run -d \
--name flask-app \
--network app-net \
--restart=unless-stopped \
-p 5000:5000 \
-v flask-logs:/app/logs \
flask-redis-counter:2.0
Step 6:验证整体状态
bash
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
输出:
bash
NAMES STATUS PORTS
flask-app Up 10 seconds (healthy) 0.0.0.0:5000->5000/tcp
redis Up 30 seconds 6379/tcp
(healthy) 标记说明 Flask 容器内的 HEALTHCHECK 命令已通过验证。
bash
# 检查网络连通性
docker exec flask-app ping -c 2 redis
# 64 bytes from redis.app-net (172.18.0.2): seq=0 ttl=64 time=0.1ms
# 64 bytes from redis.app-net (172.18.0.2): seq=1 ttl=64 time=0.05ms
redis.app-net 是 Docker DNS 自动生成的全限定域名(容器名.网络名),ping 输出证明了容器名解析和网络层双向通信均正常。
Step 7:功能测试
bash
# 测试计数器
curl http://localhost:5000
# Hello World! I have been seen 1 times.
curl http://localhost:5000
# Hello World! I have been seen 2 times.
# 测试健康检查端点
curl http://localhost:5000/health
# {"status":"ok"}
# 测试日志端点(确认 Volume 已正确挂载)
curl http://localhost:5000/logs
# {"count":0,"files":[]}
四、持久化验证:数据卷的真正价值
4.1 Redis 数据持久化验证
bash
# 查看当前计数
curl -s http://localhost:5000
# Hello World! I have been seen 3 times.
# 强制删除 Redis 容器(模拟灾难)
docker rm -f redis
# 重新创建 Redis 容器(使用同一个 Volume)
docker run -d \
--name redis \
--network app-net \
--restart=unless-stopped \
-v redis-data:/data \
redis:alpine redis-server --appendonly yes
# 等待 Redis 和 Flask 重新连接后验证
sleep 5
curl http://localhost:5000
# Hello World! I have been seen 4 times. ← 计数没有归零!
这个演示就是第 7 篇学到的 Volume 持久化的直观体现------Redis 的 AOF 文件存储在 redis-data 卷中,容器被销毁不影响数据。新容器挂载同一个卷,Redis 启动时自动从 AOF 文件中恢复所有键值对,计数器无缝衔接。
4.2 日志 Volume 验证
bash
# 查看日志卷的宿主机路径
docker volume inspect flask-logs
# "Mountpoint": "/var/lib/docker/volumes/flask-logs/_data"
# Flask 应用可以在 /app/logs 目录写入日志文件
docker exec flask-app touch /app/logs/access.log
curl http://localhost:5000/logs
# {"count":1,"files":["access.log"]}
即使 Flask 容器被删除重建,挂载同一个 flask-logs 卷即可恢复所有历史日志。
五、一键启动脚本:从手动到自动化
每次都要敲七八条命令太麻烦了。我们把整个流程写成一个脚本,实现一键启停:
bash
#!/bin/bash
set -e
NETWORK_NAME="app-net"
REDIS_VOLUME="redis-data"
FLASK_LOGS_VOLUME="flask-logs"
REDIS_CONTAINER="redis"
FLASK_CONTAINER="flask-app"
IMAGE="flask-redis-counter:2.0"
echo "=== 1. 创建网络(如已存在则跳过) ==="
docker network create $NETWORK_NAME 2>/dev/null || echo "网络 $NETWORK_NAME 已存在"
echo "=== 2. 创建数据卷(如已存在则跳过) ==="
docker volume create $REDIS_VOLUME 2>/dev/null || echo "卷 $REDIS_VOLUME 已存在"
docker volume create $FLASK_LOGS_VOLUME 2>/dev/null || echo "卷 $FLASK_LOGS_VOLUME 已存在"
echo "=== 3. 清理旧容器 ==="
docker rm -f $REDIS_CONTAINER $FLASK_CONTAINER 2>/dev/null || true
echo "=== 4. 启动 Redis ==="
docker run -d \
--name $REDIS_CONTAINER \
--network $NETWORK_NAME \
--restart=unless-stopped \
-v $REDIS_VOLUME:/data \
redis:alpine redis-server --appendonly yes
echo "=== 5. 等待 Redis 就绪 ==="
sleep 2
echo "=== 6. 启动 Flask 应用 ==="
docker run -d \
--name $FLASK_CONTAINER \
--network $NETWORK_NAME \
--restart=unless-stopped \
-p 5000:5000 \
-v $FLASK_LOGS_VOLUME:/app/logs \
$IMAGE
echo "=== 7. 等待应用健康检查通过 ==="
sleep 5
echo "=== 8. 状态检查 ==="
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo ""
echo "=== 部署完成! ==="
echo "访问地址: http://localhost:5000"
echo "健康检查: http://localhost:5000/health"
赋予执行权限并运行:
bash
chmod +x start.sh
./start.sh
输出:
bash
=== 1. 创建网络(如已存在则跳过) ===
app-net
=== 2. 创建数据卷(如已存在则跳过) ===
redis-data
flask-logs
=== 3. 清理旧容器 ===
=== 4. 启动 Redis ===
b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0
=== 5. 等待 Redis 就绪 ===
=== 6. 启动 Flask 应用 ===
c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1
=== 7. 等待应用健康检查通过 ===
=== 8. 状态检查 ===
NAMES STATUS PORTS
flask-app Up 5 seconds (healthy) 0.0.0.0:5000->5000/tcp
redis Up 10 seconds 6379/tcp
=== 部署完成! ===
访问地址: http://localhost:5000
健康检查: http://localhost:5000/health
现在,整个应用栈从零到全功能运行,只需一条 ./start.sh。
六、推送镜像到 Docker Hub
让其他人也能使用你的镜像,需要推送到镜像仓库。这里以 Docker Hub 为例:
6.1 注册并登录 Docker Hub
如果你还没有 Docker Hub 账号,先去 hub.docker.com 免费注册一个。然后在终端登录:
bash
docker login
# Username: <你的 Docker Hub 用户名>
# Password: <你的密码或 Access Token>
6.2 打标签并推送
bash
# 替换 <your-username> 为你的 Docker Hub 用户名
docker tag flask-redis-counter:2.0 <your-username>/flask-redis-counter:2.0
docker push <your-username>/flask-redis-counter:2.0
输出:
bash
The push refers to repository [docker.io/<your-username>/flask-redis-counter]
e8f9a0b1c2d3: Pushed
f6a7b8c9d0e1: Pushed
...
2.0: digest: sha256:a1b2c3d4e5f6... size: 1573
推送成功后,任何能访问 Docker Hub 的人(或你的 K8s 集群)都可以通过一条命令运行你的应用:
bash
docker run -p 5000:5000 <your-username>/flask-redis-counter:2.0
七、踩坑总结:5 个高频问题
在手动部署过程中,你可能会遇到以下问题。这些都是我从真实读者反馈中整理出来的:
问题 1:Flask 启动后立即退出(Exit 1)
症状 :docker ps 看不到 flask-app,docker ps -a 显示状态 Exited (1)。
原因与排查 :绝大多数情况是因为 Flask 在启动时无法连接 Redis 而抛出异常。检查顺序:Redis 容器是否在同一网络 app-net 中,以及 Redis 容器名是否确实叫 redis(我们 app.py 里写的是 host='redis',大小写敏感)。
解决:
bash
# 查看 Flask 退出日志
docker logs flask-app
# 如果看到 redis.exceptions.ConnectionError
# 确认 Redis 容器存在且运行
docker ps --filter name=redis
问题 2:端口已占用
症状 :Error starting userland proxy: listen tcp4 0.0.0.0:5000: bind: address already in use
解决:
bash
# 查找占用端口的进程
sudo lsof -i :5000
# 或
sudo ss -tlnp | grep 5000
# 更换端口
docker run -d --name flask-app --network app-net -p 5001:5000 flask-redis-counter:2.0
问题 3:Volume 权限错误(Permission denied)
症状 :容器日志中抛出 PermissionError: [Errno 13] Permission denied: '/app/logs/xxx.log',健康检查显示 unhealthy。
原因 :Flask 容器以 appuser(UID 1000)运行,但 Volume 的宿主机目录由 root 创建,权限为 drwxr-xr-x,appuser 无写入权。
解决:
bash
# 方法 1:在 Dockerfile 中预先创建并 chown(我们已在上面修复)
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
# 方法 2:容器启动后手动修复权限(临时方案)
docker exec -u root flask-app chown -R appuser:appuser /app/logs
# 方法 3:重新创建 Volume 并指定权限(需先删除旧 Volume)
docker volume rm flask-logs
docker volume create flask-logs
# 然后在 docker run 时 Docker 会重新初始化目录,受 Dockerfile 中 chown 控制
问题 4:DNS 解析失败
症状 :redis.exceptions.ConnectionError: Error -2 connecting to redis:6379. Name or service not known
排查:
bash
# 确认两个容器在同一个网络
docker inspect flask-app --format='{{json .NetworkSettings.Networks}}'
docker inspect redis --format='{{json .NetworkSettings.Networks}}'
# 测试 DNS 解析
docker exec flask-app nslookup redis
# 如果返回 "can't resolve 'redis'",确认它们都连接到 app-net
问题 5:镜像构建缓存未生效
症状 :每次 docker build 都重新下载 pip 依赖,耗时数分钟。
解决 :确保 requirements.txt 在 COPY . . 之前单独复制。正确的指令顺序是:
bash
COPY requirements.txt .
RUN pip install ...
COPY . .
如果先 COPY . . 再 RUN pip install,源代码任何改动都会导致 pip 安装层缓存失效。
八、手动模式 vs 编排:我们为什么需要 Compose 和 K8s?
通过本篇的实战,你应该已经体会到纯手动管理多容器应用的痛点:
-
每次启动需要记住 8+ 个命令参数,顺序还不能错
-
没有声明式的配置文件,换一台机器就得重新敲一遍
-
依赖关系(先 Redis 后 Flask)需要 sleep 手动等待,不优雅
-
扩容、更新、回滚都非常繁琐
这就是为什么我们需要 Docker Compose(第 11-18 篇)和 Kubernetes(第 19-50 篇)------它们将这些手动操作自动化、声明化、可版本化管理。
九、命令速查表
十、本篇总结
这一篇是 Docker 基础阶段(第 1-10 篇)的收官之作。我们完成了:
-
端到端容器化:从 Dockerfile 到多容器部署,覆盖了前 9 篇的全部知识点
-
数据持久化 :Redis 数据通过
redis-dataVolume 与容器解耦,删除重建不丢数据 -
服务发现 :Flask 通过 Docker DNS 将
redis解析为正确的容器 IP -
生产化配置:健康检查、重启策略、命名卷、日志卷、一键启动脚本
-
镜像分发:推送到 Docker Hub,为后续 K8s 部署做好准备
-
高频踩坑排查:5 个真实场景的诊断与解决方案
从下一篇开始,我们将进入系列的第二阶段------Docker Compose 编排。第 11 篇将教你用一条 YAML 文件替代本篇这几十条手动命令,让多容器应用的管理变得优雅而可重复。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !