当前位置: 首页 > 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.gsyq.cn/news/1418605.html

相关文章:

  • 亚控组态数据导出踩坑实录:报表保存为Excel时文件名乱码、数据错位的解决办法
  • Unity游戏特效实战:用LineRenderer复刻红警磁暴闪电(附完整C#源码)
  • STM32CubeMX外部中断实战:从按键消抖到串口打印,一个完整项目带你避坑
  • 0105【天尊法典】晶体管微缩路径全域锁死:脱离尺寸缩减,算力提升的全域实证与唯一解法
  • Lua 协程:从 API 到底层原理再到 Skynet 架构的完整学习路径
  • Sora 2多视角时空对齐难题攻克,360°视频生成延迟降至117ms——内部Benchmark独家解析
  • 面试官灵魂拷问:A2A协议到底干啥?它与MCP的区别,90%的人都搞错了!
  • 猫抓浏览器扩展:5步掌握终极网页资源嗅探工具
  • Jetson Orin Nano 新手避坑:从零部署YoloV5,我踩过的那些环境配置的坑
  • Keil C51汇编中A14错误解析与解决方案
  • Unity2021升级踩坑记:手把手教你用.androidlib文件夹解决Android资源打包报错
  • 别再傻傻等Unity Logo了!手把手教你用SplashScreen.Stop实现启动屏自定义(附避坑指南)
  • 从Warmup看栈溢出:用GDB+Pedal动态调试BUUCTF CSAW 2016题目
  • 别再手动折腾了!用Composer+PHPStudy一键搞定Imagick扩展(附常见报错解决)
  • 板厂指定用CAM350 V10?别慌!用V14.6中转一下,完美解决Allegro SPB17.4槽孔导入报错
  • Tableau筛选器太乱?教你一招,只显示“全部”和常用选项(保姆级教程)
  • Cadence Allegro出Gerber后,CAM350报错槽孔文件丢失?一个工具版本差异引发的‘血案’与排查实录
  • 从一次线上金额对账Bug说起:手把手教你用BigDecimal重构Java浮点数计算
  • 贝叶斯网络:AI处理不确定性的概率推理利器
  • 避坑指南:Docker Buildx多平台构建推送私有仓库时,如何搞定HTTP证书和network.host权限问题
  • 版图设计工程师的日常:除了画图,DRC/LVS验证和与前端‘吵架’才是重头戏
  • Arm TPIU-M与通用TPIU核心差异及选型指南
  • OrCAD建库避坑指南:从新手到高手必须知道的5个细节(以STM32为例)
  • 深入浅出:基于STM32F4 HAL库的串级PID位置控制详解(附代码与波形分析)
  • STM32F4开发板跑通Modbus TCP主从通信的全套实操资料(含LabVIEW上位机+freeModbus移植工程+调试视频)
  • 告别Cloud Compare!用Qt+PCL从零搭建自己的点云处理软件(附完整源码与避坑指南)
  • 从Neo4j数据到炫酷可视化:手把手教你用Neovis.js和D3.js打造可交互的Web图表
  • TensorFlow 2.10.1 GPU安装避坑指南:CUDA/cuDNN版本选择与Anaconda环境隔离技巧
  • 告别CUDA黑盒:手把手教你用PTX指令直接调用Tensor Core(附HGEMM实战代码)
  • STM32F103C8T6+DHT11温湿度采集:CubeMX配置与HAL库驱动避坑全记录