☰
Docker入门实战:从镜像到容器,一篇搞懂核心操作与避坑指南
2026/10/21 3:29:56 网站建设 项目流程

1. 为什么我劝你尽早把Docker用起来

刚接触容器化那会儿,我也觉得这东西离自己挺远——不就是个轻量级虚拟机吗,我本地跑得好好的,为什么要多套一层?直到有一次帮朋友处理一个线上环境的问题,同一个服务在开发机上跑得飞起,部署到服务器上就各种报错,排查了大半天才发现是系统里某个依赖库的版本对不上。那一刻我才真正意识到,环境不一致这件事,消耗的时间远比学Docker本身要多得多。

Docker解决的核心问题其实就一句话:把应用和它依赖的运行环境打包在一起,让它在任何地方都能以同样的方式跑起来。你写好的服务、装好的依赖、配好的环境变量,全部封装进一个镜像里,换台机器直接拉起来就能用,不用再对着“在我电脑上明明是好的”这句话干瞪眼。

这篇笔记面向的是完全没碰过Docker、或者只听过名字但没实际动过手的朋友。我会从最基础的概念讲起,把安装、镜像、容器、数据卷、网络、Dockerfile这些核心环节全部走一遍,每个命令都配上我实际踩过的坑和验证过的参数。看完之后你至少能做到:自己写一个Dockerfile把项目打包成镜像,用容器跑起来,数据能持久化,多个容器之间能互相通信。这些就是日常开发里最高频的操作,剩下的都是在这个基础上的延伸。

我个人的习惯是,学任何工具都先把它跑起来,再回头理解原理。所以下面不会一上来堆概念,而是边操作边解释,你跟着敲一遍,自然就懂了。

2. 先把地基打牢:核心概念一次讲透

2.1 镜像、容器、仓库到底是什么关系

很多人第一次接触Docker会被这几个词绕晕,我用一个生活化的类比帮你理清楚。

镜像(Image)就像是一张光盘或者一个安装包,它是只读的、静态的。里面包含了运行某个程序所需要的一切:代码、运行时、系统工具、库文件、配置文件。镜像本身不会运行,它只是一个模板。

容器(Container)是镜像运行起来的实例。你把光盘放进播放器里播放,光盘是镜像,正在播放的这个过程就是容器。容器是可读写的,你可以在里面装东西、改配置,但这些改动默认只存在于这个容器里,不会影响镜像本身。

仓库(Registry)是存放镜像的地方,最常用的公共仓库就是Docker Hub。你可以把它理解成一个应用商店,里面有几百万个别人做好的镜像,你直接拉下来就能用。你也可以把自己的镜像推上去,方便在其他机器上拉取。

这三者的关系用一句话概括:从仓库拉镜像,用镜像起容器。日常操作基本就是围绕这条链路展开的。

注意:容器里的改动如果不做特殊处理,容器一删就全没了。这一点新手特别容易踩坑,后面讲数据卷的时候会详细说怎么解决。

2.2 和虚拟机到底差在哪

这是被问得最多的问题。我用一张表把关键差异列出来,你一看就明白。

对比维度虚拟机Docker容器
虚拟化层级硬件级,虚拟出完整的操作系统操作系统级,共享宿主机内核
启动速度通常几十秒到几分钟通常几百毫秒到几秒
资源占用每个虚拟机需要独立的操作系统,占用大共享内核,占用小
隔离性强隔离,安全性更高进程级隔离,相对弱一些
镜像大小通常几个GB起步可以做到几十MB
适用场景需要完整操作系统隔离的场景微服务、快速部署、开发环境统一

关键差异在于共享内核这四个字。虚拟机是在硬件层面虚拟出一整套硬件,然后在上面的操作系统里跑应用;容器是直接跑在宿主机的内核上,通过命名空间和控制组做隔离。所以容器启动快、占用小,但隔离性不如虚拟机彻底。

实际工作中,两者并不是替代关系。很多团队的做法是:底层用虚拟机做资源池,上层用容器跑服务。你不需要纠结选哪个,先把容器用起来,体会到它的便利之后,自然就知道什么场景该用什么了。

2.3 安装环节的几个关键选择

安装本身不复杂,但有几个点值得提前说清楚,免得你装完了发现不对劲。

Linux上的安装,官方提供了一键脚本,但我不建议直接跑脚本。原因是你不知道它到底改了哪些配置,出了问题不好排查。我更推荐用包管理器一步步来,以常见的发行版为例,大致流程是:先卸载旧版本,然后添加官方软件源,再安装指定版本。具体命令各发行版略有差异,核心思路就是通过包管理器安装,方便后续升级和卸载。

Windows和macOS上的安装,直接下载桌面版客户端就行。它会帮你把底层环境都配好,开箱即用。但有一点要注意:桌面版默认使用的资源是有限的,如果你要跑比较吃内存的服务,记得在设置里调整CPU和内存的分配。

安装完成后,用下面这条命令验证:

docker version

如果能看到客户端和服务端的版本信息,说明安装成功。如果只显示客户端信息,提示连不上服务端,那多半是服务没启动,检查一下服务状态即可。

提示:安装完成后建议把当前用户加入docker用户组,这样就不用每条命令都加sudo了。具体操作是把自己加到docker组里,然后重新登录一次让权限生效。这一步能省掉很多日常操作的麻烦。

3. 镜像操作:从拉取到管理

3.1 拉取镜像的正确姿势

拉取镜像用docker pull命令,格式是docker pull 镜像名:标签。标签不写的话默认是latest,但我强烈建议你永远显式指定标签。

为什么?因为latest是一个浮动标签,今天拉到的和明天拉到的可能不是同一个版本。生产环境里用浮动标签,等于给自己埋雷。我一般会指定具体的版本号,比如nginx:1.25这种,确保每次拉到的都是同一个东西。

docker pull nginx:1.25

拉取的过程会分层下载,每一层下载完会显示Pull complete。这里有个细节:镜像是由多层组成的,相同的基础层在不同镜像之间是共享的。所以你拉第二个基于同样基础镜像的镜像时,那些已经存在的层不会重复下载,速度会快很多。

如果你觉得从公共仓库拉取速度慢,可以配置镜像加速器。各大云服务商都提供加速地址,配置方法是在Docker的配置文件里加上registry-mirrors字段,然后重启服务。这个配置是一次性的,配好之后所有拉取操作都会走加速。

3.2 查看和管理本地镜像

拉下来的镜像存在本地,用下面这条命令查看:

docker images

输出会包含仓库名、标签、镜像ID、创建时间和大小。镜像ID是一串十六进制字符,你在操作镜像时可以用ID的前几位来指代,不用写全。

删除镜像用docker rmi,后面跟镜像ID或者镜像名:标签。如果这个镜像正在被某个容器使用,删除会失败。这时候你有两个选择:先删掉依赖它的容器,或者加-f参数强制删除。我建议用前者,强制删除容易留下悬空镜像,时间长了占空间。

说到悬空镜像,那些没有标签、显示为<none>的镜像就是。它们通常是构建过程中产生的中间层,或者被新版本覆盖后留下的旧层。清理它们用:

docker image prune

这个命令会删除所有悬空镜像。如果想连没有被任何容器使用的镜像一起清理,加-a参数。但用-a之前想清楚,别把还需要用的镜像误删了。

3.3 镜像的导入导出

有时候你需要把镜像从一台机器搬到另一台机器,而两台机器之间网络不通,或者目标机器不方便从仓库拉取。这时候可以用导入导出的方式。

导出用docker save:

docker save -o nginx.tar nginx:1.25

这会把镜像保存成一个tar文件。传到目标机器后,用docker load导入:

docker load -i nginx.tar

导入后镜像就出现在本地镜像列表里了,和从仓库拉下来的一模一样。

注意:docker save和docker export是两回事。前者导出的是镜像,包含所有层和元数据;后者导出的是容器的文件系统快照,会丢失镜像的历史层信息。日常搬运用save和load就够了。

4. 容器操作:让镜像真正跑起来

4.1 启动容器的常用参数

启动容器用docker run,这是最核心也最复杂的命令。我把它拆成几个部分来讲。

最基本的用法是docker run 镜像名,这会启动一个容器并执行镜像里定义的默认命令。但实际使用中,我们几乎总是要加各种参数。下面这几个是我用得最多的:

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

逐个解释:-d表示后台运行,不加的话容器会在前台运行,占着你的终端;--name给容器起个名字,方便后续操作,不起名字的话Docker会随机分配一个;-p 8080:80把宿主机的8080端口映射到容器的80端口,这样你访问宿主机的8080就能访问到容器里的服务。

端口映射的格式是宿主机端口:容器端口,别搞反了。我见过有人写成80:8080,然后纳闷为什么访问不了。

还有一个常用参数是-it,通常一起用,表示分配一个交互式终端。比如你想进到容器里看看:

docker run -it --name my-shell ubuntu:22.04 /bin/bash

这会启动一个ubuntu容器并直接进入bash。退出的时候输入exit,容器就停止了。

4.2 容器的生命周期管理

容器有几种状态:运行中、已停止、已暂停。对应的操作命令如下:

操作命令说明
启动docker start 容器名启动已停止的容器
停止docker stop 容器名优雅停止,给进程发送信号
强制停止docker kill 容器名直接杀掉,不给缓冲时间
重启docker restart 容器名先停再启
暂停docker pause 容器名冻结进程
恢复docker unpause 容器名解冻进程
删除docker rm 容器名删除已停止的容器

stop和kill的区别值得说一下。stop会先给容器里的主进程发送一个终止信号,让它有机会做一些清理工作,等待一段时间后才强制杀掉。kill则是直接发强制终止信号。日常操作优先用stop,只有在进程卡死、stop没反应的时候才用kill。

删除容器时,如果容器还在运行,需要先停止再删除,或者加-f强制删除。批量清理所有已停止的容器:

docker container prune

这个命令会删除所有处于停止状态的容器,用之前确认一下没有还需要保留的。

4.3 进入容器和查看日志

容器跑起来之后,你经常需要进去看看情况。有两种方式:

第一种是docker exec,在运行中的容器里执行命令:

docker exec -it my-nginx /bin/bash

这会打开一个新的终端会话,你可以在里面操作,退出后容器继续运行。这是最常用的方式。

第二种是docker attach,连接到容器的主进程:

docker attach my-nginx

这种方式有个坑:你退出的时候,容器的主进程也会收到信号,可能导致容器停止。所以除非你明确知道自己在做什么,否则优先用exec。

查看日志用docker logs:

docker logs -f --tail 100 my-nginx

-f表示持续输出,类似tail -f;--tail 100表示只看最后100行。不加--tail的话会把所有日志都打出来,如果容器跑了很久,日志量会很大,刷屏刷到你看不清。

提示:如果容器的主进程把日志输出到文件而不是标准输出,docker logs是看不到的。所以自己构建镜像时,尽量让应用把日志打到标准输出和标准错误,这样用docker logs就能统一查看。

5. 数据持久化:别让数据随容器一起消失

5.1 为什么需要数据卷

前面说过,容器里的改动默认只存在于容器内部,容器一删就全没了。这对无状态服务来说没问题,但对数据库、文件存储这类有状态服务来说就是灾难。

数据卷(Volume)就是解决这个问题的。它把宿主机的某个目录或者Docker管理的某个存储区域挂载到容器里,容器往这个目录写数据,实际上是写到了宿主机上。容器删了,数据还在。

Docker里有两种主要的挂载方式:数据卷和绑定挂载。数据卷由Docker管理,存在/var/lib/docker/volumes/下面,你不需要关心具体路径;绑定挂载则是把宿主机的任意目录挂到容器里,路径由你指定。

5.2 数据卷的创建和使用

创建数据卷:

docker volume create my-data

查看所有数据卷:

docker volume ls

使用数据卷启动容器:

docker run -d --name my-db -v my-data:/var/lib/mysql mysql:8.0

-v my-data:/var/lib/mysql表示把名为my-data的数据卷挂载到容器的/var/lib/mysql目录。这样MySQL的数据就存在数据卷里了,容器删了数据还在。

绑定挂载的写法类似,只是把数据卷名换成宿主机路径:

docker run -d --name my-app -v /home/user/app:/app my-app:1.0

这会把宿主机的/home/user/app目录挂到容器的/app目录。你在宿主机上改文件,容器里立刻能看到,开发调试的时候特别方便。

注意:绑定挂载时,如果宿主机目录不存在,Docker会自动创建一个空目录。但如果你挂载的是一个文件而不是目录,宿主机上这个文件必须已经存在,否则Docker会把它当成目录创建,导致挂载出错。

5.3 数据卷的备份和恢复

数据卷本身不方便直接拷贝,但可以通过临时容器来操作。备份的思路是:启动一个临时容器,把数据卷和宿主机的备份目录都挂进去,然后在容器里打包。

docker run --rm -v my-data:/data -v $(pwd):/backup ubuntu:22.04 tar czf /backup/my-data.tar.gz -C /data .

这条命令启动一个ubuntu容器,把my-data卷挂到/data,把当前目录挂到/backup,然后在容器里把/data打包到/backup。--rm表示容器用完就删。

恢复的时候反过来:

docker run --rm -v my-data:/data -v $(pwd):/backup ubuntu:22.04 tar xzf /backup/my-data.tar.gz -C /data

这套操作我实际用过好几次,换机器迁移数据的时候很管用。关键点是临时容器用完就删,不会留下垃圾。

6. 网络配置:让容器之间能说上话

6.1 默认网络模式解析

Docker安装后会创建三种默认网络:bridge、host和none。

bridge是默认模式。每个容器分配一个独立的IP,通过Docker创建的虚拟网桥和宿主机通信。容器之间可以通过IP互相访问,但默认不能用容器名互相解析。

host模式下,容器直接使用宿主机的网络栈,没有独立的IP。好处是性能好,坏处是端口冲突的风险高,而且失去了网络隔离。

none模式就是不给容器配网络,只有回环接口。一般用不到,除非你有特殊需求。

查看网络列表:

docker network ls

6.2 自定义网络的实战价值

默认的bridge网络有个很大的限制:容器之间不能用名字互相访问。你只能通过IP,但IP是动态分配的,容器重启后可能就变了。这在多容器协作的场景下很不方便。

解决办法是创建自定义网络:

docker network create my-net

然后在启动容器时指定加入这个网络:

docker run -d --name app1 --network my-net my-app:1.0 docker run -d --name app2 --network my-net my-app:1.0

加入同一个自定义网络的容器,可以直接用容器名互相访问。比如app1里访问app2,直接用app2:端口就行,Docker内置的DNS会帮你解析。这个特性在实际项目中非常实用,省去了手动维护IP映射的麻烦。

6.3 端口映射的几种场景

端口映射的格式前面提过,这里补充几种常见场景。

映射到指定端口:-p 8080:80,宿主机的8080对应容器的80。

映射到随机端口:-p 80,只写容器端口,宿主机端口由Docker随机分配。用docker port 容器名可以查看实际分配到了哪个端口。

映射多个端口:-p 8080:80 -p 8443:443,需要几个就写几个。

限制绑定地址:-p 127.0.0.1:8080:80,只允许本机访问,外部访问不了。这个在开发环境里很有用,避免服务被外部意外访问到。

提示:端口映射是在启动容器时指定的,容器启动后不能修改。如果端口写错了,只能删掉容器重新创建。所以启动前想清楚端口规划,别等跑起来了才发现冲突。

7. Dockerfile:把自己的项目打包成镜像

7.1 Dockerfile的基本结构

Dockerfile是一个文本文件,里面是一条条的指令,每条指令构建一层。Docker按顺序执行这些指令,最终生成一个镜像。

一个典型的Dockerfile长这样:

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

逐行解释:FROM指定基础镜像;WORKDIR设置工作目录,后续的命令都在这个目录下执行;COPY把文件从构建上下文复制到镜像里;RUN在构建时执行命令;EXPOSE声明容器运行时监听的端口,这只是一个声明,实际映射还是靠-p参数;CMD指定容器启动时执行的命令。

7.2 构建指令的细节和坑

FROM:选择基础镜像时,优先选体积小的版本。比如python:3.11-slim就比python:3.11小很多,因为去掉了很多不常用的工具。但要注意,slim版本可能缺少一些编译工具,如果你的依赖需要编译,可能得用完整版或者手动装编译工具。

COPY和ADD的区别:COPY就是单纯复制文件,ADD除了复制还支持自动解压tar文件和从URL下载。但官方建议优先用COPY,因为ADD的行为不够直观,容易出意外。

RUN指令的合并:每条RUN都会产生一层,层数太多会让镜像变大。所以多个命令尽量用&&连起来写在一行:

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

这样只产生一层,而且清理了apt的缓存,镜像会小很多。

CMD和ENTRYPOINT的区别:CMD提供默认命令,如果docker run时指定了命令,CMD会被覆盖;ENTRYPOINT则是固定的入口,docker run后面的参数会作为ENTRYPOINT的参数传进去。简单说,CMD容易被覆盖,ENTRYPOINT不容易。

7.3 构建上下文和.dockerignore

执行docker build的时候,当前目录(或者指定的目录)就是构建上下文。Docker会把整个上下文打包发给守护进程,所以上下文越小,构建越快。

如果上下文里有很多不需要的文件,比如.git目录、node_modules、日志文件,会拖慢构建速度。解决办法是创建一个.dockerignore文件,把不需要的文件排除掉:

.git node_modules *.log __pycache__

这个文件的作用类似.gitignore,但针对的是Docker构建。我见过有人构建一个简单的Python镜像,因为上下文里有个几百MB的数据文件,每次构建都要等好几分钟。加上.dockerignore之后,几秒钟就完事了。

7.4 多阶段构建的实战应用

多阶段构建是Dockerfile里非常实用的一个特性,特别适合编译型语言。思路是在一个阶段里编译,然后把编译产物复制到另一个更小的阶段里运行。

以Go为例:

FROM golang:1.21 AS builder WORKDIR /build COPY . . RUN go build -o myapp . FROM alpine:3.18 WORKDIR /app COPY --from=builder /build/myapp . CMD ["./myapp"]

第一个阶段用完整的Go环境编译出二进制文件,第二个阶段用极小的alpine镜像,只把编译好的二进制复制过来。最终镜像可能只有几十MB,而如果直接用Go镜像,可能有好几百MB。

这个技巧在实际项目中非常有用,尤其是对镜像体积有要求的时候。我经手的一个服务,用多阶段构建把镜像从800MB压到了50MB,部署速度提升非常明显。

8. 常见问题排查与避坑经验

8.1 容器启动失败怎么查

容器启动失败是最常见的问题。排查思路分三步:

第一步,看容器状态。docker ps -a能看到所有容器的状态,如果是Exited,说明启动后很快就退出了。

第二步,看日志。docker logs 容器名能看到容器的输出,大部分启动失败的原因都会在这里显示。比如配置文件写错了、端口被占用了、依赖的服务连不上,日志里都会有线索。

第三步,如果日志里没有有用信息,可以覆盖默认命令,让容器启动后执行一个不会退出的命令,然后进去手动排查:

docker run -it --entrypoint /bin/bash 镜像名

这样容器会停在bash里,你可以手动执行原来的启动命令,看看到底哪里报错。

8.2 端口冲突和网络不通

端口冲突的报错信息很明确:Bind for 0.0.0.0:8080 failed: port is already allocated。意思是宿主机的8080端口已经被占用了。解决办法是换一个端口,或者找到占用端口的进程把它停掉。

网络不通的情况稍微复杂一些。如果是从宿主机访问容器服务,检查端口映射是否正确,docker port 容器名可以查看映射关系。如果是容器之间互相访问,检查它们是否在同一个自定义网络里,用docker network inspect 网络名可以看到网络里有哪些容器。

还有一个容易忽略的点:容器里的服务如果只监听127.0.0.1,外部是访问不到的。必须监听0.0.0.0才行。这个坑我踩过,服务在容器里跑得好好的,就是外面访问不了,查了半天才发现是监听地址的问题。

8.3 磁盘空间被占满

Docker用久了,磁盘空间会被镜像、容器、数据卷慢慢吃掉。定期清理是个好习惯。

查看磁盘占用:

docker system df

这会显示镜像、容器、数据卷、构建缓存各自占了多少空间。

清理命令有几个层级:

docker system prune

这个命令会清理停止的容器、没有被使用的网络、悬空镜像和构建缓存。加-a参数会连没有被使用的镜像一起清理。加--volumes会连没有被使用的数据卷一起清理。

注意:docker system prune --volumes会删除没有被任何容器使用的数据卷。如果你的数据卷是手动创建、暂时没有容器在用,但里面存着重要数据,这个命令会把它删掉。用之前一定确认清楚。

8.4 常见问题速查表

问题现象可能原因排查方法
容器启动后立即退出主进程执行完就结束了docker logs看输出
端口映射不生效端口写反了或服务监听地址不对docker port检查映射,确认服务监听0.0.0.0
容器之间ping不通不在同一网络docker network inspect检查
镜像拉取慢网络问题配置镜像加速器
构建镜像很慢上下文太大加.dockerignore
数据卷挂载后权限报错容器内用户和宿主机用户UID不一致调整目录权限或指定用户
磁盘空间不足镜像和容器堆积docker system df查看,prune清理

9. 我个人的几条实操心得

最后分享几个我在实际使用中总结出来的习惯,都是踩过坑之后养成的。

第一,永远给容器起名字。不起名字的话Docker会分配一个随机字符串,过两天你根本记不住哪个容器是干什么的。起名字花不了几秒钟,但能省掉很多“这个容器是啥来着”的困惑。

第二,镜像标签不要用latest。前面说过,latest是浮动的,今天和明天可能不是同一个版本。用具体版本号,出了问题能快速定位到是哪个版本引入的。

第三,数据一定要挂出来。不管是数据库还是应用产生的文件,只要是需要保留的,统统挂到数据卷或者宿主机目录。容器应该是无状态的,随时可以删掉重建。

第四,Dockerfile里少用RUN。能合并的合并,能清理的清理。镜像小一点,拉取和部署都快一点。这个习惯在项目初期可能感觉不到好处,但服务多了之后差距很明显。

第五,定期清理。我一般每周跑一次docker system prune,把不用的东西清掉。磁盘空间这东西,你不主动管理,它就会悄悄被吃光。

这些习惯看起来都是小事,但积累起来能省掉很多麻烦。Docker本身不难,难的是养成规范使用的意识。先把基础操作跑通,然后在实际项目中慢慢体会这些细节的价值,比一上来就啃文档要有效得多。

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

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

立即咨询