ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Docker实战指南:从容器原理到镜像构建与部署全解析

Docker实战指南:从容器原理到镜像构建与部署全解析 1. 从环境地狱说起为什么说Docker是集装箱革命我先讲个真实经历。去年我把用了五年的笔记本换掉光是把本地开发环境重建回来就花了两天MySQL要装、Redis要装、Nginx要配、Node版本要切换到项目要求的14、Python还要留着给另一个脚本用。装完才发现端口冲突好不容易理顺了MySQL的root密码又忘了。这还只是我自己的机器。放到团队里就更折腾新同事入职第一周一半时间都在配环境线上出了bug开发说我本地是好的啊运维说我这边复现不了。这种环境地狱在接触Docker之后基本消失了。Docker是什么简单说它是一套操作系统级别的虚拟化技术不是虚拟出一整台电脑而是利用Linux内核的 namespace命名空间和 cgroup控制组机制把进程、网络、文件系统、用户等都隔离开来让每个应用连同它依赖的环境一起打包成一个独立的镜像推到哪里都能跑。我后来给团队做内部培训时喜欢用一个比喻Docker本质上就是软件世界的集装箱。在没有集装箱之前货物搬上船靠工人一箱箱扛规格不一、容易损坏、装卸极慢有了集装箱之后全世界的货船、码头、卡车都按同一套标准设计货物在哪个港口都用一个箱子流转效率提升是数量级的。Docker干的就是这件事——把Linux容器技术标准化了让构建一次到处运行真正成为可能。这篇文章不是讲Docker源码也不是官方文档的翻译。我想以一个在业务团队里把Docker用了四五年的老用户身份把Docker是什么、到底有什么用、从哪开始上手、会踩哪些坑讲透。适合刚接触Docker的开发者也适合看了不少教程但还是没搞懂概念、又或者正在Windows上折腾Docker Desktop安装的读者。1.1 开发、测试、生产三套环境的经典割裂在Docker出现之前最常见的部署方式是什么要么拿一台服务器手动装好各种组件把代码传上去要么用虚拟机克隆一个全套环境镜像。手动部署的问题很明显你今天在服务器上装的是 MySQL 5.7开发机上是 MySQL 8.0SQL 在开发机跑得好好的上了生产就报错有些依赖是两年前装的版本号早就忘了。虚拟机呢确实把环境隔离了但一台虚拟机要模拟完整的硬件装一个完整操作系统动辄几个GB甚至几十个GB启动时间几十秒到几分钟开三五台虚拟机笔记本风扇就开始起飞。Docker 的隔离方式完全不同。它共享宿主机内核不虚拟硬件不装完整操作系统镜像里的就是一个精简的根文件系统加上应用本身。启动一个容器只需要几秒甚至几百毫秒单个镜像从几十MB到几百MB一台机器开几十个容器依然流畅。这带来的直接收益就是你可以在本地精确复刻生产环境也可以在测试服务器上同时跑好几套互不干扰的环境。1.2 Docker和虚拟机的本质区别很多人第一次接触Docker会困惑这不就是轻量级虚拟机吗理解内核共享这一点很重要。维度虚拟机Docker容器底层原理硬件级虚拟化Hypervisor内核级虚拟化namespace/cgroup是否需要完整操作系统每个VM都有一个Guest OS共享宿主机内核只需要文件系统层镜像大小几个GB起步通常几十MB到几百MB启动速度分钟级秒级隔离级别强隔离虚拟硬件级进程级隔离多个容器共享内核资源利用率每台VM都有系统开销几乎没有额外系统开销但也要说清楚Docker不是万能的。因为共享内核Windows的容器和Linux的容器不能直接混跑Windows上跑Linux容器靠的是底层WSL2/Hyper-V虚拟机容器内的进程毕竟只是宿主机的普通进程隔离强度不如虚拟机。对安全要求极高的场景该上虚拟机还是要上虚拟机。2. 镜像、容器、仓库把Docker的三大基石掰开揉碎Docker最核心的三个概念就是镜像Image、容器Container、仓库Repository。理解这三者的关系后面的命令才会变得顺理成章。2.1 镜像只读模板与分层文件系统镜像是一套只读的模板里面包含运行某个应用所需的一切代码或二进制文件、运行时、系统库、配置文件、环境变量。你可以把它理解为应用的安装包快照但这个快照是分层结构的。每一层只记录这一层相对于上一层的变化。以一条Dockerfile为例FROM ubuntu:22.04 RUN apt-get update apt-get install -y nginx COPY index.html /usr/share/nginx/html/ CMD [nginx, -g, daemon off;]上面每一条指令都会产生一个新的镜像层。构建时Docker会缓存已存在的层只构建有变化的层拉取镜像时也是按层下载并复用本地已有的层。所以多个镜像如果底层都是ubuntu:22.04它们就可以共享那些公共层既省磁盘又省带宽。这个分层机制是Docker最巧妙的设计之一也是很多人容易忽略的。我见过一些团队把基础镜像换了个tag就重新构建一遍完全没意识到分层缓存可以省下大量时间。2.2 容器运行起来之后的实例镜像和容器的关系拿类与实例来类比再合适不过。镜像是只读模板容器就是运行起来后的实例等于在镜像最上层加了一个可写层。你在容器里写的文件、改的配置都发生在这个可写层里容器删除后这层也就跟着没了。容器本身是个进程但它和普通进程不一样的地方在于它拥有自己独立的文件系统视图、独立的网络栈、独立的进程树。你在容器里看到的 /etc、/usr 和宿主机上的不是一回事所以你可以在容器里随意装软件不影响宿主机。这种隔离带来的体验是很奇妙的我第一次进容器时习惯性地执行 systemctl restart nginx结果报错说没有 systemd因为容器里根本没有初始化系统只有一个主进程和它的子进程。这也是为什么很多镜像的启动命令喜欢用 nginx -g daemon off;核心原因是把nginx从前台跑起来而不是像系统服务那样后台守护——容器里如果主进程退了容器也就结束了。2.3 仓库镜像分发的关键枢纽仓库负责分发镜像。最出名的是 Docker Hub里面放着官方维护的 mysql、nginx、redis、ubuntu 等镜像。你可以直接 pull 下来用也可以把构建好的镜像 push 到仓库分享给团队使用。自建仓库可以用 Harbor、Nexus或者用各大云服务商提供的镜像仓库服务。注意在业务团队里我不建议直接从 Docker Hub 拉非官方镜像来源不明、更新不及时都是问题。团队内部最好沉淀自己的基础镜像仓库把常用组件版本固定下来再往上面打自己的应用镜像。2.4 写个Dockerfile的常规思路把应用容器化的第一步就是写Dockerfile。以Java后端为例常规写法是四段式选基础镜像、设工作目录、拷贝文件、设置启动命令。FROM eclipse-temurin:17-jre WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]关键点是基础镜像尽量选小的后面第6章会专门讨论镜像瘦身。另外注意 WORKDIR 会直接影响后面所有相对路径约定好目录结构能少踩很多坑。3. 第一次动手从安装到跑通一个nginx容器光说不练没用。我给团队新人出的第一个作业永远是把nginx跑起来因为流程短、反馈直观——浏览器一开就能看到效果。3.1 Linux下安装Ubuntu/Debian系列就是加官方源然后安装sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release echo $VERSION_CODENAME) stable | \ sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginCentOS那套yum思路类似只是源配置方式不同。装完之后记得把当前用户加入docker组否则每条命令都要加sudosudo usermod -aG docker $USER newgrp docker然后确认守护进程已经启动sudo systemctl enable docker sudo systemctl start docker这一步很重要。很多Linux新手装完docker-ce之后执行 docker version发现client有输出但server连不上多半就是守护进程没起来。3.2 Windows/Mac下安装Docker DesktopWindows平台最省心的方式就是装 Docker Desktop这也是现在绝大多数 Windows 入门教程的默认路径。现在的 Docker Desktop 默认基于WSL2后端所以前提是Windows 10/11启用WSL2并开启虚拟化安装流程基本就是下载安装包、一路下一步装完重启打开 Docker Desktop 等右下角图标变成稳定状态。macOS 用户更简单下载 Apple Silicon 版或 Intel 版 Docker Desktop 装好即可。需要注意的是一台机器如果装过旧版 Docker Toolbox需要先彻底卸载干净否则端口和网络驱动容易冲突。3.3 跑通hello-world安装完成之后第一件事先验证环境docker run --rm hello-world这条命令做的事情是本地没有 hello-world 镜像就自动从 Docker Hub 拉取然后创建并运行一个容器容器打印一段欢迎信息后自动退出。能正常看到输出说明整个链路——客户端、守护进程、拉取镜像、创建容器——都是通的。3.4 最常用的几条命令我整理了一份日常使用频率最高的命令清单新手背熟这份就够了命令作用备注docker pull 镜像名:tag拉取镜像tag不写默认latestdocker images查看本地镜像也可用 docker image lsdocker ps -a查看容器列表加-a看已停止的容器docker run -d --name mynginx -p 80:80 nginx后台运行容器-d后台、--name命名、-p端口映射docker exec -it mynginx bash进入容器适合调试生产环境少用docker logs -f mynginx查看日志-f跟随输出docker stop mynginx停止容器删除用 docker rmdocker rm -f $(docker ps -aq)删除所有容器慎用docker rmi 镜像名删除镜像有容器占用时删不掉docker compose up -d按compose文件批量启动后面会细讲为什么 run 的时候要加 -p 80:80因为容器默认有自己独立的网络命名空间宿主机不能直接访问容器IP。把宿主机80端口映射到容器80端口后浏览器访问 localhost:80 就能直达容器里nginx。这个映射概念是整个容器编排里最容易绕晕的地方多敲几次自然就记住了。4. 实战场景拆解MySQL、Redis、微服务和靶场这一部分是很多人接触Docker的原因图省事。以前装一个MySQL要下载安装包、初始化、配权限、写服务脚本现在一行命令搞定。我挑几个真正能落地的场景展开说说。4.1 用MySQL 8.0做开发库热门搜索词里docker安装mysql8.0并使用出现频率一直很高命令并不复杂docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -v mysql_data:/var/lib/mysql \ mysql:8.0这里有两个必选项要理解。一个是 -e MYSQL_ROOT_PASSWORDMySQL官方镜像首次初始化时会读取这个环境变量来设置root密码不配这个容器根本起不来。另一个是 -v mysql_data:/var/lib/mysql这是命名卷把MySQL的数据目录挂到宿主机的一个由Docker管理的目录里。不挂卷的话容器一删数据全部丢失挂了卷删容器重建数据还在。连进去执行SQL也很方便docker exec -it mysql8 mysql -uroot -p本地开发需要一套干净的MySQL时这种方式比装一堆乱七八糟的数据库管理工具方便太多。测试完想彻底清理docker rm -f mysql8 然后重建一个新库又干干净净不会污染宿主机。4.2 用一条命令搭Redis主从开发环境想模拟Redis主从传统方式要在本地起两个Redis实例配置文件、端口、日志目录全部手工处理好。用Docker只需跑两个容器docker run -d --name redis-master -p 6379:6379 redis:7 redis-server --appendonly yes docker run -d --name redis-slave -p 6380:6379 redis:7 redis-server --slaveof redis-master 6379注意容器之间不是通过IP通信而是通过容器名互相访问。Docker默认创建的bridge网络里内置了DNS容器名可以当作主机名解析这就是为什么slave容器里直接写 redis-master 而不是某个IP。这种方式在本地模拟集群、学习主从同步原理时非常直观学到哪一步都能在redis-cli里用 info replication 查看状态。4.3 用docker-compose编排微服务当服务数量上来了比如一个微服务项目有网关、认证、用户、订单四个服务还要依赖MySQL和Redis靠 docker run 一条条敲就太累了。docker compose 就是用来声明式编排的工具写一个 yml 文件把服务、网络、卷全部声明好一条命令全部拉起。services: gateway: build: ./gateway ports: - 8080:8080 depends_on: - user-service - order-service environment: SPRING_PROFILES_ACTIVE: dev user-service: build: ./user-service depends_on: - mysql - redis order-service: build: ./order-service depends_on: - mysql - redis mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 volumes: - mysql_data:/var/lib/mysql redis: image: redis:7 command: redis-server --appendonly yes volumes: - redis_data:/data volumes: mysql_data: redis_data:然后直接在项目根目录执行docker compose up -d服务全部起来日志可以 docker compose logs -f 一起看停止是 docker compose down。做本地开发环境这就够用了。我见过不少团队把多服务本地联调的成本从每人小半天压缩到一条命令搞定靠的就是这份compose文件。4.4 用容器化镜像搭建测试靶场安全测试里经常要搭DVWA这种Web靶场以前要装Apache、MySQL、PHP再配一堆扩展每一步都可能踩坑。Docker一行搞定docker run -d -p 80:80 vulnerables/web-dvwa启动后浏览器访问本机80端口就能进入靶场。Kali下跑的也就这一条命令。这就是Docker秒级环境搭建最好的注脚——那些本机装起来很折腾的工具只要有别人做好的镜像就成了一条命令的事。5. Windows装机踩坑实录虚拟化、WSL、镜像加速那些破事Windows下用Docker体验和Linux差一大截坑尤其多。我把自己和周围人踩过的坑都列出来基本能覆盖新手会遇到的大部分问题。5.1 virtualization support wasnt detected怎么解Docker Desktop在Windows上默认运行在WSL2虚拟机里这要求两个前提CPU虚拟化必须在BIOS里开启Windows的虚拟机监控程序功能必须可用。排查路径按顺序走打开任务管理器 - 性能 - CPU看虚拟化是否显示已启用。如果显示已禁用重启进BIOS/UEFI找到类似 Intel Virtualization Technology / SVM ModeAMD的选项打开后保存重启。在控制面板 - 程序 - 启用或关闭Windows功能里勾选适用于Linux的Windows子系统和虚拟机平台然后重启。以管理员身份打开PowerShell执行wsl --update wsl --set-default-version 2这三步做完绝大多数官方报错都能解决。很多人在网上搜到一堆所谓终极解决办法其实都是绕路最根本的还是虚拟化没开。5.2 Docker Desktop一直Starting这个症状出现最多。Docker Desktop卡在starting界面可能是WSL内核版本太老也可能是Docker引擎启动失败。先检查WSL状态wsl --status如果WSL版本是1需要手动转换wsl --set-version docker-desktop 2还不行就删掉 Docker Desktop 的缓存目录重启应用。目录一般在这个位置%LOCALAPPDATA%\Docker。删之前先备份需要的镜像。5.3 修改镜像存储路径Docker Desktop默认把镜像存在C盘用久了C盘会爆炸。换到D盘的方法是设置 - Resources - Advanced - Disk image location把路径指向D盘点Apply Restart。它本质上是在Windows里动态生成一个虚拟磁盘文件ext4.vhdx所以D盘要预留足够空间。如果已经装了大量镜像直接改路径不会搬数据最稳的办法是先把需要的镜像做成tar包docker save -o myimage.tar myimage:tag改完路径后再重新导入docker load -i myimage.tar5.4 镜像下载慢的加速与换源方案Docker Hub在国内普遍不快解决办法是配置镜像加速器。Docker Desktop里打开设置 - Docker Engine在 json 里加一行{ registry-mirrors: [ https://docker.mirrors.ustc.edu.cn, https://hub-mirror.c.163.com ] }保存后Docker引擎会自动重启。Linux服务器则在 /etc/docker/daemon.json 里同样加这段然后 sudo systemctl restart docker。要注意的是镜像加速器列表是会变的有些长久不用就失效了所以收藏两三个常用加速源失效了及时替换。5.5 权限错误与连接Docker API失败Linux服务端经常遇到 docker: permission denied 或者 failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine 这类错误。前者就是当前用户不在docker组执行sudo usermod -aG docker $USER然后注销重登。后者多半是Windows下Docker引擎没启动或者你用的是PowerShell的当前用户权限不足关掉重开、以管理员身份运行基本能解决。6. 从跑起来到跑得稳数据卷、多阶段构建与生产习惯把服务容器化跑通只是第一步真正到团队协作和生产环境有几个习惯必须养成否则Docker带来的可能不是效率而是事故。6.1 数据持久化容器可以删数据不能丢前面反复提到卷volume这是Docker数据持久化的核心。除了第4章的命名卷还有绑定挂载bind mount方式docker run -d -p 8080:80 -v /home/user/nginx/html:/usr/share/nginx/html nginx这条命令把宿主机目录直接挂进容器宿主机改文件容器立即生效很适合开发环境的代码热更新。命名卷更适合数据库这类需要数据和容器生命周期分离的应用。原则就一条凡是需要保留的数据一律挂卷不要把数据写进容器的可写层。等容器被 recreate、节点被重新调度时你就知道这条原则多救命了。6.2 多阶段构建镜像从1.2GB瘦到160MB很多团队把单体应用容器化之后发现镜像巨大问题往往出在构建产物里混入了编译工具和源码。多阶段构建是解决这个问题最标准的姿势# 第一阶段编译 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src src RUN mvn clean package -DskipTests # 第二阶段运行 FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuilder /build/target/app.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]第一阶段在带完整JDK和Maven的镜像里编译第二阶段只把jar包拷到精简的JRE运行镜像里。最终镜像里没有源码、没有编译工具链体积自然下来了。前端同理第一阶段node构建第二阶段用 nginx 镜像把静态文件拷进去。6.3 生产环境的几个实战建议镜像tag不要用latest。线上部署必须锁定具体版本号不然一次 docker pull 就把环境偷偷升级了出了事故还不知道是哪次拉取导致的。容器内不要以root运行。Redis、Nginx这些官方镜像有的默认用root用来跑业务服务时建议在Dockerfile里创建非root用户避免容器被攻破后直接获得宿主机root权限。日志别往容器里写文件全部输出到标准输出docker logs 才有意义。再配合ELK或Loki这类日志采集工具做集中收集。资源限制一定要加。docker run --memory512m --cpus1 这种参数不加一个内存泄漏的容器可能把整个宿主机拖垮别的服务跟着遭殃。定期更新基础镜像。容器化的好处是升级方便但也意味着你得主动维护半年不更新可能带上老版本的系统漏洞。我个人的体会是Docker解决的不只是环境搭建烦这一个问题它更深层的价值是改变了团队交付软件的方式。以前交付的是代码和一份长长的部署文档现在交付的是镜像和一份简短的compose文件以前本地跑得好好的是团队里最尴尬的一句话现在这句话基本失去了存在的土壤。如果你还在犹豫要不要用Docker我的建议很明确先在本地把一个服务容器化跑起来感受一下从命令行到浏览器直接看到效果的那种顺畅然后再慢慢往深处走。环境搭建的问题不解决后面所有自动化、可移植、可扩展的工程实践都是空中楼阁。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进