资讯动态

Docker入门到实战:容器化部署、镜像构建与Compose编排

发布时间:2026/10/19 14:15:52 来源:尧图企业网站定制
很多人第一次接触 Docker都是因为同一个问题代码在别人电脑上跑不起来。我见过某项目组三台电脑三个表现一台正常一台缺依赖还有一台装了不同版本的底层库启动直接报错。那段时间大家的口头禅是“我这儿明明是好的啊”。后来整个团队花了半天时间统一环境装一样的运行时、一样的数据库、一样的配置结果新同事一入职又得折腾一遍。Docker 就是用来切断这一连串“环境地狱”的。它把应用连同运行环境一起打包成镜像在任意装有 Docker 的机器上都能以一致的容器方式运行。无论底层是 Windows、macOS 还是 Linux只要内核支持容器跑起来的结果就是一样的。这篇文章面向没接触过容器、打算把项目容器化的新手我会先讲清楚 Docker 的几个核心概念和运作逻辑再带着你从零安装、运行第一个容器最后用一个实际项目把 Dockerfile 的写法拆开讲透顺带列出新手最容易踩的坑。1. 在学命令之前先搞懂 Docker 到底解决了什么问题1.1 环境配置地狱是怎么来的传统部署方式里一个应用的运行依赖一大堆前置条件操作系统的版本、运行时版本、系统动态库、配置文件、环境变量、端口占用情况。这些条件散落在机器里没有统一的可视化管理方式。你在一台机器上装好了一套完整的环境可以跑通但同样的步骤换一台机器哪怕操作系统大版本一样也可能因为某个动态库没装而失败。更麻烦的是多个项目共用一台服务器时项目 A 需要 Python 3.8项目 B 需要 Python 3.10系统级的依赖一旦冲突往往只能牺牲其中一个项目。这就是开发环境和生产环境的经典矛盾。Docker 的思路不是帮你更好管理这些依赖而是从底层换一种方式打包应用——把代码、运行时、系统库、配置文件全部放进一个独立自足的镜像里。运行时不再关心宿主机装了什么东西只管基于这个镜像启动容器。1.2 容器与虚拟机的本质区别不少新手会以为 Docker 和虚拟机差不多。在“隔离”这个目标上确实有相似之处但实现原理完全不同。虚拟机通过 Hypervisor 虚拟化硬件每个虚拟机内部有完整的客户机操作系统加载大量与业务无关的系统进程因此启动慢、体积大动辄几个 GB。容器则共享宿主机内核通过 Linux 内核的命名空间做隔离通过 cgroups 做资源限制启动的是应用进程本身而不是一台完整的虚拟电脑。用一个类比虚拟机像租了一套精装修的房子里面家具电器齐备但搬家成本高容器像标准集装箱外面看起来都一样里面装什么由你决定装卸快运送成本低。这个差异带来了最直观的体验虚拟机启动以分钟为单位容器启动以秒为单位同一条机器上能跑的容器数量也远多于虚拟机数量。1.3 镜像、容器、仓库三者的关系这三个名词是 Docker 世界的基座必须在一开始就搞清楚。镜像Image只读的模板里面装好了运行某个应用所需的一切东西。可以理解成设计图纸或安装包不运行、不占内存只占磁盘空间。容器Container镜像被创建并启动之后形成的运行实例是可写、可停、可删的。同一个镜像可以创建多个容器彼此互不影响。仓库Repository集中存放镜像的地方最常用的是 Docker Hub 这种公共仓库也可以自建私有仓库。它承担了镜像的分发与版本管理职责。继续用装修类比镜像就是整体厨房样板间容器就是按照样板间在你家厨房做出的实际灶台你可以用同一套样板间给一百户人家做厨房每家的灶台都会根据实际需求微调但核心布局一致。1.4 数据卷与网络容器不是孤岛容器运行时的文件写入会存在于容器可写层但容器被删除后这些数据也会消失。为了持久化数据Docker 引入了数据卷Volume它由 Docker 管理独立于容器生命周期之外。同理不同的容器之间需要通信。Docker 默认会为每个容器分配独立的网络命名空间你可以通过端口映射让外部访问容器中的服务也可以通过自定义网络让多个容器直接用容器名互相访问。端口映射的写法是-p 宿主机端口:容器端口容器网络的细节在后面实操里展开。理解了这两点回头看 Docker 的整套运作流程就清晰多了。2. Docker 的工作流程从构建到运行的完整链路2.1 一条命令背后的几个阶段我们平时说“跑一个容器”背后其实是几个关键阶段的串联构建Build)通过 Dockerfile 里的指令创建一个镜像。拉取Pull从镜像仓库把别人构建好的镜像下载到本地。创建Create基于镜像生成一个容器的可写层。启动Start以隔离的方式运行容器进程。进入Exec进入正在运行容器的内部执行命令方便排查问题。停止与删除Stop/Remove释放资源。大多数教程喜欢聚焦在构建与运行但新手最容易卡在“为什么这个容器总是退出了”“为什么端口连不通”这类问题上所以 5 和 6 的阶段同样重要。2.2 分层存储Docker 镜像为什么又能省空间又高效Docker 镜像由一个只读的基础层和若干只读的指令层叠加而成每一层代表 Dockerfile 中的一条指令层与层之间有缓存机制。构建时如果某一层没有变化Docker 会直接复用缓存不重新执行该层指令。这个设计带来的实际好处是明显的拉取一个镜像时如果本地已经有基础层只需要下载新增层更新应用代码时只要前面的层不变构建速度会非常快。同时也意味着——想减小镜像体积、加快构建就得学会合理规划层顺序把不常变的依赖放在前面把经常改的代码放在后面。2.3 Dockerfile、镜像、容器之间的流转关系做一个简单归纳Dockerfile 是“配方”描述镜像每一层要做什么。镜像是由配方生成出来的“成品”。容器是成品被“实例化”之后的进程实体。注意一个细节Dockerfile 只是一份文本文件真正构建镜像的命令是docker build。构建成功后你得到的是一个带有 tag 的镜像比如myapp:v1。后续的docker run、docker push操作都是以这个镜像为基础的。理解了这条链路下面就可以实际动手了。3. 从零跑通第一个容器安装配置与生命周期实操3.1 安装 Docker 时的几个注意点不同系统安装方式差异很大Windows 建议直接使用 Docker DesktopmacOS 也一样。Linux 上各个发行版都有自己的包管理器安装前最好先把旧版本卸掉避免冲突。安装完成后先验证是否正常docker --version docker infodocker info能看到当前 Docker 服务端和客户端的版本匹配情况、存储驱动、容器和镜像数量等。如果执行docker version出现无法连接 Docker 守护进程的报错多半是服务没启动Linux 下先systemctl start docker再设为开机自启。3.2 拉镜像和跑容器的第一条命令执行下面这条命令docker run hello-world本地没有镜像时Docker 会先去 Docker Hub 上把hello-world镜像拉取下来然后创建并运行容器。看到一段 Hello from Docker 的输出容器就退出并被保留下来——它本来就是个演示用的临时容器跑完使命就结束。这一步能成功说明装好了、能连通仓库、镜像能走完整生命周期。很多新手卡在这一步只知道报错却不会用docker logs去看容器到底输出了什么。记住这个起点不是因为某条命令熟了就学会了而是知道出错以后怎么定位问题。3.3 容器生命周期管理的常用命令实际开发中最常用的是下面这几类。光背命令意义不大要理解命令触发了容器状态的哪一次切换。# 查看所有容器-a 包含已停止的 docker ps -a # 启动一个 nginx 容器并映射端口 docker run -d --name web -p 8080:80 nginx # 查看容器日志 docker logs web # 进入容器内部 docker exec -it web bash # 停止 / 启动 / 重启 docker stop web docker start web docker restart web # 删除容器 docker rm web-d表示后台运行--name给容器起一个别名-p 8080:80把宿主机的 8080 端口转发到容器内部的 80 端口。此时用浏览器访问http://localhost:8080就能看到 nginx 的默认页面。最后一条docker rm只能删除已停止的容器想强制删运行中的容器需要加-f。3.4 数据卷与端口映射的实操验证一个典型的 Web 服务部署一般需要同时解决两个问题数据持久化与端口转发。先给 nginx 挂载一个数据卷docker run -d --name web-vol -p 8081:80 -v webdata:/usr/share/nginx/html nginx这里webdata是 Docker 管理的数据卷名称它被挂载到容器内的 nginx 网页根目录。即使你执行docker rm web-vol把这个容器删掉之后用同样的卷名重新创建一个容器之前往卷里写入的文件依然还在。这种机制解决了“容器是不可信任的临时环境”这一痛点。数据库容器升级前数据都放在数据卷里就不会因为容器重建而丢掉数据。4. Dockerfile 详解从需求到可交付镜像的完整拆解4.1 为什么要用 Dockerfile而不是直接敲命令当然可以直接用docker run加上各种参数来启动容器但问题在于命令是易逝的。一个复杂的服务可能需要数十个参数换台机器、换个人来操作很容易把某个参数漏掉或写错。Dockerfile 把整个镜像构建过程固化成了文本进入版本管理跟代码一起走。任何人有这份文件都能构建出相同的镜像。这就是“基础设施即代码”思想在容器层面的实践。4.2 一个最小可用 Dockerfile 的逐行拆解下面用一个 Python Flask 应用做例子先建一个项目目录my-flask-app/ ├── app.py ├── requirements.txt └── Dockerfileapp.py内容from flask import Flask app Flask(__name__) app.route(/) def hello(): return Hello, Docker!requirements.txt内容flask3.0.0Dockerfile内容# 基础镜像带 Python 3.11 的官方精简镜像 FROM python:3.11-slim # 指定容器内的工作目录 WORKDIR /app # 先把依赖清单复制进去利用 Docker 层缓存 COPY requirements.txt . # 安装依赖--no-cache-dir 避免留下多余的 pip 缓存 RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # 再把应用代码复制进容器 COPY app.py . # 声明容器运行时监听的端口 EXPOSE 5000 # 容器启动时执行的命令 CMD [python, app.py]然后构建镜像并运行docker build -t my-flask-app:v1 . docker run -d --name flask-app -p 5000:5000 my-flask-app:v1浏览器打开http://localhost:5000能看到服务返回的内容。这段构建过程有几个细节需要展开说。第一COPY requirements.txt .放在COPY app.py .之前不是随意写的。构建时 Docker 会逐层执行指令并检查缓存依赖文件不变时RUN pip install这一层会命中缓存不重新安装依赖。如果你把代码复制那条指令放前面代码一改后面所有层全部失效每次构建都要重装依赖速度会慢很多。第二EXPOSE 5000只是一个声明告诉镜像使用者这个容器内服务监听 5000 端口并不会自动产生端口映射。真正对外发布端口还得靠运行时加-p参数。所以初学者要分清“容器内端口”和“宿主机对外端口”这两个概念。第三CMD使用了 JSON 数组形式exec 形式它在 Dockerfile 里推荐程度远高于CMD python app.py这种 shell 形式。原因在于 exec 形式不会额外启动一层 shell收到 SIGTERM 信号时应用进程能第一时间响应shell 形式则容易把信号吞掉导致优雅停机失效。4.3 构建缓存与 .dockerignore 的作用构建过程中 Docker 会把当前目录内容作为构建上下文发送给守护进程目录越大发送越慢。跟 .gitignore 一样在项目里创建.dockerignore文件把不需要进入镜像的目录排除掉__pycache__ *.pyc .git .venv venv .env这个文件的价值经常被新人忽略。我见过一个项目没写 .dockerignore把整个 node_modules 目录都塞进构建上下文里每次构建都慢得让人怀疑机器出了故障。排除之后只发送必要文件构建速度立竿见影。4.4 优化后的 Dockerfile 进阶写法生产环境通常不会真把多个 COPY 指令拆得那么碎而是使用多阶段构建来进一步控制体积。多阶段构建的字面意思就是 Dockerfile 里有多个FROM阶段前一阶段负责编译或安装依赖后一阶段只复制产物最终镜像只保留后面这一阶段的内容。# 第一阶段构建阶段 FROM python:3.11-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . # 第二阶段运行阶段 FROM python:3.11-slim WORKDIR /app COPY --frombuilder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --frombuilder /usr/local/bin/flask /usr/local/bin/flask COPY --frombuilder /app . EXPOSE 5000 CMD [python, app.py]第一阶段装了一堆依赖文件第二阶段只把必要部分复制过来最终生成的镜像里面不会残留 pip 的缓存、构建期临时文件。对新手而言暂时不需要在第一个 Dockerfile 里就追求这种写法但要建立这个概念——镜像大小和启动速度、拉取时间直接相关尤其是项目上生产环境之后这个优化带来的收益非常明显。5. 新手最容易踩的坑与改进思路5.1 容器启动后立刻退出这是最常见的问题docker run -d执行完容器还在但过几秒再看状态变成 Exited。原因通常是容器内的主进程运行完了没有常驻进程。比如你只是把代码复制进去了却没有用CMD启动服务容器自然就“空转”结束了。排查思路是用docker logs 容器名看输出再用docker inspect 容器名看退出码。排查清楚了再反过来修正 Dockerfile 或者启动命令。5.2 忘记处理时区问题很多基础镜像默认时区是 UTC跟国内时间差 8 小时。日志里打印的时间戳不对会让排查问题的过程变得很痛苦。一个通用做法是在 Dockerfile 里设置时区RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ echo Asia/Shanghai /etc/timezone不同基础镜像可能缺少 tzdata 库遇到报错就先用包管理器装上 tzdata。这个问题虽然很小但是真上生产后会引发各种诡异的时间相关问题。5.3 镜像体积越来越大、构建越来越慢根因往往有两个一是没写 .dockerignore构建上下文很大二是依赖安装不分层、不用缓存每次构建都从零开始。前面已经给过解决方案这里再补充一句查看镜像体积用docker history 镜像名每一层的大小都能看得很清楚定位哪层占空间特别大再针对性优化。5.4 代码复制进容器之后一改代码又要重新构建本地开发时代码改动频繁如果每次都docker build一遍再跑体验确实差。这时候适合用 bind mount 方式把宿主机代码直接挂载进容器docker run -d --name flask-dev -p 5000:5000 -v $(pwd):/app my-flask-app:v1$(pwd)指向当前宿主机目录容器内/app目录实时同步宿主机内容。开发环境里改了代码容器内服务如果开启了热重载就能立即生效。但同样的命令不能直接搬进生产环境生产中应当使用构建好的镜像而不是挂载一堆源码进去运行。5.5 错误使用 latest 标签在 Dockerfile 里写FROM python:latest短时间内可用但一旦官方发布新版本再构建时镜像内容可能完全不一样导致“昨天能跑今天跑不了”。正确做法是锁定主版本或精确版本比如python:3.11-slim或者进一步锁定镜像的摘要值。生产环境的镜像 tag 最好带上版本号而不是全部用 latest。6. 多容器协作从单个容器走向 Compose 编排6.1 为什么单条 docker run 不够用单个容器好管但真实项目往往有多个服务前端、后端、数据库、缓存、消息队列。你当然可以用五条docker run命令把它们一个个跑起来但服务间的网络配置、依赖顺序、重启策略、环境变量管理很快就会成为新的负担。Docker Compose 的价值在于用一份 YAML 文件描述整个服务架构。你用一条docker compose up -d就能把全套服务拉起来。6.2 一个简单的 Compose 文件示例假设有一个 Flask 后端需要连接 Redisversion: 3.8 services: web: build: . ports: - 5000:5000 environment: - REDIS_HOSTredis depends_on: - redis redis: image: redis:7-alpine执行docker compose up -dCompose 会先构建 web 镜像然后创建 redis 容器最后启动 web。depends_on保证 redis 容器先启动但这里要注意depends_on只控制启动顺序不代表 redis 已经就绪应用代码里还是得做连接重试。服务名redis在 Compose 自定义网络里可以直接作为域名解析web 容器里连接 Redis 的地址就填redis而不是 localhost。6.3 资源限制与日志轮转Compose 文件里同样可以声明资源限制services: web: build: . deploy: resources: limits: cpus: 0.5 memory: 512M没有资源限制时某个容器出现内存泄漏可能把宿主机吃干榨净直接影响同机器上其他服务的稳定性。日志轮转也是新人容易忽略的容器无限产出日志磁盘很快被打满配置一下 log 的max-size很有必要。7. 从入门到上手的最后一步把构建流程内化成习惯入门阶段你会被各种新概念撞击镜像、容器、数据卷、网络、Dockerfile、Compose每一样都像新工具。但当你真正用一个实际项目走完一遍之后就会发现所有这些名词都围绕一个核心把应用和环境绑定在一起交付。我在实际项目里最受益的一个习惯是每写一个新服务第一天就把 Dockerfile 和 .dockerignore 建好而不是等到部署阶段才补。这样从第一天起服务就是在容器里开发和验证的而不是“开发完再容器化”。另外一个小建议练手项目不要只跑 hello-world去把平时用到的一个小服务容器化哪怕是几百行的脚本也要走完整的构建、运行、数据持久化、参数配置流程。踩一遍坑比看十篇教程都管用。如果你正在为某个项目配置 Docker先把这份指南里的基础命令逐条跑通再根据你项目的实际技术栈去调整 Dockerfile 和 Compose 配置。后面实在遇到说不清楚的问题还有一招百试百灵——把需要排查的容器日志和 docker inspect 输出拿出来问题往往就已经暴露一半了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑