写这篇教程的起因,是我见过太多人第一次接触Docker就卡在安装这一步:要么apt源指向官方地址半天拉不下来,要么装好之后不知道怎么和镜像、容器打交道,还有人干脆绕道去用Windows版的Docker Desktop,结果在Linux服务器上照样不会玩。其实Docker的安装和上手,本质上是两件事——先把守护进程跑起来,再学会管理镜像和容器。这两件事搞明白,后面学编排、学部署都是水到渠成的事情。
这篇文章我准备按"为什么要装Docker → Ubuntu怎么装 → CentOS怎么装 → 二进制怎么装 → 镜像命令 → 容器命令 → 踩坑实录"的顺序来写,全程围绕零基础视角,所有命令我都会解释它在干什么、为什么这么写。你可以把它当成一份可以直接跟着敲的参考手册,也可以当成理解Docker内部逻辑的入门读物。
1. 从"环境地狱"到容器化:为什么绕不开Docker
1.1 一个把系统装坏了的真实场景
先讲一个我早期吃过的亏。当时在一台Ubuntu服务器上部署一个Python项目,项目要求Python 3.8,但系统自带的是3.6。我为了保险,编译安装了3.8,然后手动改了update-alternatives把默认python3指向新版本。结果系统里一堆依赖Python 3.6的工具全部开始报错,apt都跟着出问题,最后只能重装系统。
这种"装一个软件、毁一个环境"的经历,是几乎所有Linux用户的共同记忆。依赖库版本冲突、多个项目需要不同版本的运行时、删除一个包时牵连删掉了一堆系统组件——这些问题不是靠细心就能避免的,根源在于应用和运行环境绑得太死。
Docker解决这个问题的思路很直白:把你应用需要的整个运行环境(代码、依赖、配置、系统库)一起打包成一个独立的镜像,然后用容器把这个镜像和宿主机隔离开来。你要部署多少个项目都行,每个项目用各自的镜像,互不干扰。系统坏了,把容器删了重建就是,不用动宿主机的一根汗毛。
1.2 Docker的三个核心概念:镜像、容器、仓库
在动手安装之前,务必先把三个概念分清楚,因为后面所有命令都围绕它们展开:
| 概念 | 通俗理解 | 类比 |
|---|---|---|
| 镜像(Image) | 一个只读的模板,包含程序运行所需的完整环境 | 乐高积木的说明书和零件包,本身不会运行 |
| 容器(Container) | 镜像创建出来的运行实例,是真正跑起来的进程 | 按说明书拼好的成品 |
| 仓库(Repository) | 集中存放和分发镜像的地方 | 积木零件包的上架货架 |
| 守护进程(Docker daemon/dockerd) | 负责管理镜像、容器、网络的常驻后台服务 | 乐高店的组装师傅 |
这里要特别强调:镜像和容器是"模板与实例"的关系。同一个镜像可以创建多个容器,每个容器都是独立的运行环境。你在一个容器里改了什么文件,不影响镜像,也不影响其他容器——因为容器有自己可写层。这就是后面讲docker commit和docker build时需要用到的底层逻辑。
1.3 这篇教程使用的基础约定
我假设你的环境是:
- Ubuntu 20.04/22.04 或 CentOS 7 系列
- 有root权限,或能用sudo执行命令
- 系统可以访问外网(安装包需要下载)
- 如果是内网环境,我会在第4节专门讲二进制安装方案
所有命令我按照"能直接复制"的标准写,但请你不要无脑粘贴,至少看一眼每条命令在干什么,这样后面出了问题才能自己排查。
2. Ubuntu下从零到可用的安装流程:以docker-ce方式为例
2.1 为什么不用apt install docker.io
很多Ubuntu入门教程会让你直接执行sudo apt install docker.io。这个命令确实能装出Docker,但它装的是Ubuntu系统源(universe仓库)里的版本,维护节奏远落后于Docker官方仓库。举个例子,我遇到过装了docker.io之后docker compose语法支持不全的情况,最后还得切换成官方docker-ce版本。
所以我的建议是:只要网络允许,一律装官方docker-ce系列包。这类包的命名是docker-ce、docker-ce-cli、containerd.io,分别对应主程序Docker二进制文件、客户端命令以及容器运行时管理器containerd。官方仓库通过APT源发布,安装方式稍复杂几步,但一劳永逸。
2.2 完整安装步骤(基于清华镜像源)
由于官方download.docker.com在国内的访问速度一直不太稳定,我习惯配置为清华TUNA镜像源。步骤拆解如下:
# 1. 更新系统软件源,让apt能识别最新软件包列表 sudo apt update # 2. 安装依赖工具:apt-transport-https用于HTTPS源,curl用于下载GPG密钥 sudo apt install -y apt-transport-https ca-certificates curl gnupg lsb-release # 3. 下载并信任Docker官方的GPG密钥 curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # 4. 向apt源列表写入Docker官方仓库(注意版本代号是通过lsb_release自动识别的) echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 5. 再次更新软件源,让apt识别刚才添加的Docker仓库 sudo apt update # 6. 安装Docker相关组件 sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin第3步有一个容易被忽略的细节:gpg --dearmor把GPG公钥从ASCII格式转成二进制格式,并放到/usr/share/keyrings/目录下。新版apt要求keyring文件以二进制存放在特定位置,否则会提示"Key is stored in legacy trusted.gpg keyring"的警告,后面每次apt update都会刷一行黄字。
安装完成之后,先不急着用。按下面几步验证和初始配置:
# 查看运行状态,应为active (running) sudo systemctl status docker # 查看版本信息 sudo docker version # 设置开机自启 sudo systemctl enable docker # 验证Docker能正常创建并运行容器 sudo docker run hello-worlddocker run hello-world如果顺利,会输出一段欢迎信息并退出,说明整个链路完全通了。
2.3 权限处理:告别每行命令都要sudo
默认情况下,执行docker命令需要root权限,因为守护进程/var/run/docker.sock这个socket文件属于root组。如果你不想每次都忍受sudo前缀,把当前用户加入docker组即可:
sudo usermod -aG docker $USER newgrp docker这里特别注意:加组之后需要重新登录shell才能真正生效。如果你是在SSH里操作,直接退出重新登录一次。另外,把用户加入docker组意味着该用户拥有和root几乎等同的权限(docker可以挂载宿主目录、操作宿主网络),这在多用户机器上需要谨慎对待——如果只是自己开发机,完全不用顾虑。
3. CentOS 7上安装Docker:yum源的配置与systemd管理的细节
3.1 系统源里的docker与官方docker-ce的区别
CentOS 7自带源里也有一个docker,包名叫docker,来自extras仓库。问题是它的版本一般停留在2017年左右,很多新特性(比如BuildKit构建缓存)都没有。所以和Ubuntu一样,在CentOS上也走官方docker-ce源。
CentOS官方源配置相对简单,有两种方式:手动写repo文件,或者用yum-config-manager自动添加。我推荐后者,少打几个字,也不容易写错路径。
3.2 基于清华源的一键式安装
# 安装yum-config-manager工具(它属于yum-utils) sudo yum install -y yum-utils # 添加Docker官方源(走清华镜像) sudo yum-config-manager --add-repo https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo # 安装Docker相关组件 sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # 启动并设置开机自启 sudo systemctl start docker sudo systemctl enable docker # 验证 sudo docker run hello-world如果是CentOS 7.9,我强烈建议安装后先看一眼docker info里的Storage Driver字段。默认通常是overlay2,这是目前最稳定的驱动。如果显示devicemapper,说明你的系统可能启用了老式的LVM瘦池方案,这个方案在某些内核版本上会触发"镜像层无法删除"的问题,建议手动配置daemon.json切换到overlay2:
sudo mkdir -p /etc/docker cat <<EOF | sudo tee /etc/docker/daemon.json { "storage-driver": "overlay2" } EOF sudo systemctl restart docker切换驱动后,原有的镜像和容器会看不到(数据没有丢,只是换驱动后不可见)。建议在安装完Docker之后、正式拉镜像之前就把驱动确认好,切驱动之后重启Docker即可生效。
3.3 firewalld与Docker端口发布的一个隐藏坑
CentOS 7默认开着firewalld,装完Docker后如果你立刻用-p 8080:80这样的参数发布容器端口,会发现公网访问不通,但服务器本机curl却是好的。原因是Docker在iptables里创建的规则和firewalld的管理方式有冲突,firewalld重启时会对Docker已有的规则执行flush。
一种处理方式是把firewalld彻底停掉(服务器里如果没其他端口管理需求,这是最省事的路子):
sudo systemctl stop firewalld sudo systemctl disable firewalld另一种做法是保留firewalld,但显式放行对应端口:
sudo firewall-cmd --permanent --add-port=8080/tcp sudo firewall-cmd --reload我更推荐第一种——Docker对端口的NAT管理已经比较完善,再叠一层firewalld反而容易产生规则打架的问题。
4. 二进制安装:不依赖任何包管理器也能跑Docker
4.1 什么场景下必须用二进制安装
包管理器安装固然省事,但有几个场景你只能选二进制:
- 服务器完全内网隔离,无法访问任何外部yum/apt源
- 系统版本太旧,比如CentOS 6或老版本Ubuntu,Docker官方包管理器源已经放弃支持
- 公司内部对软件版本有强管控要求,不允许apt/yum自动安装
- 想固定某个具体版本的Docker用于生产环境,避免包管理器静默升级
二进制安装的本质很简单:Docker官方把编译好的静态文件打包成一个tar.gz压缩包,解压出来就是全套可执行文件,拷贝到PATH目录下即可运行。这种方式的优点是不产生系统级的依赖残留,删的时候直接删文件就行。
4.2 二进制包的下载、解压与启动
以Docker 24.0.7为例,完整过程如下:
# 1. 进入下载目录(请根据实际网络情况选择可访问的镜像下载站) cd /opt wget https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz # 2. 解压 tar xzf docker-24.0.7.tgz # 3. 把解压后的文件复制到/usr/bin目录 cp /opt/docker/* /usr/bin/ # 4. 启动docker守护进程(这在普通终端运行时,进程会占用前台) dockerd --version注意第3步,解压出来的目录里包含docker、dockerd、containerd、containerd-shim-runc-v2、ctr、docker-init等文件。其中dockerd是核心守护进程,docker是客户端命令,其余是底层容器运行时组件。把整个目录都拷到/usr/bin,后面调用时互为依赖才能正常找到彼此。
直接执行dockerd &也不是不行,但它没有自动重启策略、没有日志转储,进程挂了很麻烦。更好的做法是手动创建一个systemd服务单元,让二进制方式安装的Docker也能实现开机自启和异常重拉。下面是我在内部服务器上使用的unit文件:
[Unit] Description=Docker Application Container Engine (standalone binary install) Documentation=https://docs.docker.com After=network-online.target firewalld.service Wants=network-online.target [Service] Type=notify ExecStart=/usr/bin/dockerd ExecReload=/bin/kill -s HUP $MAINPID LimitNOFILE=infinity LimitNPROC=infinity LimitCORE=infinity Restart=always RestartSec=2 TimeoutStartSec=0 Delegate=yes KillMode=process [Install] WantedBy=multi-user.target将文件保存为/usr/lib/systemd/system/docker.service后执行:
sudo systemctl daemon-reload sudo systemctl start docker sudo systemctl enable docker4.3 二进制安装与包管理器安装的差异总结
| 对比项 | 包管理器安装 | 二进制安装 |
|---|---|---|
| 版本管理 | 随源更新,可能自动升级 | 完全固定,不会自动变化 |
| 依赖管理 | 自动处理glibc/iptables等依赖 | 自带全部依赖,需要手动拷贝文件 |
| 删除方式 | yum/apt remove,清理不完整 | 删除二进制文件即可 |
| systemd服务 | 安装后自动生成 | 需要手动创建 |
| 适用场景 | 日常开发、外网服务器 | 内网环境、版本强管控的生产环境 |
如果你需要在多台内网机器上重复部署,把这套systemd文件和tar包一起打包内部发布,底层逻辑完全一致。
5. 镜像操作:先管理好你的"乐高零件包"
5.1 镜像的拉取、查看与删除
Docker安装好后第一个常规动作是拉镜像。核心命令就这几个:
# 从默认仓库拉取镜像,冒号后是标签(tag) docker pull nginx:latest # 查看本地已有的镜像 docker images # 或者用新版命令 docker image ls # 删除镜像(注意先确保没有容器在使用它) docker rmi nginx:latestdocker images输出里有一个"SIZE"字段,这里必须纠正一个常见误解:多个镜像共用基础层时,SIZE列的数值是相加的,但磁盘实际占用远小于这个总和。比如你拉了ubuntu:20.04、mysql:8.0、nginx:latest三个镜像是,底层可能共享了同一批基础层,实际占用的空间比"每个镜像单独拉一次基础层"小很多。这个机制叫联合文件系统(OverlayFS),理解它有助于解释"为什么占用和想象中不一样"。
docker rmi删除镜像时如果还有容器在用,会报错"unable to remove repository reference"。你需要先docker rm删除容器,再删镜像。这也是新手最容易碰到的删不掉问题。
5.2 镜像的导入导出:不依赖仓库也能迁移镜像
生产环境经常遇到"这台机器上的镜像要想办法搬到另一台没有外网的机器上"的需求。Docker提供了两组对用的命令:
# 把镜像保存成tar文件 docker save -o nginx-image.tar nginx:latest # 在目标机器上加载 docker load -i nginx-image.tar另一组命令是docker export和docker import,它俩和save/load有本质区别:
docker save保存的是"镜像"本身,包含完整的分层结构和历史信息,加载后还是同样的镜像docker export导出的是"容器"的文件系统内容,不含元数据、不含分层信息,加载进去后镜像历史全部丢失
所以日常迁移镜像请优先用save/load,不要混用。我曾见过有人用export备份容器,结果恢复出来的镜像不知道原始启动命令是什么,只能手动重新拼docker run参数,非常折腾。
5.3 镜像加速配置:解决拉取超时的关键
拉镜像超时是国内使用Docker几乎必然会遇到的问题。解决办法是配置镜像加速器。Docker daemon支持在/etc/docker/daemon.json里声明多个registry-mirrors,拉镜像时会优先尝试它们:
sudo mkdir -p /etc/docker cat <<EOF | sudo tee /etc/docker/daemon.json { "registry-mirrors": ["https://docker.m.daocloud.io"] } EOF sudo systemctl restart docker配置完后可以执行docker info,在输出的Registry Mirrors一栏确认生效。如果你所在的环境能直接访问官方仓库不超时,也可以不配,优先保证其他配置正确就行。
6. 容器操作:日常高频命令的生命周期与运维场景
6.1 创建一个容器的完整参数:run命令深度拆解
镜像管理是前提,真正和工作打交道的是容器管理。docker run是最核心的命令,没有之一。众新人在刚开始时容易看到一长串参数就头大,其实拆开看就三件事:要跑什么、怎么隔离、资源如何分配。
docker run -d \ --name my-web \ -p 8080:80 \ -v /opt/nginx/html:/usr/share/nginx/html \ --restart unless-stopped \ nginx:latest逐行解释:
-d:后台运行。不加这个参数,容器直接占用当前终端,按Ctrl+C容器就停了--name my-web:给容器起名字,后续stop/rm都靠名字定位标签,省去记容器ID-p 8080:80:端口映射,格式是"宿主机端口:容器内端口"。访问宿主机的8080端口等于访问容器里的80端口-v /opt/nginx/html:/usr/share/nginx/html:数据卷挂载,把宿主机目录映射到容器内目录,让容器可以读取或写入宿主机的文件--restart unless-stopped:设置重启策略,Docker守护进程启动时自动拉起该容器(除非你手动stop过它)nginx:latest:镜像名
目睹完整的容器的生命周期,还有一套常用的状态管理命令:
# 查看正在运行的容器 docker ps # 查看所有容器(包括已退出的) docker ps -a # 停止容器(发送SIGTERM,优雅停止) docker stop my-web # 启动一个已停止的容器(不会重新创建,原配置全部保留) docker start my-web # 重启 docker restart my-web # 删除容器 docker rm my-web # 强制删除(即使容器还在运行) docker rm -f my-web这里的一个关键细节要记牢:docker stop只是停止进程,容器本身还在(docker ps -a能看到),随时能start;docker rm才是真正销毁容器。很多新手把stop和rm混为一谈,结果发现"停了之后东西还在占用端口",其实是容器没删掉。
6.2 进入容器、看日志、拷文件:排障三件套
容器内出现异常时,进入容器排查、查看日志、把配置文件拷出来是日常工作。这套命令我称之为排障三件套:
# 进入容器的交互式shell docker exec -it my-web /bin/bash # 查看容器的全部日志(跟踪方式与tail类似) docker logs my-web docker logs -f --tail 200 my-web # 宿主机与容器间双向拷贝文件 docker cp /tmp/nginx.conf my-web:/etc/nginx/nginx.conf docker cp my-web:/var/log/nginx/access.log /tmp/access.log需要注意,docker exec -it <容器名> /bin/bash能成功的前提是容器的主进程还在运行。如果容器已经退出了,先用日志查崩溃原因,而不是试图进入一个不存在的运行环境。另外,容器镜像不一定装了bash,有些精简镜像只有/bin/sh,这时候用/bin/sh或改用docker exec -it <容器名> sh。
docker logs是排障的优先手段。容器里的主进程把日志写到stdout/stderr,Docker会收集到日志文件里。如果容器启动后立刻退出,docker logs能看到退出前的报错信息,这几行字就是排查入口。
6.3 容器网络模式:bridge、host、none的选择
Docker默认创建一种名为bridge的虚拟网络,每个容器在一个私有的网段内,通过NAT访问外部,外部要访问容器则靠前面讲过的-p映射。这个模型安全性好,适合大多数应用。
但有些场景想让容器直接用宿主机的网络栈,比如要在容器里跑一个服务,希望它直接监听宿主机的某个端口、希望容器能直接访问宿主机的localhost地址——这时候可以用host网络模式:
docker run -d --network host --name my-app my-image网桥模式下,需要先创建自定义网络让多个容器互相通信;host模式则天然共享宿主机网络。使用host模式时注意,容器内进程监听端口等于宿主机监听端口,不会再经过NAT转换。
如果什么都不想配,只是临时跑个一次性命令(比如测试数据库连接),可以用none模式彻底关闭网络:
docker run --rm --network none curlimages/curl curl https://example.com6.4 资源限制:防止容器吃光宿主机
生产环境不设资源限制,等于把宿主机交给运气管。Docker在run时支持直接限制CPU和内存:
# 限制使用最多2个CPU核心、最多1GB内存,且不允许swap docker run -d --name limited --cpus=2 --memory=1g --memory-swap=1g nginx:latest压测观察时配合检查资源状态,通常用docker stats可以看实时指标:
# 实时查看所有容器资源占用 docker stats # 输出一次后就退出,适合脚本抓取 docker stats --no-stream小知识:--memory-swap=1g和--memory=1g同时设置意味着容器不允许使用任何swap 空间,内存使用超限时进程会被OOM杀掉。如果只想允许用swap,就把--memory-swap设为2倍memory值。
7. 零基础踩坑实录:常见问题的完整排查思路
7.1 拉取镜像超时或卡住不动
现象:执行docker pull nginx:latest后长时间没有反应、过一会报"Get https://registry-1.docker.io/v2/: net/http: TLS handshake timeout"。
这是我的排查链路:
# 第一步:看看是不是网络问题 curl -I https://mirrors.tuna.tsinghua.edu.cn/docker-ce/ # 第二步:检查daemon.json是否配置了加速器 cat /etc/docker/daemon.json # 第三步:确认配置生效 docker info | grep -A2 "Registry Mirrors"如果加速器配置没问题但依然超时,看看是否是DNS解析问题。用nslookup registry-1.docker.io确认域名能解析,如果DNS本身有问题,需要在daemon.json里补充dns字段:
{ "dns": ["223.5.5.5","114.114.114.114"] }然后重启Docker。这个问题的常见根源是系统DNS无法有效解析外网域名,导致Docker在TLS握手阶段就超时。
7.2 容器启动后马上退出
现象:docker run -d nginx执行成功,但一回头docker ps看不到这个容器,用docker ps -a能看到STATE是Exited。
排查步骤:
# 第一步:查看退出日志,这是最关键的一步 docker logs <容器名> # 第二步:查看退出码 docker inspect <容器名> --format='{{.State.ExitCode}}'退出码对应含义:0表示正常退出(说明容器主进程主动结束了),125/126/127表示docker本身执行出错,其余大于0的码通常是容器内应用自己的错误码。比如MySQL镜像如果没有正确环境变量就会因为初始化失败退出,日志里能看到完整报错。
对于后台服务型容器(nginx、mysql、redis等),它们的主进程是守在前台的。如果你在run命令后直接一行写成了nginx -d这种后台方式启动,Docker会认为主进程已退出,容器立刻关闭——你要看清镜像默认的启动命令,别额外指定成后台模式。
7.3 容器内无法解析域名
现象:容器能启动,docker exec进去之后apt update报could not resolve host,或者curl外网域名报名字解析错误。
原因分析:容器内DNS依赖宿主机网络配置。如果宿主机用的是systemd-resolved(Ubuntu 20.04及以上默认),/etc/resolv.conf会指向127.0.0.53这个本地回环地址。容器网络在默认bridge模式下访问不了这个地址,就会出现域名解析不到的现象。
解决方案:
打开daemon.json设置一个可用的DNS服务器(比如给国内用户常见的公共DNS):
{ "dns": ["223.5.5.5", "119.29.29.29"] }然后systemctl restart docker重建容器生效。
7.4 安装MySQL失败的真实处理流程
"docker安装mysql失败"是网上出现频次非常高的热搜词,我之前也帮同事排查过一个典型案例,值得完整展开。
场景:同事执行docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0,立刻报"bind: address already in use"。
排查链路:
# 第一步:确认谁占用了3306端口 ss -tlnp | grep 3306看到宿主机本来就已经装了一个MySQL,端口被占。这是最直接的原因——-p 3306:3306要求宿主机端口3306空闲,但宿主机自己的MySQL已经监听了。
解决思路:改容器映射的宿主机端口,比如3307:
docker run -d \ -p 3307:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -v /opt/mysql-data:/var/lib/mysql \ --name mysql8 \ mysql:8.0然后docker logs mysql8观察初始化日志,等输出"ready for connections"就可以连了。这一步做完,数据库安装这件事才算真正落地。
这个案例本质上是"端口冲突"问题,但排查思路值得借鉴:看到报错不要先问为什么失败,第一动作永远是看日志和看端口。
7.5 磁盘空间被Docker悄悄吃光
现象:df -h里/目录使用率100%,但du查完所有目录也没有明显大文件。很可能是overlay2的数据量失控。
排查与清理:
# 查看Docker占用明细 docker system df # 一键清理所有悬空镜像和停止的容器、未使用的网络和构建缓存 docker system prune -a注意docker system prune -a会把所有未被容器使用的镜像全部删掉,执行前确认你不是在依赖本地镜像做实验。另一个常见的空间黑洞是容器日志文件无限增长,/var/lib/docker/containers/<容器ID>/<容器ID>-json.log可能会膨胀到几十GB。可以在daemon.json里做日志轮转限制:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } }这个配置只对新建容器生效,已有容器改配置后需要重建容器才不占用逻辑。养成定期执行docker system df的习惯,比等到磁盘报警再去清理省心得多。
最后再分享一个我自己的使用习惯:不要用latest标签上生产的机器。latest看起来省事,但它指向的镜像随时可能被更新,两个月前和两个月的部署版本可能完全不一致。我通常固定到具体版本号,比如mysql:8.0.36,并在镜像构建或拉取的脚本里做好版本记录。Docker本身只是一个工具,真正让容器发挥价值的是你对版本、资源和生命周期的管理纪律。把这些命令和思路吃透,你离"熟练使用Docker"其实只差日常的反复操作了。