☰
Docker实战指南:从镜像容器到Compose编排
2026/10/15 9:08:04 网站建设 项目流程

正好最近带几个新同学入门后端开发,发现一个特别有意思的现象:大家一听到Docker就头皮发麻,总觉得它是运维才用的东西,自己写业务代码用不上。但实际练过几周之后,几乎所有人都回头跟我说,这玩意儿的核心概念一旦打通,后面全是水到渠成的事。这篇博文我不打算写那种官方文档式的命令罗列,而是把“Docker学习”这件事按我自己上手时的真实路径拆开揉碎——从镜像、容器、数据卷这些基础概念,到Dockerfile、Compose、网络通信这些进阶玩法,再配上排障经验和真实踩坑记录。你可以把它当成一份“从零到能自己把项目容器化”的实操笔记,适合刚开始接触容器、或者被各种术语劝退过的同学,也适合想补齐底层知识的开发朋友。

1. 先搞明白Docker到底在解决什么问题

1.1 镜像、容器与仓库:忘掉玄乎定义,用模板类比

很多人第一次接触Docker,看到的是一堆黑话:镜像(Image)、容器(Container)、仓库(Repository)、守护进程(Daemon)、数据卷(Volume)。其实这些东西在你生活里早就有对应物了。

可以把镜像理解成安装程序里打包好的模板,但它比安装包更彻底——里面不光有程序代码,连运行这个程序所需要的操作系统底层环境、依赖库、配置文件全都打包了。而容器,就是把这个模板“运行起来”之后得到的一个独立小世界。同一个镜像可以同时启动五个容器,它们互不干扰,就像你用一个PPT模板复制出来五份不同内容的幻灯片。

仓库就更好懂了,它是存放镜像的“应用商店”。你从仓库拉取别人的镜像,也可以把自己做好的镜像推上去。平时我们用docker pull nginx拉取的nginx,就是某个仓库里公开的一份“带好环境配置的nginx模板”。理解这一层,很多命令就好记了:run是启动容器,build是根据你自己的Dockerfile构建模板,push是上传模板,pull是下载模板。

1.2 分层与联合文件系统:为什么Docker镜像能做到这么省

这是我学Docker时觉得最惊艳的地方。一个基础镜像可能几百兆,但一个Nginx镜像却可能只有几十兆——因为Docker镜像不是一整块数据,而是由很多**只读层(Layer)**叠加出来的。

每一条Dockerfile指令基本都会生成一层。比如基础镜像层是centos底层系统,之后一层加了环境变量,再后来一层安装了Nginx。当你新做一个基于centos的Python镜像时,基础层是共享的,Docker不用重新下载,只用把你新增的层补上就行。这也是为什么同一个基础镜像在多台机器上反复拉取时,本地磁盘不会疯狂膨胀的原因。

这种分层机制带来的一个直接习惯是:写Dockerfile时,公共依赖尽量放在前几层,经常变动的代码放在最后一层。以后重新构建镜像时,前面那些层没变就不会重跑,构建速度和缓存命中率都会明显提升。

1.3 环境一致性:开发机跑通,服务器就一定跑通

我常跟身边人举这个例子:本地开发时用的Python版本可能是3.10,服务器上是3.7;本地依赖装得乱七八糟,服务器却干净得要命。结果本地能跑,部署上去就报各种依赖错漏。Docker的出现把这个问题从“运维难题”变成了“流程标准”。

因为镜像里已经把Python运行时、pip依赖、系统库、配置文件全部锁死了,你本地用这个镜像构建起来的服务,和服务器上用同一个镜像拉起来、运行的服务,几乎不可能是两种表现。真正做过部署的人都懂,这条“一致性”有多值钱——过去最让人头疼的线上问题,一半都是环境差异闹的。

2. Docker安装与镜像加速的冷门细节

2.1 安装时的两个坑:默认组与启动服务

我一直在用Linux环境做示例,因为Docker在Linux上表现最好,也是生产环境的常态。很多人装了Docker之后直接敲docker version,然后发现Client正常、Server那边却提示连不上。这不是没装好,而是一般Linux发行版安装后,docker守护进程服务还没启动。需要执行:

sudo systemctl enable --now docker sudo systemctl status docker

把docker服务设为开机自启并查看状态。除了服务问题,还有用户组权限。正常安装之后,只有root用户能直接跑docker命令,普通用户想不敲sudo就用,需要把自己加入docker用户组:

sudo usermod -aG docker $USER newgrp docker

这两步做完,再执行docker info试试,基本就通了。别看这两个坑小,新手至少有一半的时间卡在这。

2.2 镜像加速配置:不配好这一步,后面全是输

从官方仓库直接拉取镜像,如果你在的地区访问不太顺畅,经常是速度慢到令人绝望,甚至直接超时。这不是Docker本身的问题,而是仓库服务距离、网络链路这些现实原因造成的。所以我通常建议装上Docker后,第一件事就配置镜像加速,而不是先跑hello-world测试。

配置方式是在/etc/docker/daemon.json里写入加速地址,比如:

{ "registry-mirrors": ["https://你的可用加速地址"] }

具体用哪个地址,取决于你所在环境。一般云服务商的控制台里都有镜像加速器入口,注册之后能拿到专属地址;也有公共的镜像加速服务可直接填入。选一个实际测试速度靠谱的即可。

配置完记得重启:

sudo systemctl daemon-reload sudo systemctl restart docker

然后docker info里看到Registry Mirrors一栏出现你填的地址,就生效了。这一步千万别偷懒,未配置加速之前拉一个稍微大点的镜像,你可能会以为自己在用拨号上网。

3. 高频命令“肌肉记忆”与一条龙跑容器

3.1 十个最常用命令,先背下来再说

Docker命令确实多,但日常实际高频使用的就那十几个。我整理过一张极简速查表,新手把这张表记熟,基本就能应付绝大多数操作场景。

场景命令说明
从仓库拉取镜像docker pull nginx下载镜像到本地
查看本地镜像docker images列出所有镜像
运行容器docker run -d --name web -p 8080:80 nginx后台运行,映射端口,命名
查看运行中的容器docker ps加-a看所有含已停止
停止/启动容器docker stop web/docker start web生命周期控制
进入容器内部docker exec -it web bash调试必备
查看容器日志docker logs -f web实时输出日志
删除容器docker rm -f web强制删除结合停止
删除镜像docker rmi nginx删除本地镜像
构建镜像docker build -t myapp:v1 .按Dockerfile构建

不需要死记,多用几次肌肉记忆就形成了。真正难的不是命令本身,而是理解每个命令背后到底影响的是“镜像层”还是“容器层”。

3.2 一条龙实战:跑一个能访问的Nginx

纸上谈兵没意思,直接上手。我们先从仓库里拉Nginx镜像,然后跑起来一个可以访问的服务:

docker pull nginx docker run -d --name my-nginx -p 8080:80 nginx

这里拆解一下-d表示后台运行,--name my-nginx给容器起了个名字,-p 8080:80是端口映射——宿主机8080端口进来的流量,会被转发到容器内部的80端口。跑起来之后,浏览器访问http://localhost:8080,能看见Nginx默认欢迎页就成功了。

想验证一下容器独立性?再起一个:

docker run -d --name my-nginx2 -p 8081:80 nginx

两个容器用的是同一个镜像,但各自是独立环境,互不影响。这就是容器隔离最直观的体验。

3.3 交互式调试:容器内部也有“壳”

很多新手不会调试容器,出问题了只能删掉重建。其实在运行的容器里可以直接开一个交互Shell:

docker exec -it my-nginx bash

进去了以后,你面对的就是一个精简版的Linux系统,ls、cat、ps这些命令如果能用,就能自己排查。比如想看Nginx默认配置在哪:

# 在容器内执行 cat /etc/nginx/conf.d/default.conf

这种“开壳调试”的能力太重要了。不过要注意,很多官方基础镜像是对外发布用的,里面不一定装了编辑器,连vim可能都没有。我自己的习惯是:调试一次想改文件,就直接挂载数据卷,或者在宿主机改好后用docker cp复制进容器。

3.4 清理与资源回收:不会删容器的人到处都是

一开始我见过很多人跑一个项目就docker run一次,从来不删,过了一个月docker ps -a一列七八十个容器,磁盘也快满了。建议对手动启动的容器保持“用完即删”的习惯:测试用途的容器直接加--rm参数,容器退出后自动删除;要保留数据的容器,至少在确认不需要时执行docker rm -f。

镜像同理,构建过程会产生一堆<none>的悬空镜像,定期docker image prune -f可以一键清理。这些看似不起眼的习惯,能省掉你后来很多磁盘空间焦虑。

4. 数据卷和端口映射:文件与网络的两个关键闸门

4.1 为什么容器一删,数据就“蒸发”了

这是Docker学习过程中必然会遇到的一个惊吓时刻:你费了好大劲往容器里写了个文件,容器一删,文件全没了。

原因是容器的文件写入都发生在最上层的可写层,而这个可写层是跟随容器生命周期的。容器被删,可写层一起销毁。这其实是设计上的优点——容器是临时、可替代的计算单元,它不该承载持久化数据。如果你想保存数据,必须把它放到容器之外,这就是数据卷(Volume)存在的意义。

4.2 三种数据卷用法:按场景挑

第一种是最常用的命名卷:

docker volume create mydata docker run -d --name myapp -v mydata:/app/data myapp-image

-v左侧是宿主机上的卷名,右侧是容器内的路径。容器写入/app/data的数据,实际存到了宿主机上名为mydata的卷里,容器删除卷还在,下一次用同一个卷接上,数据就回来了。

第二种是绑定挂载(Bind Mount),直接把宿主机的目录映射进容器,开发时最常用:

docker run -d --name dev -v /home/user/code:/app code-image

宿主机/home/user/code下改代码,容器内/app立刻同步,等于免去了代码重新构建的过程。很多本地开发环境就是这样跑的。

第三种就是单纯的 Dockerfile 里声明VOLUME ["/app/data"],用来在构建时预留数据目录,真正执行运行时的挂载还是在run阶段做。

4.3 端口映射与容器网络:容器不是孤岛

默认情况下容器跑着,宿主机外面访问不到它。想让服务被外部访问,必须做端口映射,也就是-p。这里要分清左右两边:左侧是宿主机端口,右侧是容器内端口。多人同时跑多个容器时,左侧端口不能冲突,右侧端口在各自容器内部则无所谓,因为容器网络空间是隔离的。

如果容器内部之间需要互相访问,比如后端要连数据库容器,不建议通过-p把数据库端口暴露到宿主机再让后端访问,这绕了一大圈。正确做法是让两个容器在同一个自定义网络里,后端直接用容器名访问数据库:mysql-db:3306这种。下一章会详细说网络方案。

5. 从“跑别人的镜像”到“写自己的镜像”:Dockerfile实战

5.1 Dockerfile基础指令与顺序逻辑

跑别人的镜像练熟了,就轮到把自己的项目“装进盒子”。Dockerfile是构建镜像的说明书,指令不多,但顺序几乎决定成败。我先给一份最常见的Python项目模板:

FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["python", "app.py"]

逐行看:

  • FROM指定基础镜像,选官方镜像且尽量带-slim或-alpine,体积能省好几倍。
  • WORKDIR设定工作目录,后续命令默认在这里执行。
  • COPY把文件从宿主机复制进镜像。
  • RUN在构建过程中执行命令,这里安装依赖。
  • EXPOSE纯粹是“声明”容器监听端口,实际还是要靠-p映射。
  • CMD是容器启动时默认执行的命令。

为什么先COPY requirements.txt再RUN pip install,最后COPY . .?这就是前面聊过的层缓存机制:依赖文件一般很少变动,提前单独复制出来,就能命中缓存;如果先把全部代码复制进来,你改一个业务文件,依赖层就白装了全部重来,构建速度会打骨折。

5.2 多阶段构建:一个镜像也能“瘦身”

经常有人问我,为什么自己构建出来的镜像动辄一个多G,官方镜像却那么苗条。秘诀往往在于多阶段构建。

以Go项目为例,编译时需要一个带Go编译器的环境,但运行只需要编译好的二进制文件。多阶段构建可以分成两步:

# 第一阶段:编译 FROM golang:1.22 AS builder WORKDIR /src COPY . . RUN go build -o myapp app.go # 第二阶段:运行 FROM alpine:latest WORKDIR /app COPY --from=builder /src/myapp . CMD ["./myapp"]

第一阶段专门负责编译,第二阶段从第一阶段“拷贝”产物,基础环境换成了轻量的alpine。最终镜像里只保留二进制和运行环境,体积能缩小一个数量级。这个思路适用于几乎所有编译型语言:Java项目可以先在Maven镜像里打包,再把Jar复制进精简JDK镜像里。

5.3 .dockerignore:别把垃圾打包进镜像

写Dockerfile的时候,很多人只关心装了什么环境,却忽略了构建上下文本身。执行docker build .时,Docker会把当前目录当成“构建上下文”传到守护进程,如果你目录里有node_modules、__pycache__、.git这类大目录,传输和构建都会异常痛苦。

解决方案是在项目根目录创建.dockerignore文件,语法和.gitignore几乎一样:

node_modules __pycache__ .git *.log .env

写不写这个文件,构建一次的速度差距能达到几十倍。标注“写代码十分钟,构建一小时”的吐槽贴,多半就是没做这个。

6. 多容器协同:网络模式与Compose编排

6.1 容器间通信:网络模式速览

单个容器好跑,真实项目往往要一组容器配合——前端、后端、数据库、缓存。容器之间怎么互访,我总结过一张表:

网络模式通信方式使用建议
bridge(默认)通过IP互访适合单机少量容器,但IP变化多,不推荐依赖IP
自定义bridge网络通过容器名互访最推荐,容器间用名字直接访问,自动DNS解析
host共享宿主机网络栈适合对网络性能极其敏感的场景,但隔离弱
none无网络极少用

自定义网络是日常的核心技能。先建网络:

docker network create mynet

然后运行容器时加--network mynet,同网络内两个容器之间就可以直接用容器名访问了:

docker run -d --name mysql-db --network mynet -e MYSQL_ROOT_PASSWORD=123456 mysql:8 docker run -d --name backend --network mynet -p 8080:8080 myapp-image

在backend容器内部,访问数据库地址写mysql-db:3306就对了,不需要知道MySQL容器的IP是多少。这个“用名字代替IP”的特性在容器重启后依然有效,极大降低了配置维护成本。

6.2 Compose编排:一份YAML描述整个项目

容器一多,每次手工敲一堆docker run参数就容易出错,而且别人没法复现你的环境。这时候就需要Compose了。Compose用一份YAML文件描述整个项目需要的容器、网络、卷和依赖关系,一条docker compose up -d全部拉起。

一份典型配置:

version: "3.8" services: db: image: mysql:8 container_name: project-db environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: mydb volumes: - db-data:/var/lib/mysql networks: - appnet backend: build: ./backend container_name: project-backend ports: - "8000:8000" depends_on: - db networks: - appnet frontend: build: ./frontend container_name: project-frontend ports: - "80:80" depends_on: - backend networks: - appnet volumes: db-data: networks: appnet:

depends_on控制启动顺序,volumes声明命名卷,networks自动创建自定义网络。最关键的好处是:这份文件本身就成了项目的“环境部署文档”,新同事拿到仓库,只需要一个docker compose up -d就能复现整个开发环境。

6.3 一个绝对建议的进阶习惯:环境变量注入

不要在镜像里写死数据库密码、Redis地址这种配置,而是通过环境变量在运行阶段注入。Compose里的environment就是干这个的;如果敏感信息多,还可以用env_file引一个独立的环境变量文件。

这样做的好处是:代码、镜像、配置三者彻底分离。同一个镜像,在开发环境传一套环境变量,在测试环境传另一套,根本不用重新构建。这是容器化从“能用”走向“好用”的必经一步。

7. 排障与维护:日志、清理与生命周期管理

7.1 日常排查三连:日志、描述、进壳

容器起不来了,别急着删了重建。先按这个顺序查:

# 1. 看日志,大部分问题都能在这里找到 docker logs -f 容器名 # 2. 看容器状态和退出码 docker ps -a docker inspect 容器名 # 3. 如果还在运行,进到容器里看进程和文件 docker exec -it 容器名 bash

举个例子,之前做模拟项目X时,容器一直是restarting状态。docker logs一看,数据库连不上,但代码配置里的地址写的是localhost:3306,在容器内部localhost指的是容器自己,不是宿主机,更不是数据库容器。这就是网络理解不到位导致的问题。把连接地址从localhost改成同网络里的数据库容器名,问题立刻消失。

7.2 磁盘空间战争的三个救命命令

Docker用久了最实际的痛点是磁盘。镜像、容器、数据卷、构建缓存,四样东西都在吃空间。我定期会用这三条命令做空间清理:

# 清理悬空镜像 docker image prune -f # 清理所有未使用的容器、网络、悬空镜像 docker system prune -f # 清理所有未使用的镜像、缓存、数据卷(慎用,会删你忘掉的有名字卷) docker system prune -a --volumes

第二条是日常保命首选,第三条建议大扫除时用,而且用前先docker volume ls看看有没有不该删的。

7.3 生命周期管理:你的容器该什么时候删

Docker容器的生命周期设计是“随时拉起、随时销毁”,但实际使用中很多人的习惯还停留在“虚拟机思维”——容器跑到一半不敢重启,怕数据丢、怕配置没保存。

现在把这个顾虑彻底解开:只要你的数据放在数据卷里、配置放在环境变量或Dockerfile里,容器随便删,下一次一条docker run或者docker compose up -d就能原样回来。真正该被珍惜的永远是镜像和卷,而不是容器本身。抱着这个理念去操作,你会发现Docker用起来爽太多了。

另外给一个实操习惯:每次跑新容器之前想一下,这个容器是会长期运行的正式服务,还是临时调试的试验品。正式服务要起名字、加卷、加网络;临时试验品直接--rm跑完即焚。把这个意识培养成肌肉记忆,你的容器列表永远都是清清爽爽的。

7.4 关于时区和基础镜像的碎碎念

最后分享几个小细节。默认官方镜像时区基本都是UTC,如果你的日志分析、定时任务对时间敏感,记得在Dockerfile里加上时区设置:

ENV TZ=Asia/Shanghai

具体写法根据发行版略有差异,用alpine的还得装tzdata包。另一个细节是基础镜像尽量用带-alpine或-slim的标签,不为别的,体积直接少一半,拉取速度、启动速度、磁盘占用全都跟着受益。这种细节你在官方文档里未必看得到,但在生产环境里都是实打实的收益。

学到这一步,你手里已经有一套完整的Docker使用地图了:从理解镜像和容器的本质,到安装配速,再到日常命令跑容器,然后是自己写Dockerfile做定制镜像,进阶到多容器网络与Compose编排,最后掌握排障和清理的办法。剩下的就是多折腾几个真实项目,把别人的镜像跑烂,再把自己的项目容器化跑通,你就彻底赢了。我自己到现在都保留着一个小习惯:任何新项目启动第一件事,就是把开发环境用Docker跑起来,省下来的环境配置时间,用来多写几行业务代码不香吗。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询