当前位置: 首页 > news >正文

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

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。

从第 1 篇搭建环境到现在,我们一路学完了镜像、容器、Dockerfile、数据卷、网络。但说实话——前面的知识都是"散装"的。你学会了怎么单独操作容器,但还没有真正把一整个应用"端到端"地容器化过。

这篇就是来补这一课的。我们将把贯穿全系列的Flask + Redis 计数器应用从头到尾完整容器化,并且不依赖 Docker Compose——用纯 Docker 命令完成网络创建、数据卷挂载、多容器启动和验证。这不仅是对前 9 篇的系统性综合实战,更是为接下来进入 Docker Compose 以及 Kubernetes 编排世界打下最坚实的基础。当你手动完成一次完整的容器化部署后,你才能真正理解 Compose 的docker-compose.yml里每一行在背后帮你做了什么,也才能真正理解 Kubernetes 的 Service 和 Deployment 在解决什么痛点。

一、回顾:我们走到了哪里?

在动工之前,快速回顾一下前 9 篇积累的核心能力:

现在,是时候把它们全部串联起来了。

二、项目全景:我们要交付什么?

我们最终交付的是一个可运行的多组件应用栈:

┌─────────────────────────────────────────────────────┐ │ 宿主机(localhost)│ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Flask 容器 │──────▶│ Redis 容器 │ │ │ │(端口5000)│ DNS │(端口6379)│ │ │ │ │ 解析 │ │ │ │ │ Volume: │ │ Volume: │ │ │ │ flask-logs │ │ redis-data │ │ │ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ ┌──────▼───────────────────────▼───────────┐ │ │ │ 自定义网络: app-net │ │ │ │ 内置 DNS:127.0.0.11 │ │ │ └──────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘

2.1 项目结构

先看一眼最终的项目目录——一个标准的 Docker 化项目通常就包含这些文件:

flask-redis-counter/ ├── app.py# Flask 应用主程序├── requirements.txt# Python 依赖清单├── Dockerfile# 生产级多阶段构建├── .dockerignore# 构建排除文件└── start.sh# 一键启动脚本(本节新增)

2.2 应用代码回顾

以下是我们打磨了两个版本后的最终代码。新增了/health健康检查端点(配合 HEALTHCHECK 指令使用,第 6 篇已详解)和/logs日志查看端点(方便验证 Volume 持久化效果)。

app.py:

importtimeimportosimportredis from flaskimportFlask app=Flask(__name__)cache=redis.Redis(host='redis',port=6379,decode_responses=True)def get_hit_count(): retries=5whileTrue: try:returncache.incr('hits')except redis.exceptions.ConnectionError as exc:ifretries==0: raise exc retries -=1time.sleep(0.5)@app.route('/')def hello(): count=get_hit_count()returnf'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'ifnot os.path.exists(log_dir):return{'error':'logs directory not found'},404files=os.listdir(log_dir)return{'files':files,'count':len(files)}if__name__=='__main__':app.run(host='0.0.0.0',port=5000)

requirements.txt:

flask==3.1.1redis==6.4.0

2.3 Dockerfile(沿用第 5 篇优化版)

# syntax=docker/dockerfile:1# ============================================================# Flask + Redis 计数器应用 —— 生产级多阶段 Dockerfile# 系列贯穿案例 v2.0# ============================================================# ---- 阶段 1:Builder ----FROM python:3.12-slim AS builder RUNapt-getupdate&&\apt-getinstall-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-rrequirements.txt# ---- 阶段 2:Runtime ----FROM python:3.12-slim LABELmaintainer="IT策士"\description="Flask + Redis 计数器应用(贯穿案例 v2.0)"\version="2.0"ENVPYTHONUNBUFFERED=1\PYTHONDONTWRITEBYTECODE=1RUNgroupadd-rappuser&&\useradd-r-m-u1000-gappuser appuser WORKDIR /app COPY--from=builder /wheels /wheels COPY requirements.txt.RUN pipinstall--no-cache-dir --no-index --find-links=/wheels-rrequirements.txt&&\rm-rf/wheels requirements.txt# 创建日志目录RUNmkdir-p/app/logs&&chown-Rappuser:appuser /app/logs COPY--chown=appuser:appuser..USERappuser EXPOSE5000HEALTHCHECK--interval=30s--timeout=3s --start-period=5s--retries=3\CMDcurl-fhttp://localhost:5000/health||exit1CMD["python","app.py"]

变更说明:相比第 5 篇,此处在pip install之后新增了mkdir -p /app/logs并调整权限,确保日志目录存在且可写。这是因为后续我们会将flask-logsVolume 挂载到此路径,如果目录不存在,Docker 会自动创建但权限为 root,导致appuser无法写入。

2.4 .dockerignore

__pycache__ *.pyc *.pyo *.log .env .git .gitignore *.md .vscode .idea venv .venv *.tar *.gz Dockerfile .dockerignore

三、Step by Step:手动启动全套应用

Step 1:构建镜像

cdflask-redis-counterdockerbuild-tflask-redis-counter:2.0.

输出关键行:

[+]Building42.3s(17/17)FINISHED=>[builder1/4]FROM python:3.12-slim0.0s=>[builder2/4]WORKDIR /build0.1s=>[builder3/4]RUNapt-getupdate&&...14.2s=>[builder4/4]RUN pip wheel --no-cache-dir...9.5s=>[runtime1/9]FROM python:3.12-slim0.0s=>[runtime2/9]RUNgroupadd-rappuser&&...0.4s=>[runtime3/9]WORKDIR /app0.0s=>[runtime4/9]COPY--from=builder /wheels /wheels0.2s=>[runtime5/9]COPY requirements.txt.0.1s=>[runtime6/9]RUN pipinstall--no-index...3.8s=>[runtime7/9]RUNmkdir-p/app/logs&&chown...0.3s=>[runtime8/9]COPY--chown=appuser:appuser..0.1s=>[runtime9/9]USERappuser0.0s=>exporting to image2.1s=>=>naming to docker.io/library/flask-redis-counter:2.00.0s
# 确认镜像dockerimages flask-redis-counter# REPOSITORY TAG IMAGE ID SIZE# flask-redis-counter 2.0 b2c3d4e5f6a7 138MB

Step 2:创建自定义网络

dockernetwork create app-net

Step 3:创建命名卷(数据持久化)

dockervolume create redis-datadockervolume create flask-logs

Step 4:启动 Redis 容器

dockerrun-d\--nameredis\--networkapp-net\--restart=unless-stopped\-vredis-data:/data\redis:alpine redis-server--appendonlyyes

参数回顾:

Step 5:启动 Flask 容器

dockerrun-d\--nameflask-app\--networkapp-net\--restart=unless-stopped\-p5000:5000\-vflask-logs:/app/logs\flask-redis-counter:2.0

Step 6:验证整体状态

dockerps--format"table {{.Names}}\t{{.Status}}\t{{.Ports}}"

输出:

NAMES STATUS PORTS flask-app Up10seconds(healthy)0.0.0.0:5000->5000/tcp redis Up30seconds6379/tcp

(healthy)标记说明 Flask 容器内的 HEALTHCHECK 命令已通过验证。

# 检查网络连通性dockerexecflask-appping-c2redis# 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:功能测试

# 测试计数器curlhttp://localhost:5000# Hello World! I have been seen 1 times.curlhttp://localhost:5000# Hello World! I have been seen 2 times.# 测试健康检查端点curlhttp://localhost:5000/health# {"status":"ok"}# 测试日志端点(确认 Volume 已正确挂载)curlhttp://localhost:5000/logs# {"count":0,"files":[]}

四、持久化验证:数据卷的真正价值

4.1 Redis 数据持久化验证

# 查看当前计数curl-shttp://localhost:5000# Hello World! I have been seen 3 times.# 强制删除 Redis 容器(模拟灾难)dockerrm-fredis# 重新创建 Redis 容器(使用同一个 Volume)dockerrun-d\--nameredis\--networkapp-net\--restart=unless-stopped\-vredis-data:/data\redis:alpine redis-server--appendonlyyes# 等待 Redis 和 Flask 重新连接后验证sleep5curlhttp://localhost:5000# Hello World! I have been seen 4 times. ← 计数没有归零!

这个演示就是第 7 篇学到的 Volume 持久化的直观体现——Redis 的 AOF 文件存储在redis-data卷中,容器被销毁不影响数据。新容器挂载同一个卷,Redis 启动时自动从 AOF 文件中恢复所有键值对,计数器无缝衔接。

4.2 日志 Volume 验证

# 查看日志卷的宿主机路径dockervolume inspect flask-logs# "Mountpoint": "/var/lib/docker/volumes/flask-logs/_data"# Flask 应用可以在 /app/logs 目录写入日志文件dockerexecflask-apptouch/app/logs/access.logcurlhttp://localhost:5000/logs# {"count":1,"files":["access.log"]}

即使 Flask 容器被删除重建,挂载同一个flask-logs卷即可恢复所有历史日志。

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

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

start.sh:

#!/bin/bashset-eNETWORK_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. 创建网络(如已存在则跳过) ==="dockernetwork create$NETWORK_NAME2>/dev/null||echo"网络$NETWORK_NAME已存在"echo"=== 2. 创建数据卷(如已存在则跳过) ==="dockervolume create$REDIS_VOLUME2>/dev/null||echo"卷$REDIS_VOLUME已存在"dockervolume create$FLASK_LOGS_VOLUME2>/dev/null||echo"卷$FLASK_LOGS_VOLUME已存在"echo"=== 3. 清理旧容器 ==="dockerrm-f$REDIS_CONTAINER$FLASK_CONTAINER2>/dev/null||trueecho"=== 4. 启动 Redis ==="dockerrun-d\--name$REDIS_CONTAINER\--network$NETWORK_NAME\--restart=unless-stopped\-v$REDIS_VOLUME:/data\redis:alpine redis-server--appendonlyyesecho"=== 5. 等待 Redis 就绪 ==="sleep2echo"=== 6. 启动 Flask 应用 ==="dockerrun-d\--name$FLASK_CONTAINER\--network$NETWORK_NAME\--restart=unless-stopped\-p5000:5000\-v$FLASK_LOGS_VOLUME:/app/logs\$IMAGEecho"=== 7. 等待应用健康检查通过 ==="sleep5echo"=== 8. 状态检查 ==="dockerps--format"table {{.Names}}\t{{.Status}}\t{{.Ports}}"echo""echo"=== 部署完成! ==="echo"访问地址: http://localhost:5000"echo"健康检查: http://localhost:5000/health"

赋予执行权限并运行:

chmod+x start.sh ./start.sh

输出:

===1. 创建网络(如已存在则跳过)===app-net===2. 创建数据卷(如已存在则跳过)===redis-data flask-logs===3. 清理旧容器======4. 启动 Redis===b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0===5. 等待 Redis 就绪======6. 启动 Flask 应用===c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1===7. 等待应用健康检查通过======8. 状态检查===NAMES STATUS PORTS flask-app Up5seconds(healthy)0.0.0.0:5000->5000/tcp redis Up10seconds6379/tcp===部署完成!===访问地址: http://localhost:5000 健康检查: http://localhost:5000/health

现在,整个应用栈从零到全功能运行,只需一条./start.sh

六、推送镜像到 Docker Hub

让其他人也能使用你的镜像,需要推送到镜像仓库。这里以 Docker Hub 为例:

6.1 注册并登录 Docker Hub

如果你还没有 Docker Hub 账号,先去 hub.docker.com 免费注册一个。然后在终端登录:

dockerlogin# Username: <你的 Docker Hub 用户名># Password: <你的密码或 Access Token>

6.2 打标签并推送

# 替换 <your-username> 为你的 Docker Hub 用户名dockertag flask-redis-counter:2.0<your-username>/flask-redis-counter:2.0dockerpush<your-username>/flask-redis-counter:2.0

输出:

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 集群)都可以通过一条命令运行你的应用:

dockerrun-p5000: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',大小写敏感)。

解决

# 查看 Flask 退出日志dockerlogs flask-app# 如果看到 redis.exceptions.ConnectionError# 确认 Redis 容器存在且运行dockerps--filtername=redis

问题 2:端口已占用

症状Error starting userland proxy: listen tcp4 0.0.0.0:5000: bind: address already in use

解决

# 查找占用端口的进程sudolsof-i:5000# 或sudoss-tlnp|grep5000# 更换端口dockerrun-d--nameflask-app--networkapp-net-p5001: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无写入权。

解决

# 方法 1:在 Dockerfile 中预先创建并 chown(我们已在上面修复)RUNmkdir-p/app/logs&&chown-Rappuser:appuser /app/logs# 方法 2:容器启动后手动修复权限(临时方案)dockerexec-uroot flask-appchown-Rappuser:appuser /app/logs# 方法 3:重新创建 Volume 并指定权限(需先删除旧 Volume)dockervolumermflask-logsdockervolume create flask-logs# 然后在 docker run 时 Docker 会重新初始化目录,受 Dockerfile 中 chown 控制

问题 4:DNS 解析失败

症状redis.exceptions.ConnectionError: Error -2 connecting to redis:6379. Name or service not known

排查

# 确认两个容器在同一个网络dockerinspect flask-app--format='{{json .NetworkSettings.Networks}}'dockerinspect redis--format='{{json .NetworkSettings.Networks}}'# 测试 DNS 解析dockerexecflask-appnslookupredis# 如果返回 "can't resolve 'redis'",确认它们都连接到 app-net

问题 5:镜像构建缓存未生效

症状:每次docker build都重新下载 pip 依赖,耗时数分钟。

解决:确保requirements.txtCOPY . .之前单独复制。正确的指令顺序是:

COPY requirements.txt.RUN pipinstall... 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 思维 !

http://www.jsqmd.com/news/907301/

相关文章:

  • 新手也能搞定的TPS5430电源设计:从24V到15V,手把手教你选对每个元器件(附完整BOM清单)
  • 别再只用欧氏距离了!用Python实战Hausdorff距离,搞定图像匹配与异常检测
  • Unity游戏特效实战:用LineRenderer复刻红警磁暴闪电(附完整C#源码)
  • ArcMap新手必看:三种要素选择方法(按属性、位置、图形)的保姆级图文教程
  • 不只是安装:用ArcSWAT做水文分析前,你最好先调整好这3个界面设置
  • 从实验室到产线:Imatest枯叶图在摄像头批量质检中的实战应用与自动化脚本思路
  • Arm CoreLink NIC-400与NI/NoC动态调频技术详解
  • STM32CubeMX外部中断实战:从按键消抖到串口打印,一个完整项目带你避坑
  • Majorana量子码原理与容错计算实践指南
  • 别再手动调动画了!用Unity Timeline + Animation Track制作过场动画的5个高效技巧
  • 0105【天尊法典】晶体管微缩路径全域锁死:脱离尺寸缩减,算力提升的全域实证与唯一解法
  • Sora 2多视角时空对齐难题攻克,360°视频生成延迟降至117ms——内部Benchmark独家解析
  • 告别死板教程!用ShaderGraph复刻《和平精英》动态海面,这5个参数调好了效果直接翻倍
  • Lua 协程:从 API 到底层原理再到 Skynet 架构的完整学习路径
  • UGV多传感器融合:时钟同步与标定技术解析
  • 【免费领】历史典故系列Scratch源码《投鼠忌器》+ 6.1 儿童节源码
  • C语言在嵌入式Linux系统开发中的实战应用
  • 终极免费.brd文件查看器:OpenBoardView完整解决方案
  • 从OCR到工业质检:图像骨架提取(Thinning)的隐藏技能与实战避坑指南
  • 东北大学 Open6G 被指定为 AI-RAN 联盟认可的实验室
  • PriLLM: 为LLM服务实时定价的 Stackelberg Game 建模 【School of CS and Eng,Southeast University】
  • 别再只会拖Button了!用Python脚本+Unity UGUI EventSystem,5分钟自动化测试你的UI交互
  • OpenCV 4.x时代,如何用ORB替代SIFT搞定Python图像拼接(附完整代码)
  • 面试官灵魂拷问:A2A协议到底干啥?它与MCP的区别,90%的人都搞错了!
  • 别再问卖家了!手把手教你用ESP-IDF和esptool查询ESP32的Flash和PSRAM大小(附代码)
  • 猫抓浏览器扩展:5步掌握终极网页资源嗅探工具
  • Python描述符协议深入
  • Win10安装报‘缺驱动’?可能是你的U盘启动盘制作工具该升级了(附最新Ventoy/Rufus避坑指南)
  • Unity TextMeshPro字体突然不显示?别慌,可能是你的动态字体图集满了(附三种解决方案)
  • 避坑指南:Unity ShaderGraph制作透明火焰效果时,Alpha混合和Surface设置的那些坑