首页 / 资讯中心 / 文章详情

Docker容器化入门:从镜像管理到Dockerfile实战

Docker容器化入门:从镜像管理到Dockerfile实战 ★ FEATURED ARTICLE
1. 为什么我建议你从Docker开始上手容器化刚接触容器化那会儿我也被一堆名词绕得头晕——镜像、容器、仓库、编排听起来像是另一个维度的东西。但真正用起来之后才发现Docker的核心逻辑其实特别朴素把应用和它依赖的一切打包成一个标准单元走到哪都能跑。这个单元就是容器而Docker就是目前最主流的容器管理工具。这篇笔记是我自己从零开始学Docker时整理下来的适合完全没有容器基础、但已经能熟练使用Linux基本命令的开发者。不管你是做后端、前端还是运维只要经历过“在我电脑上明明能跑”的尴尬Docker就值得你花一个周末认真搞一遍。我会从最基础的概念讲起一步步带你完成安装、镜像管理、容器操作、数据持久化和自定义镜像构建最后聊几个新手最容易踩的坑。核心关键词就几个镜像、容器、Dockerfile、数据卷、端口映射。搞懂这五个东西日常开发中八九成的场景你都能应付了。2. 先把概念理清楚镜像、容器和仓库到底是什么关系2.1 用做菜来类比镜像是菜谱容器是那道菜很多人一开始分不清镜像和容器我用一个特别土的类比你就明白了。镜像就像一本写好的菜谱里面写清楚了需要什么食材、什么调料、几步操作。它是只读的你没法直接吃菜谱。容器则是按照这本菜谱真正做出来的那道菜你可以吃、可以加盐、可以回锅它是可读写的。同一本菜谱可以做无数道菜同一个镜像也能跑出无数个容器互相之间完全隔离。再往深一层说镜像采用的是分层存储结构。每一层代表Dockerfile里的一条指令层与层之间是叠加关系。比如你先拉了一个Ubuntu的基础镜像然后在上面装Nginx那Nginx那一层只记录变化的部分底下的Ubuntu层是共享的。这样做的好处是如果你再基于同一个Ubuntu基础镜像装MySQL底下的Ubuntu层不需要重新下载省时省空间。这个分层机制是Docker高效的核心原因之一后面讲Dockerfile的时候还会反复提到。仓库就是存放镜像的地方最常用的是Docker Hub。你可以把它理解成一个应用商店里面有官方维护的镜像比如nginx、mysql、redis也有其他开发者上传的。拉取镜像的命令是docker pull推送自己的镜像用docker push。2.2 容器和虚拟机的本质区别在哪经常有人问我直接用虚拟机不就行了为什么要用容器这个区别值得说清楚。虚拟机是在硬件层面做虚拟化每台虚拟机都跑着一个完整的操作系统内核资源开销大启动通常要几十秒甚至几分钟。容器是在操作系统层面做隔离所有容器共享宿主机的内核只是通过命名空间和控制组做资源隔离。这意味着容器启动基本是秒级甚至毫秒级的一台普通开发机上同时跑十几个容器毫无压力。但共享内核也带来一个限制容器里不能跑和宿主机内核不兼容的操作系统。比如你的宿主机是Linux那容器里跑Linux没问题但想跑一个依赖Windows内核的程序就不行。虚拟机没有这个限制因为每台虚拟机有自己的内核。所以两者不是替代关系而是各有适用场景。日常开发、微服务部署、CI/CD流水线容器是首选需要完整隔离不同操作系统环境的场景虚拟机更合适。2.3 安装Docker不同系统下的注意事项Linux上安装Docker最省事的方式是用官方脚本但生产环境我不建议这么做因为脚本会直接装最新版版本不可控。更稳妥的做法是通过包管理器安装指定版本。以Ubuntu为例先更新源再安装docker-ce然后启动服务并设置开机自启。Windows和macOS用户直接下载Docker Desktop就行安装过程基本一路下一步。但有一点要注意Windows家庭版需要开启WSL2后端才能正常运行Docker Desktop专业版和企业版可以用Hyper-V后端。如果你在Windows上遇到启动失败先检查WSL2是否安装并更新到最新版本。安装完成后运行docker version能看到客户端和服务端的版本信息就说明成功了。如果只显示客户端信息、服务端报错大概率是Docker服务没启动Linux下用systemctl start docker启动即可。注意Linux下默认只有root用户和docker组的用户能执行Docker命令。把当前用户加入docker组可以免sudo命令是sudo usermod -aG docker $USER执行完需要重新登录才生效。但要注意docker组权限等同于root权限生产环境慎用。3. 镜像管理拉取、查看和删除的实操细节3.1 拉取镜像时到底发生了什么执行docker pull nginx的时候Docker客户端会先去本地找有没有这个镜像没有的话就去配置的仓库地址拉取。拉取过程是分层下载的你会看到终端里一行行显示每一层的下载进度。如果某一层已经存在本地了它会直接跳过显示“Already exists”。这就是分层存储带来的好处——多个镜像共享的底层只需要存一份。拉取时可以指定标签比如docker pull nginx:1.25。不指定标签默认是latest但生产环境绝对不要用latest因为它是浮动的今天拉到的和明天拉到的可能不是同一个版本出了问题很难排查。我一般会明确指定具体版本号比如nginx:1.25.3这样每次部署的环境完全一致。3.2 查看和筛选本地镜像的实用技巧docker images是最常用的查看命令输出包含仓库名、标签、镜像ID、创建时间和大小。但镜像多了之后这个列表会很长。我常用的几个筛选方式docker images nginx只看nginx相关的镜像docker images --filter danglingtrue查看悬空镜像也就是没有标签的中间层镜像docker images --format table {{.Repository}}\t{{.Tag}}\t{{.Size}}自定义输出格式只看我关心的列悬空镜像通常是构建新镜像时产生的旧层它们不再被任何标签引用但依然占用磁盘空间。定期清理是个好习惯命令是docker image prune。如果想连没在使用的镜像一起清理用docker image prune -a但执行前一定确认没有容器依赖这些镜像。3.3 删除镜像的正确姿势和常见报错删除镜像用docker rmi后面跟镜像ID或仓库名加标签。但经常会出现删不掉的情况报错信息通常是“image is being used by running container”或者“image has dependent child images”。第一种情况说明有容器正在使用这个镜像你需要先停止并删除相关容器。第二种情况说明这个镜像有子镜像依赖它需要先删除子镜像。如果确认要强制删除可以加-f参数但强制删除有风险可能导致依赖它的容器无法启动。我的习惯是先用docker ps -a看看有没有相关容器清理干净再删镜像基本不会出问题。实操心得给镜像打标签用docker tag格式是docker tag 源镜像:源标签 新仓库名:新标签。这个操作不会复制镜像只是给同一个镜像ID增加了一个引用。推送镜像到仓库前必须先打上符合仓库规范的标签。4. 容器操作从启动到进入内部的完整流程4.1 启动容器的几种模式和参数解读docker run是启动容器的核心命令但它的参数特别多新手容易看花眼。我把它拆成几组来理解第一组基本运行模式。-d表示后台运行不加这个参数容器会在前台跑终端一关容器就停了。-it是两个参数的组合-i保持标准输入打开-t分配一个伪终端合起来就是让你能进入容器交互。--name给容器起个名字方便后续操作不然Docker会随机分配一个名字。第二组网络配置。-p做端口映射格式是宿主机端口:容器端口。比如-p 8080:80表示把宿主机的8080端口映射到容器的80端口外部访问宿主机的8080就能打到容器里的80。--network指定容器加入哪个网络默认是bridge网络。第三组存储配置。-v做数据卷挂载格式是宿主机路径:容器路径。这个后面会详细讲。第四组环境变量。-e传入环境变量比如启动MySQL容器时用-e MYSQL_ROOT_PASSWORDxxx设置root密码。一个典型的启动命令长这样docker run -d --name my-nginx -p 8080:80 -v /data/nginx/html:/usr/share/nginx/html nginx:1.25.3这条命令的意思是后台启动一个名为my-nginx的容器把宿主机8080端口映射到容器80端口把宿主机的/data/nginx/html目录挂载到容器内的网页目录使用的镜像是nginx的1.25.3版本。4.2 查看容器状态和日志的常用命令容器启动后用docker ps查看正在运行的容器加-a可以看到所有容器包括已停止的。输出里的STATUS列会显示容器运行时长或退出状态。如果显示“Exited (1)”说明容器启动后很快就退出了通常是配置有问题。排查容器问题最直接的方式是看日志命令是docker logs 容器名或ID。加-f可以持续跟踪日志输出类似tail -f的效果。加--tail 100只看最后100行。如果日志量特别大可以配合--since按时间过滤比如--since 10m只看最近10分钟的。还有一个命令我经常用docker inspect 容器名。它会输出容器的完整配置信息包括IP地址、挂载信息、环境变量、网络配置等是排查问题的利器。输出是JSON格式内容很多可以配合grep过滤比如docker inspect my-nginx | grep IPAddress。4.3 进入容器内部和文件拷贝进入正在运行的容器用docker exec -it 容器名 /bin/bash。如果容器里没有bash可以换成/bin/sh。进去之后就像操作一台普通Linux机器一样可以查看文件、修改配置、执行命令。但要注意在容器里做的修改默认不会持久化容器删除后就没了。需要持久化的东西要么通过数据卷挂载出来要么写进Dockerfile重新构建镜像。从宿主机拷贝文件到容器用docker cp反过来也行docker cp ./config.conf my-nginx:/etc/nginx/conf.d/ docker cp my-nginx:/var/log/nginx/access.log ./access.log这个命令在临时调试时很方便但不适合作为常规部署手段。生产环境的配置文件应该通过数据卷挂载或构建进镜像。4.4 停止、重启和删除容器的规范操作停止容器用docker stop 容器名它会先发SIGTERM信号让容器内的主进程优雅退出等待一段时间默认10秒后再发SIGKILL强制终止。如果容器里的进程响应慢可以加-t参数指定等待时间比如-t 30等30秒。重启用docker restart相当于先stop再start。删除容器用docker rm只能删除已停止的容器。如果容器还在运行需要先停止或者加-f强制删除。强制删除不会给容器内进程清理的机会可能导致数据丢失或文件损坏非必要不用。批量清理已停止的容器可以用docker container prune这个命令会删除所有已停止的容器执行前会提示确认。我一般在开发环境定期跑一下保持环境干净。5. 数据持久化为什么容器删了数据不能丢5.1 容器文件系统的问题和两种解决方案容器里的文件系统是临时的容器一删里面写入的所有数据都没了。这在开发时可能无所谓但数据库容器、日志容器、文件存储容器绝对不能这样。Docker提供了两种数据持久化方式数据卷volume和绑定挂载bind mount。数据卷由Docker管理存储在宿主机的/var/lib/docker/volumes/目录下。创建命令是docker volume create 卷名启动容器时用-v 卷名:容器路径挂载。数据卷的好处是跨平台一致性好备份和迁移方便适合存储数据库文件这类由Docker完全管理的数据。绑定挂载直接把宿主机的某个目录挂到容器里用-v 宿主机绝对路径:容器路径。它的好处是直观你在宿主机上直接就能看到和修改文件适合开发时挂载代码目录、配置文件。但绑定挂载依赖宿主机的目录结构跨平台时路径格式不一样可移植性差一些。5.2 数据卷的创建、挂载和备份实操创建一个数据卷docker volume create mysql-data启动MySQL容器时挂载docker run -d --name mysql \ -e MYSQL_ROOT_PASSWORDmy-secret-pw \ -v mysql-data:/var/lib/mysql \ mysql:8.0这样MySQL的数据文件就存在mysql-data这个卷里即使容器删了卷还在下次启动新容器挂载同一个卷数据就回来了。查看所有数据卷用docker volume ls查看某个卷的详细信息用docker volume inspect mysql-data输出里会显示它在宿主机上的实际路径。备份数据卷可以用一个临时容器把卷内容打包docker run --rm -v mysql-data:/source -v $(pwd):/backup alpine \ tar czf /backup/mysql-data-backup.tar.gz -C /source .这条命令启动一个alpine容器把mysql-data卷挂到/source把当前目录挂到/backup然后在容器里执行tar打包。--rm表示容器用完自动删除。5.3 绑定挂载在开发环境中的典型用法开发时我经常这样用绑定挂载把本地的代码目录挂到容器里这样在宿主机上改代码容器里立刻生效不用重新构建镜像。比如docker run -d --name dev-app \ -p 3000:3000 \ -v $(pwd)/src:/app/src \ -v $(pwd)/config:/app/config \ node:18但绑定挂载有个坑宿主机目录会完全覆盖容器内的对应目录。如果容器里那个目录本来有文件挂载后就被宿主机目录的内容替换了。所以挂载前要确认宿主机目录里有必要的文件或者挂载的是空目录但容器启动时会自动生成文件的场景比如某些应用的日志目录。注意事项在macOS和Windows上使用绑定挂载时文件系统的性能会比Linux下差一些因为跨了文件系统层。如果项目文件特别多建议用数据卷代替绑定挂载或者开启Docker Desktop的文件共享优化选项。6. Dockerfile实战从零构建一个自定义镜像6.1 Dockerfile的基本指令和编写逻辑Dockerfile是一个文本文件里面写了一系列指令Docker按顺序执行这些指令来构建镜像。常用的指令有这么几个FROM指定基础镜像必须是第一条指令RUN在构建过程中执行命令比如安装软件包COPY把宿主机的文件复制到镜像里ADD类似COPY但支持自动解压和远程URL不推荐滥用WORKDIR设置后续指令的工作目录ENV设置环境变量EXPOSE声明容器运行时监听的端口只是声明不实际映射CMD容器启动时执行的默认命令ENTRYPOINT类似CMD但不会被docker run后面的命令覆盖编写Dockerfile的核心逻辑是从基础镜像开始逐层叠加变化。每一条RUN、COPY、ADD指令都会产生一个新层。层数越多镜像越大构建越慢。所以要把多个RUN命令用连接起来减少层数。6.2 一个Node.js应用的Dockerfile完整示例假设有一个简单的Node.js应用目录结构是app.js、package.json、package-lock.json。Dockerfile可以这样写FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . EXPOSE 3000 CMD [node, app.js]逐行解释一下基础镜像用node:18-alpinealpine版本体积小适合生产环境。设置工作目录为/app。先复制package文件再执行npm ci这样只要package文件没变这一层就能复用缓存不用每次重新安装依赖。然后再复制其余代码。声明暴露3000端口。最后指定启动命令。构建命令docker build -t my-node-app:1.0.0 .-t指定镜像名和标签最后的.表示Dockerfile在当前目录。6.3 多阶段构建把镜像体积压到最小上面的Dockerfile构建出来的镜像包含了完整的node_modules和源码体积可能有好几百MB。如果应用需要编译比如TypeScript项目还需要构建工具链体积会更大。多阶段构建可以解决这个问题# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # 运行阶段 FROM node:18-alpine WORKDIR /app COPY --frombuilder /app/dist ./dist COPY --frombuilder /app/node_modules ./node_modules EXPOSE 3000 CMD [node, dist/app.js]构建阶段装了所有依赖并执行编译运行阶段只复制编译产物和运行时依赖。最终镜像里没有源码、没有开发依赖、没有构建工具体积能小很多。我实测过一个TypeScript项目单阶段构建出来是800多MB多阶段构建后只有120MB左右。6.4 构建缓存机制和加速技巧Docker构建时会逐层检查缓存如果某一层的指令和之前构建时完全一样就直接用缓存不重新执行。所以把变化频率低的指令放在前面变化频率高的放在后面能最大化利用缓存。比如上面的Node.js示例先COPY package文件再RUN npm ci最后才COPY源码。因为源码经常改但package文件很少变这样改代码后重新构建时npm ci那一步直接用缓存几秒钟就构建完了。如果某次构建不想用缓存可以加--no-cache参数。另外docker build默认从Docker Hub拉取基础镜像如果网络慢可以配置镜像加速器具体方法这里不展开各云服务商都有提供。实操心得构建镜像时经常遇到“明明改了代码但构建出来还是旧行为”的情况八成是缓存导致的。先确认Dockerfile里COPY的顺序是否合理如果确实需要强制刷新用--no-cache重新构建一次。另外.dockerignore文件可以排除不需要复制到镜像里的文件比如node_modules、.git、日志文件能显著加快构建速度。7. 常见问题排查新手最容易踩的六个坑7.1 容器启动后立刻退出怎么办这是新手遇到最多的问题。容器启动后马上变成Exited状态用docker ps -a能看到退出码。排查步骤是这样的先看日志docker logs 容器名通常能看到具体的报错信息。常见原因有几个启动命令写错了比如CMD指定的可执行文件不存在配置文件有语法错误应用启动时解析失败端口被占用容器内应用绑定端口失败依赖的服务没启动比如应用连不上数据库。如果日志里没有有用信息可以用docker run -it 镜像名 /bin/sh以交互模式启动一个临时容器手动执行启动命令看具体报什么错。这个方法特别适合排查启动脚本的问题。7.2 端口映射不生效的排查思路明明加了-p 8080:80但访问宿主机的8080端口就是不通。先确认容器内的应用确实在监听80端口用docker exec进入容器执行netstat -tlnp或ss -tlnp查看监听状态。如果容器内应用监听的是其他端口比如3000那映射关系就要改成-p 8080:3000。如果容器内监听正常检查宿主机的防火墙规则是否放行了8080端口。Linux下用iptables -L或firewall-cmd --list-ports查看。另外如果Docker运行在虚拟机或远程服务器上还要确认虚拟机的安全组或网络ACL是否放行。还有一个容易忽略的点应用绑定的IP地址。如果应用只绑定了127.0.0.1那容器外部访问不到需要改成绑定0.0.0.0。这个在开发时特别常见本地跑没问题一进容器就访问不了。7.3 数据卷挂载后权限报错的处理挂载数据卷后容器内的进程可能因为权限不足无法读写挂载目录。这是因为容器内进程的UID/GID和宿主机目录的属主不匹配。解决方法有几种最简单的是在启动容器时加--user参数指定运行用户比如--user $(id -u):$(id -g)让容器内进程以宿主机当前用户的身份运行。但有些镜像不支持任意UID运行可能会报错。另一种方法是在Dockerfile里创建对应的用户和组并设置好目录权限。或者启动容器后进入内部手动修改权限但这样每次重建容器都要重新操作不适合生产环境。绑定挂载时宿主机目录的权限设置也很关键。我一般会把开发用的挂载目录权限设为755或775确保容器内用户有读取和执行权限。如果容器需要写入权限要开到777或者把容器内用户加入宿主机目录的属组。7.4 镜像体积过大的优化方向拉了一个官方镜像发现好几个GB构建出来的自定义镜像也小不到哪去。优化镜像体积有几个方向优先选择alpine版本的基础镜像它比默认的Ubuntu或Debian版本小很多。比如node:18是900多MBnode:18-alpine只有100多MB。但alpine用的是musl libc而不是glibc某些依赖glibc的二进制程序可能跑不起来需要提前测试。合并RUN指令减少层数每条RUN都会产生一层层数越多元数据越大。把多个RUN用连接并在最后清理缓存文件比如apt-get clean、rm -rf /var/lib/apt/lists/*。使用多阶段构建把构建工具和源码留在构建阶段只把运行必需的产物复制到最终镜像。这个前面已经详细讲过了。7.5 容器间网络通信的配置方式默认情况下每个容器都有自己的网络命名空间互相之间不能直接通信。如果两个容器需要通信比如应用容器要连数据库容器有几种方式第一种是用--link参数但这种方式已经过时了不推荐在新项目中使用。第二种是创建自定义bridge网络把两个容器都加入这个网络然后就可以用容器名作为主机名互相访问。创建网络用docker network create my-net启动容器时加--network my-net。第三种是用Docker Compose它会在后台自动创建网络并管理容器间的依赖关系。自定义bridge网络是最推荐的方式因为它提供了自动的服务发现——容器之间可以直接用容器名通信不用记IP地址。而且自定义网络里的容器默认可以通过端口互相访问不需要额外暴露端口到宿主机。7.6 磁盘空间被Docker占满的清理方法用了一段时间后/var/lib/docker目录越来越大磁盘告警。Docker占用的空间主要来自几个方面镜像层、容器可写层、数据卷、构建缓存。查看占用情况用docker system df它会列出镜像、容器、数据卷和构建缓存各自占用的空间。清理命令按粒度从细到粗docker image prune清理悬空镜像docker container prune清理已停止容器docker volume prune清理未被使用的数据卷docker builder prune清理构建缓存docker system prune以上全部清理加-a连未使用的镜像一起清理注意docker system prune -a会删除所有未被容器使用的镜像包括你手动拉取但暂时没用的。执行前确认没有正在开发但未启动容器的项目依赖这些镜像。数据卷清理更要谨慎docker volume prune会删除所有未被容器挂载的卷如果里面有重要数据就找不回来了。8. 从单容器到多容器下一步该往哪走单容器玩熟了之后你很快会遇到需要多个容器配合的场景——应用容器、数据库容器、缓存容器、反向代理容器。手动用docker run一个个启动、配置网络、管理依赖关系很快就变得不可维护。这时候就该上Docker Compose了。Compose用一个YAML文件描述整个应用栈一条命令就能启动所有服务自动处理网络和依赖关系。它特别适合开发环境和单机部署。再往上走多机集群、自动扩缩容、滚动更新这些需求就要用到Kubernetes这类容器编排系统了。但我的建议是不要急着往上跳。Docker本身的基础操作——镜像管理、容器生命周期、数据卷、网络、Dockerfile——这些才是根基。根基不牢上Compose和Kubernetes只会更迷糊。我见过太多人连容器日志怎么看都没搞明白就开始折腾Kubernetes集群最后出了问题完全不知道从哪查起。把这篇笔记里的命令都亲手敲一遍每个参数都试试改改看效果遇到报错就按第7节的排查思路走一遍。等你能够不查文档就完成一个自定义镜像的构建和部署再往下一步走会顺畅得多。最后分享一个我自己的习惯每次用docker run启动一个新容器我都会把完整的命令记在一个文本文件里包括所有参数和注释。因为容器的可写层是临时的容器删了之后启动命令就找不回来了。这个习惯帮我省了很多次重新回忆参数的时间也方便我把命令分享给团队其他人。
阅读完成 · 觉得有帮助?
咨询建站