docker 实战:将一个多组件应用完整容器化

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 持久化效果)。

app.py

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 卷即可恢复所有历史日志。

五、一键启动脚本:从手动到自动化

每次都要敲七八条命令太麻烦了。我们把整个流程写成一个脚本,实现一键启停:

start.sh

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-appdocker 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-xappuser 无写入权。

解决

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.txtCOPY . . 之前单独复制。正确的指令顺序是:

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-data Volume 与容器解耦,删除重建不丢数据

  • 服务发现 :Flask 通过 Docker DNS 将 redis 解析为正确的容器 IP

  • 生产化配置:健康检查、重启策略、命名卷、日志卷、一键启动脚本

  • 镜像分发:推送到 Docker Hub,为后续 K8s 部署做好准备

  • 高频踩坑排查:5 个真实场景的诊断与解决方案

从下一篇开始,我们将进入系列的第二阶段------Docker Compose 编排。第 11 篇将教你用一条 YAML 文件替代本篇这几十条手动命令,让多容器应用的管理变得优雅而可重复。


想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !

相关推荐
seacracker4 小时前
Ceph 太重运维成本高?轻量统一存储 PowerFS 对比测评(HPC/AI 场景首选)
运维·人工智能·ceph·ai存储·统一存储
ZhengO_Oz4 小时前
虚拟机CentOS 网络配置【实操】
运维·服务器·网络
utf8mb4安全女神4 小时前
如果复制windows 内容到 Linux,带入不可见特殊符,怎么解决
linux·运维·服务器
艾德金的溪4 小时前
【windows安装使用openclaw】
运维·windows·ai
肖永威4 小时前
麒麟 V10 编译 Python 3.11 依赖包缺失与版本冲突问题解决实录
运维·python·麒麟操作系统
志栋智能5 小时前
超自动化运维的治理框架与最佳实践
运维·自动化
辉的技术笔记5 小时前
Dify 配完了怎么知道对不对?5 个关键指标 + 诊断命令
docker
阿成学长_Cain5 小时前
Linux grpck命令超全详解|校验组文件完整性、修复/etc/group错误实战
linux·运维·服务器·网络
进击切图仔5 小时前
基于千问的白盒蒸馏 SOP
运维·服务器·人工智能·python·效率