资讯详情

资讯详情

Docker零基础实战:镜像、容器到Compose编排的避坑指南

Docker这东西刚接触的人容易把它想复杂觉得又是一个需要啃很久的新技术。其实本质上它解决的就两件事环境一致性和交付标准化。你不想再听到我本机跑得好好的啊你不想为了部署一个应用写一长串的环境准备文档你不想把生产环境搞得跟玄学一样那Docker就是现阶段最务实的答案。这篇笔记是给零基础同学的但也不是把官方文档念一遍核心是把我自己从知道Docker到能利索地用Docker之间踩过的坑、理顺的思路全部整理出来你看完能直接上手干活。平时写完代码最烦的不是逻辑而是怎么让它在别人机器上跑起来。Docker最迷人的地方在于你把应用连同它的运行环境一起打包成一个镜像拿到哪儿都能跑。这份笔记会从镜像、容器、仓库这三个基础概念讲起带你完成环境安装、镜像拉取、容器启动、数据持久化、网络配置、镜像定制、Compose编排这一整套流程最后附上我在实际项目里遇到过的高频问题排查方案。无论你是后端开发、前端同学还是做运维的只要需要交付和部署这套东西早晚用得上。1. 先搞懂Docker到底解决了什么问题1.1 镜像、容器、仓库这三兄弟很多人学Docker第一个被绕晕的就是镜像和容器到底啥关系。我换个说法你就懂了镜像是一份只读的打包模板容器是这份模板运行起来的实例。同一个镜像可以同时启动多个容器每个容器互不干扰。打个比方镜像就是装系统的光盘容器就是插了光盘跑起来的那台机器光盘本身是共享的、只读的但每台机器上跑出来的状态是独立的。仓库就更简单它是存放镜像的地方。你从仓库拉镜像就好比从网上下载一个安装包。最常用的公共仓库就是Docker Hub里面有nginx、mysql、redis、python、node这些官方镜像。国内访问可能慢后面我会专门讲怎么优化拉取速度。概念层面你先记住一句话镜像是文件容器是进程。你docker pull下来的是镜像它不占内存、不耗CPU就是躺在磁盘上的一堆只读文件你docker run之后才产生容器它才真正在跑。后面所有操作理解到这层就不会乱。1.2 容器和虚拟机为什么不一样这个坑几乎每个新手都会问Docker和虚拟机有啥区别不都是隔离环境吗区别很大简单来说虚拟机是在你的物理机上虚拟出一台完整的假电脑每个虚拟机里都有一个独立的操作系统内核而Docker容器是在你宿主机操作系统之上通过内核的namespace和cgroup机制做隔离所有容器共享宿主机内核。换个生活化的比喻。虚拟机是你从小区里租了一套独立公寓有独立的房门、独立的水电表、独立的户口独立内核房东完全管不到你内部容器则是合租室友模式大家住在一个大套间里共用客厅和厨房宿主机内核但每人有自己的卧室独立进程空间用水用电有限额资源限制。合租显然比整租启动快、开销小这也是为什么容器能在毫秒级启动而虚拟机往往要用秒算。但要注意共享内核这事儿是有代价的。你可以在Linux上用Docker跑centos和ubuntu容器没问题因为它们内核同源。但你在纯Windows上想直接跑Linux容器就费劲因为Windows自身不是Linux内核需要通过WSL2或者虚拟机做兼容层。这个认知决定了你装Docker时选对方式后面环境准备我会重点说。2. 环境准备先把Docker跑起来2.1 安装方式与版本验证安装这块不同系统差别很大。Linux用户最简单各大发行版都有包管理器装好后启动服务即可。macOS用户直接装Docker DesktopWindows用户也建议装Docker Desktop它会自动处理WSL2后端的问题。装完之后不要急着一顿操作先做两个验证docker version docker info这两条命令能看到客户端和服务端的版本信息。如果你发现服务端没起来说明Docker引擎没启动。Linux上执行sudo systemctl enable --now dockermacOS和Windows则直接在桌面端打开软件。我见过太多人装完Docker一敲命令报错cannot connect to the Docker daemon第一反应是重装其实只是服务没拉起来。验证安装的经典一步是跑hello-worlddocker run hello-world如果看到一段解释你的Docker工作正常的提示就说明整个链路通了。这一步同时也是你第一次体会拉取镜像到运行容器的完整过程虽然它只是个打印日志的小家伙。2.2 拉镜像太慢怎么办这是国内环境下的刚需痛点。你能拉得到hello-world这么小的镜像等你拉mysql、redis这种几百兆甚至上GB的镜像时速度能慢到怀疑人生。解决方案是配置镜像加速器也就是给Docker配置registry-mirrors。好多云厂商都提供免费的镜像加速地址你只需要在Docker的配置文件里把它们加进去就行。Linux下改/etc/docker/daemon.jsonmacOS和Windows可以在Docker Desktop的Settings里直接配置核心内容如下{ registry-mirrors: [ https://自己的加速地址 ] }改完重启Docker服务再用docker info看一下Registry Mirrors一栏有没有生效。我要提醒一句加速器只对公共仓库的镜像生效如果你拉的是私有仓库的镜像那还得走网络路径本身这个不要混淆。实测下来这个配置能快上好几倍属于入门必做动作。3. 镜像管理从仓库拉到你本地3.1 拉取镜像与版本选择日常用得最多的就是docker pull。这里有个非常容易踩的习惯坑直接写docker pull nginx或者docker pull mysql不带版本号。这样拉下来的是latest最新版标签。最新版看着香但它的Bug、行为变更、配置项变动都可能在你不知情的情况下发生长期用latest等于把镜像的稳定性交给上游随机掷骰子。真实项目里我强烈要求大家拉取时明确锁定版本docker pull nginx:1.25.3 docker pull mysql:8.0 docker pull redis:7.0主版本号定型比如mysql:8.0代表8.0系列的最新补丁版生产环境如果要更严格可以精确到具体小版本。版本确定的好处不仅是稳定还方便团队协作大家拉同一个tag行为才一致。这也是快速入门里最应该尽早养成的习惯。3.2 查看、删除与善用inspect拉完镜像docker images或docker image ls查看本地已有的镜像列表。你会看到列出的仓库名、标签、镜像ID、创建时间和大小。镜像ID是完整的sha256哈希的前12位够日常区分使用。觉得本地镜像太杂想清理用docker rmi删除。删除时注意如果某个镜像正被容器使用会报错。你得先删容器再删镜像。还有一种批量清理操作用的很少因为我总是担心误删反而是docker image prune这种命令在开发机上比较稳妥它会把没有对应运行容器的悬挂镜像清理掉。平时排查问题或者想做精细配置时docker inspect值得记住。比如你想看某个镜像的环境变量、入口命令、暴露端口一条docker inspect 镜像名就能把底层配置全部翻出来有些官方文档没写明白的细节inspect里都有答案。4. 容器操作日常命令一条龙4.1 docker run参数逐个拆解进入实操核心环节。一条docker run命令是整个Docker日常操作里参数最多、信息量最大的docker run -d --name web01 -p 8080:80 -v /opt/html:/usr/share/nginx/html nginx:1.25.3逐个拆解-d表示后台运行不加它容器在前台运行日志会把你终端霸占你一关终端容器就跟着关了--name给容器起个名字不然后续操作你要依赖一长串的容器ID-p 8080:80做端口映射左边是宿主机端口右边是容器内服务监听的端口访问宿主的8080就会进到容器的80-v做目录挂载等于是把宿主机目录映射进容器改宿主机文件等于改容器文件前面提过数据持久化的事后面会展开。实际试一下用nginx启动一个站点docker run -d --name web01 -p 8080:80 nginx:1.25.3启动后用浏览器访问http://localhost:8080看到nginx欢迎页就成功了。如果页面打不开优先排查端口是否冲突换一个比如-p 8081:80再试。这条命令你反复用几次Docker就算入门一半了。4.2 容器生命周期管理容器启动之后高频操作就是查状态、停启、看日志、进容器docker ps查看运行中的容器docker ps -a查看所有容器包括已退出的docker stop 容器名优雅停止docker start 容器名再次启动已存在的容器docker restart 容器名重启等同于停启docker logs -f 容器名追踪容器日志排障神器docker exec -it 容器名 bash进入容器内部执行命令这里有个我踩过的坑docker exec -it进去之后某些精简镜像比如alpine版本里没有bash只有sh你直接敲bash会报bash not found。解决方式是先用sh试或者命令写成docker exec -it 容器名 sh。另外进程在容器里做的修改容器一删就全没了这个特性初期很容易让人误以为容器是永久的实际恰恰相反它是一次性的临时进程。4.3 容器内外的文件拷贝与清理有时候你会想把宿主机上的文件塞进容器或者把容器里生成的产物拷出来。docker cp就是干这个的docker cp ./config.json web01:/etc/nginx/conf.d/反过来也一样。这个操作在调试阶段非常常用。比如你用nginx容器跑前端项目直接从容器里看文件对不对可以先把构建产物docker cp进去验证确认没问题再上正式的持久化方案。清理容器用docker rm 容器名同样的删容器不等于删镜像两者别搞混。开发机上我定期执行docker container prune清理已停止的容器docker image prune清理悬挂镜像。刚开始用的时候别太心疼数据容器本来就是临时进程心态重要的数据一定要走持久化而不是指望容器本身长生不老。5. 数据持久化容器没了数据还在5.1 先理解为什么数据会丢很多新手第一次用Docker跑了个mysql往里存了点数据然后不小心docker rm了容器再启动一个新容器发现数据全没了那个崩溃的瞬间我经历过。根本原因在于容器销毁时它内部的可写层一起被销毁了它只是个临时进程你给进程里写的数据进程没了就是没了。解决办法就是使用持久化方案把容器内部的可变状态放到宿主机上的独立存储空间。业务数据不该跟容器共存亡必须在外部独立保存。理解了这个逻辑后面所有卷和挂载的用法你都不用死记自然知道哪里该挂。5.2 绑定挂载与命名卷怎么选Docker提供两种主流持久化方式。第一种是绑定挂载bind mount把宿主机上的绝对路径映射到容器路径docker run -d -p 8080:80 -v /home/user/html:/usr/share/nginx/html nginx这种方式的好处是你直接在宿主机改文件容器里立刻生效非常适合开发调试。比如前端项目用nginx镜像托着改完代码直接刷新浏览器就看到效果不用重新构建镜像。缺点是宿主机和容器共用目录权限和路径绑死生产环境迁移麻烦一点。第二种是命名卷named volume由Docker管理存储位置docker volume create mysql_data docker run -d -p 3306:3306 -v mysql_data:/var/lib/mysql mysql:8.0数据跟容器解耦换容器不丢数据生产环境我更推荐这种方式。两种方式在docker run里的写法区别就是绑定挂载的-v后面跟的是宿主机绝对路径命名卷的-v后面跟的是卷名。判断标准一句话开发调试用绑定挂载数据存储用命名卷。注意-v /path和-v name的区别就看第一个字符是不是/。如果是/开头的绝对路径就是绑定挂载否则Docker会把它当作命名卷来创建。5.3 持久化里的权限与路径坑持久化最容易踩的坑是权限问题。宿主机目录的属主和属组跟容器内的用户不一定一致尤其数据库镜像。mysql容器默认用mysql用户跑但宿主机挂载的目录属主可能是root结果容器启动时没权限写数据直接闪退。解决方式比较粗暴也比较常用先给挂载目录开权限比如chmod -R 777或者chown改成容器期望的UID但别图省事一刀切777安全隐患大可以通过docker inspect看镜像声明的用户再对应设置属主。路径方面新手还容易栽在相对路径上。-v后面永远写绝对路径否则Docker会把它理解成卷名行为跟你预期完全不一样。观察它到底挂载在哪儿用docker inspect 容器名里的Mounts字段一清二楚。6. 网络通信容器之间怎么互访6.1 端口映射的本质你运行一个容器容器里有服务监听在某个端口。但容器自己是一个虚拟子网IP宿主机之外的机器没法直接访问这个IP。-p参数干的事就是端口转发把宿主机某个端口上的流量转给容器内端口。规则是左外右内这一点我反复占很多人会反-p 8080:80就是宿主8080进容器80反过来就错了。还有个容易犯迷糊的为什么docker ps里显示0.0.0.0:8080-80/tcp你还是访问不了排查顺序大概是容器起来没docker ps、端口映射对不对docker ps的PORTS列、宿主防火墙放没放行、容器内服务绑定的监听地址是不是0.0.0.0。有些容器内的服务默认监听127.0.0.1那映射了也白搭流量进来它不认。6.2 自定义网络里的容器互联单机场景下多个容器之间通信有两条路一是走端口映射容器A访问宿主IP:映射端口二是让它们处在同一个自定义网络中直接通过容器名互访。第二种更优雅也推荐。先创建网络docker network create demo-net启动两个容器时都加上--network demo-net比如一个跑后端一个跑mysql。后端容器里访问mysql直接用mysql这个容器名当主机名就行docker run -d --name backend --network demo-net myapp docker run -d --name mysql --network demo-net -e MYSQL_ROOT_PASSWORDroot mysql:8.0后端里写数据库连接地址host直接填mysql不要写IP。Docker自带的DNS解析会自动把容器名解析成对应IP而且容器重启换IP了也不影响写死IP反而一重启就断。这个特性是我最常用的强烈推荐大家把多个容器放进同一个自定义网络管理。桥接bridge是默认网络模式适合单个容器的端口映射host模式让容器跟宿主机共用网络性能好一点但隔离差none就是无网络一般场景用不上。7. 镜像定制写一个能用的Dockerfile7.1 最小Dockerfile逐段拆解跑别人的镜像是入门能按自己的需求做镜像才算真正掌握Docker。Dockerfile就是一个如何组装镜像的配方文件。给你看一份很常见也最实用的Node.js项目镜像FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --registryhttps://registry.npmmirror.com COPY . . EXPOSE 3000 CMD [node, app.js]每一行都讲一下。FROM指定基础镜像相当于我要在这个系统上盖房子alpine标签是精简版Linux镜像体积小依赖也干净推荐优先选它。WORKDIR设定工作目录后面所有命令都默认在这个目录执行这行不写你的COPY和RUN就可能散落在容器根目录里乱得不行。COPY package*.json ./是先只拷贝依赖描述文件然后再RUN npm install这样依赖描述文件没变时这一层能命中缓存构建速度会快非常多。COPY . .把项目代码拷进去。EXPOSE只是声明容器内的服务端口它不自动做映射映射还是靠-p。CMD指定容器启动时执行的进程命令注意一个Dockerfile里只能有一个生效的CMD。7.2 构建镜像的正确姿势与缓存陷阱在Dockerfile所在目录执行构建docker build -t myapp:v1.0 .-t是给镜像打标签最后的.表示构建上下文是当前目录。这里引出一个关键概念构建上下文。COPY只能复制构建上下文目录内的文件所以别想着COPY .. /path去拷Dockerfile之外的东西那不在上下文里构建会直接报错。这就是为什么我建议在项目根目录放Dockerfile构建时就在根目录执行。构建加速的核心是理解层缓存。Dockerfile每一行都会生成一个镜像层如果某层的前置层没变且这一层指令也没变Docker会复用缓存的旧层。所以把不容易变的内容放在Dockerfile前面把容易变的代码放在后面。前面我特意先COPY依赖描述再RUN install就是为了让npm install这层尽量命中缓存否则每次改一行代码都要把依赖全装一遍那种痛苦你试过一次就懂。再说一个实战经验尽量合并同类命令减少层数。比如RUN apt-get update apt-get install -y curl vim不要把两个RUN拆开写。因为每个RUN都会生成一层层多了不仅镜像体积大构建也慢。此外项目根目录放一个.dockerignore文件把node_modules、.git、dist这些不需要进镜像的目录排除掉构建速度和镜像大小都会有明显改善。我见过有人忘记写.dockerignore结果把几个GB的依赖目录全部拷进镜像镜像体积直接失控。8. 多人多服务编排Docker Compose入门8.1 compose文件长什么样当你开始同时跑前端、后端、数据库、缓存一条条docker run命令写下来已经够乱了重启顺序靠人脑记住更是灾难。Docker Compose就是把一堆容器配方化用一个YAML文件声明所有服务、网络、卷一条命令全部启动。它的价值在于项目级编排你的一整套环境定义成了代码别人拿到就能一键复现。默认的配置文件叫docker-compose.yml长这样services: web: image: nginx:1.25.3 ports: - 8080:80 volumes: - ./html:/usr/share/nginx/html depends_on: - db db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: app volumes: - db_data:/var/lib/mysql volumes: db_data:注意新版Compose已经不需要再写version字段了你网上看到的老教程里version: 3那行可以忽略。services下面定义每个服务ports、volumes、environment都写成清晰的结构化配置。depends_on表示启动顺序上db要先起但注意它不等待db完全就绪严格的生产场景要用健康检查才可靠。8.2 compose常用命令基本操作就几条docker compose up -d # 启动所有服务 docker compose ps # 查看服务状态 docker compose logs -f # 跟踪日志 docker compose exec web sh # 进入某个服务容器 docker compose down # 停止并删除容器up -d是启动并后台运行第一次运行会先拉镜像再创建容器。down会把容器和默认网络一起删掉但命名卷里的数据默认保留这点设计很合理。如果改了compose文件想按新配置重建用docker compose up -d会智能化地重建有变化的服务不用手动删。我在实操中最大的体会是一定不要手动docker run和Compose混着管理同一个项目。Compose有它自己的容器命名规则你手动创建一个顶着相同名字的容器下次运行docker compose up就可能报name conflict。统一用Compose管项目内服务手动docker run只用来做临时调试。9. 常见问题排查与避坑实录9.1 高频问题速查表整理一份实际项目里高频出现的Docker故障排查速查表都是我自己踩过的现象常见原因处理方式端口访问不通端口映射写反或端口被占换-p 8081:80测试netstat查端口占用镜像拉取极慢未配置镜像加速器配置registry-mirrors并重启Docker容器秒退前台进程没保持检查容器内进程是否缺少-d或进程异常mysql容器启动后闪退挂载目录权限问题检查挂载目录属主调整权限和UID执行docker logs没日志应用缓冲未刷新应用中关闭日志缓冲或改用stdout/stderr容器内中文乱码镜像缺中文字符集基础镜像换成带中文字符集的版本容器时间不对时区未设置启动加-e TZAsia/Shanghai容器被杀掉内存超限docker stats查看资源占用调高--memory限制9.2 排查思路与工具链路很多人遇到容器故障喜欢删了重建前期调试没问题但生产环境这么干等于销毁现场证据。正确排查链应该是先docker ps -a看容器状态容器已退出的话用docker logs看报错信息不够再多加docker inspect看配置最后用docker stats观察资源占用。这套链路把启动时干了什么、运行中发生了什么、配置是否合理全部覆盖了。最实用的案例是容器秒退。新手第一反应是命令写错了其实核心是容器里没有一个能常驻的前台进程。Docker容器存活的生命线是CMD指定的进程如果这个进程启动后立刻退出了容器就跟着退出。比如你写CMD [nginx]但镜像里没有可执行文件或者缺依赖就会报错退出。排查时docker logs看到的最后几行往往就是根因。还有一个容易忽略的容器内存限制。一个项目里我跑了个比较重的服务没加--memory限制结果宿主机内存被打满OOM直接杀进程。后来加了限制让它在可控范围内运行虽然牺牲一些性能但稳定优先。docker stats可以实时看CPU和内存出门排查先跑它准没错。9.3 分享几个提升效率的小习惯最后分享几个让我日常操作顺手很多的小习惯。第一给所有常用服务容器都加--restart unless-stopped宿主机重启后Docker能自动拉起容器省去手动恢复的麻烦。第二善用docker inspect --format做查询比如要查容器的IP一条命令搞定docker inspect -f {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} 容器名。第三频繁调试小项目时优先用Compose而不是一堆docker run配置文件写在项目里别人接手看一眼就明白整个环境结构。这些习惯没有一个是复杂的硬核知识但都来自实际踩坑后的肌肉记忆。Docker入门到熟练的分水岭不是你背了多少命令而是你开始理解容器是临时进程、数据要持久化、环境要声明化这三条核心原则。后面的路还长编排集群、容器安全、镜像仓库管理都会慢慢碰到但把这篇笔记里的命令和思路吃透日常开发部署你已经能跑得比大多数人顺了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →