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 思维 !

相关推荐
郝亚军13 小时前
RK3562 nfs mount
linux·运维·服务器
热爱Liunx的丘丘人13 小时前
Dockerfile 构建自定义 Nginx Web 服务镜像
运维·前端·nginx
IT策士13 小时前
Docker 数据管理:Volume 与 Bind Mount
运维·docker·容器
IT策士13 小时前
Docker Compose 入门:一条命令启动多服务
运维·docker·容器
“码”力全开14 小时前
【架构深析】基于 Docker 与边缘计算的 AI 视频管理平台:从 GB28181/RTSP 统一接入到源码交付的闭环演进
人工智能·docker·架构
Cat_Rocky14 小时前
CICD-DevOps简单学习
运维·学习·devops
IT策士14 小时前
Docker Compose 文件详解:服务、网络与卷
网络·docker·容器
陈海明hack14 小时前
AI的变革下,AI基础设施工程师的技术核心和培养方案(原运维架构师)
运维·人工智能
wanhengidc14 小时前
服务器如何防范病毒攻击
运维·服务器·游戏