☰
Ubuntu安装Docker完整指南:源选择到安全加固与排障
2026/10/22 3:50:54 网站建设 项目流程

最近又有人来问我“Ubuntu安装Docker是不是一条命令就搞定”。我一般会把这个回答拆成两半:装一个能跑 demo 的 Docker,确实一条命令就够了;但要装出一个能长期用、升级不翻车、排障不抓狂的 Docker 环境,还是得把安装路径选对,把权限、镜像源、日志、数据卷这些配置提前想清楚。这篇内容就把 Ubuntu 安装 Docker 和日常使用这个主题拆开聊,覆盖从 apt 源选择到容器资源限制、安全加固、常见报错排查的完整链路,适合刚在虚拟机或云主机上入门 Docker 的朋友,也适合装了好久但一直凭感觉敲命令、没系统整理过思路的人。

1. 安装方式选不对,后面全是暗坑

1.1 四条常见安装路径有什么差别

Ubuntu 下装 Docker,绕不开这几种方式:发行版自带的 apt 包、Docker 官方 apt 仓库、官方提供的 convenience script、以及 Snap 包。很多人第一反应是sudo apt install docker.io,省事是真省事,但这套包本质上是发行版维护者重新打包过的版本,更新节奏通常比 Docker 上游慢一大截。你今天装的是 24.04 自带的 Docker,过半年你发现上游早支持了新特性,你还被按在老版本上,遇到 bug 连修复补丁都要等发行版那边维护。

Docker 官方 apt 仓库是生产环境里最主流的做法。它一样走 apt,但软件源指向download.docker.com,你装到的是docker-ce、docker-ce-cli这些上游直接发布的二进制包。好处是版本新、升级路径稳定,apt 的依赖管理还能帮你处理 containerd、runc 这些底层组件之间的关系,出问题也好排查。

convenience script 是官方提供的curl -fsSL ... | sh一键脚本,装起来速度最快,但我不建议在正经服务器上用。主要原因是你没法精确控制脚本做了什么,它偷偷改了哪些源、加了哪些 key,你很难一眼审查完。这违背了服务器管理里最基本的“操作可审计”原则。我自己只在帮人临时搭学习环境时用过,生产环境从来不用。

Snap 版 Docker 也经常被提到。它优点是有 Snap 商店的自动更新,但劣势也很明显:Snap 使用的挂载和权限模型与普通 Docker 环境不同,很多人装完发现/var/lib/docker路径都找不到,或者 with systemd 集成时出一些莫名其妙的问题。如果就想跑个测试,当我没说;如果想正经用 Docker,优先考虑前面两种。

1.2 我的选型建议

默认情况下,我推荐 Ubuntu 用户走 Docker 官方 apt 仓库。原因不复杂:Docker 本身迭代快,容器运行时、网络模块、存储驱动经常有安全修复,长期停留在老版本是很大的隐患。官方仓库能让你用普通 apt 工作流去管理升级,apt update之后看到docker-ce有更新,直接apt upgrade就行。而且只有官方仓库能方便地同时安装 buildx 和 compose 插件,这两个插件是现代 Docker 工作流里几乎绕不开的东西。

如果机器完全离线,或者公司内部有严格的软件采购流程,那就别折腾在线源了,去 Docker 官方页面下载对应 Ubuntu 版本的 deb 包,本地安装即可。注意要按依赖顺序装:containerd.io、docker-ce-cli、docker-ce,最后再装插件包。顺序搞反了,apt 会提示依赖缺失。

另外,无论选哪条路,装完第一步都应该执行docker version和docker info,确认 Server 端不是被当成“未运行”的假安装。我见过不止一个案例:客户端装好了,daemon 没起来,用户敲docker ps报一堆连接错误,然后开始怀疑人生。

2. 官方仓库安装的完整步骤与权限细节

2.1 先把系统环境清干净

如果你想从官方仓库装,第一步不是直接apt install,而是把可能残留的旧包清掉。Ubuntu 默认源里可能带着docker.io、docker-engine、containerd、runc这些,如果之前手动装过,会跟官方包冲突。执行一遍:

sudo apt remove docker docker-engine docker.io containerd runc

这一步只是卸载,不会删/var/lib/docker里的数据。你倒是要留意,如果以前跑过容器,旧的镜像和容器数据都留在/var/lib/docker下面,卸掉包之后它们还在,等新 Docker 装好后会继续读到,有时会因此出现“旧镜像还在、新旧配置不一致”的情况,这时候就要自己决定是保留还是清空。

接着安装基础依赖:

sudo apt update sudo apt install ca-certificates curl

ca-certificates和curl是后面下载 GPG key 和加源的基础。注意一定要先apt update,因为刚清掉的包可能让本地索引和实际软件状态不一致,装依赖时容易碰到 404。

然后准备 GPG key。新版本 Ubuntu 推荐把 key 放到/etc/apt/keyrings目录:

sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc

很多人会忽略权限,key 文件要是其他用户也能写,apt 会警告,虽说不至于立刻不能用,但还是按官方建议把权限做好,少点噪音。

添加 apt 源这一行,建议把系统版本代号写对。Ubuntu 24.04 就是noble,22.04 是jammy,20.04 是focal。一条更稳妥的方式是直接让 shell 帮你取:

echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

这里的关键点是用VERSION_CODENAME自动获取版本代号,而不是手敲。手敲容易出错,尤其是你从某个教程复制命令时,人家写的是旧版本代号,你新系统照着跑,后面 update 就会 404。

2.2 安装和验证

源配好后,再apt update,然后安装完整组件:

sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

这四个包分别是 daemon、客户端、容器运行时、buildx 构建插件和 compose 编排插件。我不建议只装docker-ce,因为现在很多操作依赖 compose 插件,单装一个主包后面还得补,不如一次到位。

装完把服务打开,并设为开机自启:

sudo systemctl enable --now docker sudo systemctl status docker

看到active (running)就说明 daemon 起来了。再跑一次官方测试镜像验证完整链路:

sudo docker run hello-world

能打印出 “Hello from Docker!” 就代表整个拉取、创建、运行流程都正常。这里注意我特意加了sudo,原因后面会讲。

最后确认一下客户端和 daemon 版本是否一致:

docker version

如果 Client 和 Server 都正常输出,说明装到位了。如果只有 Client 有信息、Server 显示 error,多半是 daemon 没起来,或者你当前用户连不上 socket。

2.3 普通用户执行 docker 命令的正确打开方式

只要用sudo才能跑 docker,很快你就会烦。更合理的做法是把需要执行 docker 命令的用户加入docker组:

sudo groupadd docker sudo usermod -aG docker $USER

然后退出当前登录会话重新登入,或者执行newgrp docker让组权限在当前 shell 里生效。之后就不用每次敲sudo docker了。

这里必须把丑话说在前面:能执行docker命令的用户,等于有权限操作 Docker socket,而这个 socket 通常可以提供宿主机的 root 权限,本质上和拥有 root 权限没太大差别。所以千万不要为了让某个人方便用 docker 就把他随便加进docker组,除非你信任他能拥有 root 级别的系统控制权。很多团队踩过这个坑:为了方便,把开发账号加进了 docker 组,结果某个不在预期内的容器挂载了宿主机目录,一条命令就能改掉系统关键文件。

我自己的习惯是,服务器上单独建一个部署用户,比如deploy,然后只把这个用户加进 docker 组。应用跑在这个用户下的 shell 环境里,权限边界更清楚,出问题也好定位。

3. 镜像、容器、网络:日常使用必须拎清的几个概念

3.1 镜像和容器到底什么关系

很多人把镜像和容器当成两个东西背概念,我常用的类比是:镜像是一台电脑的完整装机盘,容器是你用它装好系统后正在运行的那台机器。装机盘可以复制出多台机器,互不干扰;你在一台机器里装的软件、改的配置不会自动变到另一台。所以你会看到同一个镜像能启动好几个容器,每个容器里都有自己的文件系统状态。

底层实现上,镜像是由多层只读文件系统叠加出来的,容器则在最上面加一个可写层。这个设计的直接好处是镜像复用效率高,你拉一个镜像,多个容器共用只读层,占用的额外磁盘空间主要是各自的可写层。很多人发现自己磁盘没满却被占了很多,多半是镜像和容器堆积,后面会专门讲清理。

3.2 run 命令里最常配的几个参数

日常使用频率最高的一条命令就是docker run,但裸跑一个容器往往不够,至少要把这几类参数配好:

docker run -d \ --name web \ --restart unless-stopped \ -p 8080:80 \ -v /opt/html:/usr/share/nginx/html:ro \ nginx:stable

这条命令我拆开解释一下:

  • -d表示后台运行。不加它,容器会挂在前台,Ctrl-C 之后容器就停了,不适合长期服务。
  • --name web给容器起名字。起了名字之后,后续docker logs web、docker exec -it web bash就不用记一串随机 ID 了。
  • --restart unless-stopped是重启策略。它表示 docker daemon 启动时自动拉起这个容器,除非你手动 stop 过。这对服务器场景非常实用,系统重启后服务能自动恢复。
  • -p 8080:80是端口映射,宿主机 8080 映射到容器 80。注意容器内很多服务默认只监听自己那个端口,不做映射宿主机是访问不到的。
  • -v /opt/html:/usr/share/nginx/html:ro是目录挂载,把宿主机站点目录挂进容器,并以只读方式挂载。后面数据持久化部分会细讲。
  • 末尾的nginx:stable是镜像名加 tag,可以用latest,但生产环境我更推荐指定明确的版本 tag,至少你要知道今天部署的是哪个版本。

除了这些,-e环境变量也经常用,比如给数据库容器设置密码,如下:

docker run -d --name pg -e POSTGRES_PASSWORD=xxx -p 5432:5432 postgres:16

容器初始化脚本会读取这些环境变量来创建数据库用户。这些参数看着简单,但组合起来能覆盖大多数单容器场景。

3.3 容器生命周期和清理

容器的常见状态有created、running、paused、exited、dead。很多人对exited有误解,以为容器停了就“没了”,其实它只是进程退出、容器对象还在。这就需要配一套命令习惯:

  • docker ps:只看 running 状态的容器。
  • docker ps -a:看所有容器,包括已退出的。
  • docker start web/docker stop web/docker restart web:控制指定容器的启停。
  • docker logs -f web:跟踪容器日志,排查问题第一件事就是看它。
  • docker exec -it web bash:进入容器里执行命令,适合调试。注意容器里未必有 bash,有的最小镜像只有sh。
  • docker rm web:删除容器。配合docker rm -f web可直接删运行中的容器。

镜像层面的命令则更少:docker images列出本地镜像,docker pull nginx:stable拉取指定镜像,docker rmi nginx:stable删除镜像。当你删镜像时提示“被容器使用”,说明还有容器引用它,先把相关容器删掉再操作。

最后是清垃圾文件:

docker system prune

默认只清不用的容器、网络和悬空镜像,不会乱删 tag 镜像。后面加-a才会连没被容器使用的所有镜像一起删,用的时候一定要看清输出内容。我见过有人一句docker system prune -a --volumes把本地所有镜像和没名字的数据卷全清了,当场后悔。

4. daemon.json 里的实用配置:加速、日志上限、数据目录迁移

4.1 先理解 daemon.json 是什么

Docker daemon 的全局配置集中在/etc/docker/daemon.json。这个文件不是默认存在的,你需要自己创建。以前我见过有人把配置写成docker run的参数反复传,每个容器都得带,其实很多基础配置更适合写在 daemon.json 里,一劳永逸。

改完这个文件通常要重启 docker:

sudo systemctl restart docker

注意重启会把所有正在运行的容器先停掉,所以生产环境尽量安排在维护窗口做。而且如果配置文件内容写错,docker 可能会启动失败,systemctl status docker会告诉你哪里不对,别在没备份的情况下盲改。

4.2 公共镜像加速服务的配置

在部分网络环境下,直接从 Docker Hub 拉镜像会明显感觉到慢,或者偶尔超时。解决思路通常是配置公共镜像加速服务。写法如下:

{ "registry-mirrors": [ "https://your-mirror.example.com" ] }

不同云服务商提供的镜像加速地址并不一样,有些需要登录账号后获取专属地址,有些是共用的公共地址,具体地址请去你所用服务商的官方文档里找。我不建议随便找来路不明的镜像加速源填进去,安全性和稳定性都不可控。

配置完可以通过这条命令验证是否生效:

docker info | grep -A1 "Registry Mirrors"

看到你配的地址出现在列表里就说明 daemon 已经读到配置了。这里有个容易踩的坑:registry-mirrors只对docker pull时的 Docker Hub 镜像生效,如果你拉的是其他第三方镜像仓库的镜像,比如某个私有仓库,这个配置是无感的。

4.3 日志别等撑满磁盘才处理

容器日志默认由json-file驱动写入/var/lib/docker/containers/<id>/下,同一个容器的 stdout 日志会不停追加。如果不设上限,一个打印频繁的容器可以把磁盘写满。我处理过一台服务器,就是某容器每秒钟打几百行日志,一天下来几十 GB,整个分区直接No space left on device。

推荐在 daemon.json 里直接设旋转:

{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }

意思是单个日志文件超过 10MB 就切分,最多保留 3 个文件。注意这个配置只对新建容器生效,已经创建的容器不会自动切换,需要重建容器才会拾取新的日志配置。所以最好在搭建环境的时候就配好,别等服务出问题再补。

接下来安排一个定时清理容器日志的脚本?你可以用journalctl之外的方式,比如定期执行:

sudo find /var/lib/containers -name "*.log" -type f -delete

但我不推荐硬删,因为正在写的日志文件直接删掉,fd 还开着,容器输出会继续占空间。用 Docker 自己的 rotation 配置才是正规解法。

4.4 数据目录迁移到足够大的分区

Docker 默认数据目录是/var/lib/docker。如果系统盘只有几十 GB,而你的镜像和容器数据都堆在上面,很快会出问题。迁移方法不复杂:

  1. 停止 docker:sudo systemctl stop docker。
  2. 拷贝数据到新目录,比如/data/docker:sudo rsync -av /var/lib/docker/ /data/docker/。
  3. 在/etc/docker/daemon.json里加"data-root": "/data/docker"。
  4. 启动 docker:sudo systemctl start docker。

启动前一定要先确认复制完成,否则 Docker 会认为新目录是全新环境,所有镜像都不见了。另外,保险起见先备份,别把 rsync 中断当玩笑。

5. 从单容器到多服务:数据持久化与容器编排实战

5.1 数据放容器里,重启一次就没了

这是新手最容易忽略的一点。容器可写层在容器删除时会一起被清掉,数据库、上传目录这些必须放持久化存储里。Docker 提供了命名数据卷和绑定挂载两种形式。

命名数据卷是 Docker 自己管理的目录,创建后挂到容器:

docker volume create pgdata docker run -d --name pg -v pgdata:/var/lib/postgresql/data postgres:16

数据卷由 Docker 管理,备份时可以针对性操作,推荐用于数据库这类需要稳定存储的场景。

绑定挂载则是直接把宿主机路径挂进容器,如上一节写的-v /opt/html:/usr/share/nginx/html:ro。适合配置文件、代码目录这类需要你直接从宿主机编辑的场景。两者在docker inspect里的 Mounts 部分都能看到详细信息。

特别注意权限问题:容器内的进程如果以 root 运行,写入挂载目录时会在宿主机生成 root 所有权的文件。你后面在宿主机上想删改这些文件,会发现权限不够,这就是很多人遇到的“目录删不掉”的真相。解决办法可以是让容器进程以非 root 用户运行,或者主动把目录所有权调整到对应 UID/GID。

5.2 用 Compose 编排一个多容器示例

单容器用docker run没问题,但一旦有多个服务,比如一个 Web 服务依赖一个缓存服务,我再推荐用docker compose管理。平时不用写长命令行,所有配置放在一个compose.yaml里,还能用版本管理工具跟踪改动。

一个最简示例:

services: web: image: registry.example/app:latest ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html:ro restart: unless-stopped cache: image: redis:7-alpine restart: unless-stopped

在compose.yaml所在目录执行:

docker compose up -d

这个命令会拉取镜像、创建网络、启动服务。-d表示后台运行。后续操作为:

docker compose ps docker compose logs -f docker compose down

docker compose down默认不会删除数据卷,这通常是我们想要的行为,但也因此容易被忽略:Compose 服务重建后数据还在,其实是数据卷起了作用。如果你想彻底清掉,才需要加-v参数,但要非常确认自己真的想删除所有数据。

Compose 自动创建的网络里,服务之间可以直接用服务名互相访问。比如web容器里连接cache,直接连主机名cache而不是 IP,这个机制叫服务发现,由 Docker 内置 DNS 提供。我经常强调,容器编排能不大改代码就别改,让网络层面的服务名互通替你解决配置问题。

5.3 给容器设置资源上限

默认情况下,单个容器可以消耗宿主机所有可用 CPU 和内存,这在多容器共存的机器上非常危险。一个内存泄漏的容器能把整台机器拖到 OOM。所以资源限制我从一开始就会加。

运行参数方式:

docker run -d --name worker --memory="512m" --cpus="1.0" --pids-limit 256 worker-image

含义分别是内存上限 512MB、最多使用 1 个核心、最多创建 256 个进程。Compose 里对应写法:

services: worker: image: worker-image deploy: resources: limits: cpus: "1.0" memory: 512M

这里的--memory是硬限制,容器超过会触发 OOM,可能被系统杀掉。--cpus限制 CPU 时间片比例,不会直接杀死容器,但能防止单容器占满宿主机。--pids-limit可以防止容器内进程 fork 炸弹,服务器场景强烈建议加。

6. 权限隔离与安全加固,别让容器变成裸奔

6.1 容器里的 root 是“假 root”,但风险依然大

默认进入容器后你看到的用户是 root,这个 root 和宿主机 root 的权限并不完全等价,有 Linux 内核用户命名空间和各种权限机制隔着。但只要没配置用户命名空间映射,容器里的 UID 0 在处理挂载目录时,宿主机视角下依然是 UID 0,这意味着一旦容器被攻破,攻击者在宿主机上的恶意操作难度低很多。

所以安全实践是明确指定容器运行用户:

docker run -d --user 1000:1000 -v /home/app/data:/data myapp:latest

用--user 1000:1000指定 UID/GID,容器内进程权限就被限制在普通用户级别。缺点是如果镜像里某些目录只有 root 能写,启动可能报权限错误,但这是安全的代价。很多基础镜像通过创建非 root 用户并建议使用该用户运行,属于更好的镜像设计。

6.2 干掉不需要的权限

容器不像虚拟机,没必要给全部内核权限。最典型的错误是图省事加--privileged,这等于把宿主机绝大多数设备访问能力和内核权限都给了容器。绝大多数场景根本不需要,禁止使用。

更精细的做法是删除默认 capabilities,只保留必需的:

docker run -d \ --cap-drop ALL \ --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges:true \ --read-only \ myapp:latest

--cap-drop ALL先把所有 privilege 清掉,再仅添加例如NET_BIND_SERVICE这种让 Web 服务监听低端口的权限。--read-only让根文件系统挂载为只读,容器里想往它自己的/写文件都写不进去。如果应用还需要写临时目录,可以单独挂一个 tmpfs:

docker run -d --tmpfs /tmp --read-only myapp:latest

这些参数组合起来,会让容器内部的攻击面小很多,虽然会带来一点点使用上的不便,但对在公网暴露的服务来说值得。

6.3 秘密信息别直接出现在命令行和配置文件里

很多人会把数据库密码写进docker run -e PASSWORD=123456,这个命令可能在 shell history、systemd 配置里留下记录,非常危险。非交互式场景下尽量用--env-file:

docker run -d --env-file .env myapp:latest

.env文件的权限要设成只能自己读:chmod 600 .env。Compose 项目里也可以使用环境变量替换或 secret 机制,不要让密钥以明文形式出现在 Compose 文件里并被 git 跟踪。

还有一条很容易犯的错误:把 Docker daemon 监听在0.0.0.0:2375,并且不设置 TLS。这种配置一旦暴露到公网,等于把机器上所有容器的控制权送给了任何能连上端口的人。远程管理 Docker 一定走加密通道,或者直接在内网环境里隔离管理,绝不要裸开 2375。

7. 几个高频报错的排查记录

7.1 权限不足:连不上 Docker daemon 的 socket

错误信息通常是:

Got permission denied while trying to connect to the Docker daemon socket

原因就是你当前用户不在docker组里。先运行id看用户组,再执行sudo usermod -aG docker $USER。但请注意:如果你是通过 sudo 临时提升权限,执行完usermod后必须重新登录一次,或者执行newgrp docker,否则当前 shell 仍然不会识别新组。很多新手在这里反复报错,以为是系统没生效,其实就是没重登。

7.2 容器重名和端口占用

创建容器时提示:

Error response from daemon: Conflict. The container name "/web" is already in use

要么换个名字,要么先删除旧容器。端口冲突的时候也会抛类似提示,比如:

bind: address already in use

此时用ss -ltnp | grep 8080查看哪个进程占用了端口,再把对应容器或进程处理掉。注意docker ps -a才能看到已退出但仍占端口关联的容器,别只查 running。

7.3 容器内 DNS 解析不了外部域名

如果宿主机能解析、容器里不行,常见原因是 Docker 的 embedded DNS 转发配置有问题,或者宿主机/etc/resolv.conf里的 nameserver 在容器内不可达。可以先在容器里执行:

cat /etc/resolv.conf

再看是不是残留了奇怪的 nameserver。临时解决办法是在daemon.json里配置:

{ "dns": ["1.1.1.1", "8.8.8.8"] }

然后重启 docker。但要注意,如果公司内网有私域 DNS,直接换成公共 DNS 会让内部服务名解析不了,所以要结合具体环境来定。

7.4 磁盘空间被 Docker 吃光

这是最典型的隐患。先du -sh /var/lib/docker看总量,再用docker system df看镜像、容器、数据卷、构建缓存分别占了多少。占用大头通常来自堆积的历史镜像、悬空镜像和构建缓存。定期执行:

docker system prune -a

是有效手段,但下次需要用到某个镜像时还得重新拉,成本也不低。更合理的思路是只清理无用的容器和悬空镜像:

docker system prune

如果你发现某个容器日志文件巨大,按第 4 节说的方式配置 log rotation,再删除现有大日志文件,从根上解决。

最后分享一个我自己的习惯:每周五下午跑一次docker system df看看资源走向,每月至少清理一次悬空镜像和停止的容器。这不需要花多少时间,但能避免很多“磁盘突然满了”的深夜告警。Ubuntu 安装 Docker 并不难,真正体现水平的是后续这些配置细节,希望这篇内容能帮你把常用姿势补齐,少踩几个我已经踩过的坑。

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

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

立即咨询