这一两年我在Ubuntu上反复折腾Docker,从最开始的“装个环境怎么这么难”,到后来能一套命令把MySQL、Redis、业务应用全都拉起来,中间的坑踩了不少。今天这篇就把ubuntu docker从选型、安装、网络配置到常见排错的完整经验摊开写,不绕弯子。适合刚接触Linux的新手,也适合那些已经在用Docker、但经常被“网络不通”或者“启动失败”折磨的开发者参考。
1. 先想清楚:Docker Engine 还是 Docker Desktop?
1.1 两者到底差在哪里
很多人一上来就问我:Ubuntu上是不是直接装Docker Desktop就完事了?不是的。Docker Desktop是一个带图形界面的完整套件,它依赖一个虚拟机来运行,在Windows和macOS上这是合理方案,但在Ubuntu这种本身就是Linux的系统上,套一层虚拟机会白白损耗性能,也会带来额外的资源占用。Docker Engine才是Linux上真正跑容器服务的那个守护进程,它直接调用宿主机的内核资源,效率最高,也是服务器部署时的标准选择。
简单理解:Desktop像是一个“一体机”,开箱即用;Engine像是一个“发动机”,需要你自己接线、配油、点火,但一旦跑起来,又稳又省事。
1.2 我为什么建议这样选型
如果你是在自己的Ubuntu桌面机上做日常开发,需要可视化界面,想点两下就启动一个容器,那么装Docker Desktop更方便。但如果你是在一台云服务器、公司内网服务器或生产环境中使用,请一定选择Docker Engine。原因很简单:服务器通常没有图形桌面,而且你用不到那一层额外的虚拟机管理,装了Docker Desktop反而会让问题变复杂。
还有一类很常见的场景:在VMware虚拟机里先装一个Ubuntu,再在Ubuntu里装Docker。这种情况如果选Desktop,就可能遇到Docker Desktop启动时报错,提示virtualization support not detected,因为内层虚拟机没开嵌套虚拟化。换成纯Docker Engine就不会有这个困扰。我自己用VMware装Ubuntu 22.04跑Docker,一直用的都是Engine,稳定很多。
2. Ubuntu 上安装 Docker Engine 的完整流程
2.1 安装前的环境准备与旧版本清理
无论你Ubuntu是20.04、22.04还是更新的版本,第一步都是先确认系统里没有残留的旧版Docker。我见过太多人直接apt install docker,结果装的是系统自带的docker.io,版本老、依赖乱,后面再想装Docker CE反而冲突。正确的做法是先清理:
sudo apt remove docker docker-engine docker.io containerd runc sudo apt update第二步是把依赖工具准备好,主要是apt的HTTPS访问支持和密钥管理组件:
sudo apt install ca-certificates curl gnupg lsb-release这一步不是可选项。没有这些工具,后面添加官方软件源时会报错或者找不到安装包。我在Ubuntu 22.04上实测过多次,不装gnupg直接添加Docker源,必然遇到“key is stored in legacy trusted.gpg keyring”的警告,看起来不致命,但后续安装过程会非常别扭。
2.2 添加官方源并安装Docker
添加Docker官方GPG密钥,然后写入apt源。这里有个经验:如果你在墙内环境,官方源偶尔会连接慢,但源本身是可以用的,只是拉取二进制包的速度取决于网络。官方源地址不能用国内镜像替代吗?可以,但我更建议先按官方流程操作,只在拉取镜像超时时才配置镜像加速器,这个后面会单独讲。
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg添加软件源信息时,不同版本的Ubuntu对应不同代号,比如22.04是jammy,20.04是focal。用lsb_release自动生成可以避免写错:
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update然后安装Docker本身:
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin安装完成后验证:
sudo docker run hello-world如果看到“Hello from Docker!”说明一切正常。这里我强烈建议把docker-compose-plugin一起装,现在官方推荐用docker compose子命令替代独立的docker-compose,编排多容器时会用到。
2.3 用户组配置与开机自启
装完Docker后,直接用docker命令大概率会报权限不足,因为守护进程监听在/var/run/docker.sock上,只有root用户和docker组用户能访问。解决方法是把当前用户加入docker组:
sudo usermod -aG docker $USER newgrp docker配置开机自启,让Docker守护进程随系统启动:
sudo systemctl enable --now docker systemctl status docker很多人忽略这步,结果重启服务器后容器全部失联。实际生产中,建议给每个容器都设置--restart unless-stopped策略,配合systemd的Docker自启,可以最大程度保证服务恢复。
3. 网络配置:最容易踩雷的区域
3.1 认识Docker默认的三种网络模式
Docker默认有三种网络:bridge、host和none。刚接触的人容易搞混,我举一个生活化的例子:bridge网络就像一个小区,每个容器有自己独立的门牌号和虚拟网卡,大家通过小区大门(宿主机)和外界通信;host网络则是容器直接占用宿主机的IP和端口,不经过小区大门,性能高但隔离性差;none就是完全断网的容器,适合跑一些不需要网络的批量任务。
日常部署网站、数据库、微服务,90%的场景用的是bridge网路,尤其是自定义的bridge网络。默认的bridge网络功能太弱,不支持容器之间用服务名直接互访,我几乎不用。
3.2 自定义网络与端口映射的核心操作
创建一个自定义bridge网络:
docker network create app_net要让两个容器互相通信,可以把它们都挂到同一个自定义网络下,之后在容器里就能直接用容器名替代IP访问对方。比如一个Nginx容器要反代一个Python应用容器,Python容器启动时加入app_net,Nginx配置里写proxy_pass http://my-python:8000;,这样不需要去查IP。
端口映射是外部访问容器的关键,格式是宿主机端口:容器端口:
docker run -d --name nginx -p 8080:80 nginx这个命令把宿主机的8080端口映射到容器的80端口,外部请求访问http://宿主机IP:8080就能到达Nginx。注意不要写反,我见过很多人写成-p 80:8080,容器根本监听不了8080端口,结果怎么都打不开页面,排查半天。
3.3 docker网络不通的排查步骤
这是热词里出现频率最高的问题。每次收到“docker网络不通”的咨询,我都按固定顺序排查:
第一个检查点:确认容器启动状态和网络模式。
docker ps docker inspect 容器名 | grep NetworkMode第二个检查点:验证容器能否访问宿主机和外网。
docker exec 容器名 ping 宿主机IP docker exec 容器名 ping 8.8.8.8第三个检查点:看DNS解析。
docker exec 容器名 ping google.com如果容器能ping通IP,但ping不通域名,说明DNS配置有问题。Docker默认从宿主机的/etc/resolv.conf读取DNS配置,但在某些网络环境里这个配置不生效。解决方案是在/etc/docker/daemon.json里指定DNS,比如:
{ "dns": ["223.5.5.5", "114.114.114.114"] }修改后重启Docker。这一招解决了我碰到的大多数“容器里能上网,但访问不了域名”的问题。
还有一个很容易被忽略的坑:宿主机开启了防火墙,但没有放行Docker映射的端口。比如Ubuntu自带的ufw,默认可能外部无法访问你映射到宿主机的端口。这时需要运行:
sudo ufw allow 8080/tcp或者检查iptables的FORWARD链。如果之前手动改过iptables规则,很可能把Docker的forward流量也拦了,看到ping不通别急着怪Docker,先看看防火墙规则。
3.4 修改默认网段避免冲突
在局域网环境里,Docker默认网段172.17.0.0/16偶尔会和内网其他设备冲突,导致路由异常。解决办法是在daemon.json里自定义网段:
{ "bip": "10.10.0.1/24" }这里的bip表示bridge network IP。修改后重启:
sudo systemctl restart docker我建议大一点的环境直接规划好容器网段,比如用10.10.0.0/16作为容器网段,跟公司内网错开,后续加虚拟化、加其他设备都不会撞车。
4. 实战:用Docker部署MySQL与Redis
4.1 MySQL 8.0容器化启动
看到热词里有“docker安装mysql8.0并使用”,这部分我直接讲生产可用的命令。拉取MySQL 8.0镜像,启动容器时关键在于数据持久化和初始化配置:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=YourPassword \ -e TZ=Asia/Shanghai \ -v /data/mysql:/var/lib/mysql \ -v /data/mysql-config:/etc/mysql/conf.d \ --restart unless-stopped \ mysql:8.0-v挂载宿主机目录到容器内,这样删掉容器重建,数据还在。TZ设置时区为上海时间,避免容器内时间和宿主机不一致,导致日志歪了、时间查询错误。MySQL 8.0的默认字符集是utf8mb4,如果遇到中文乱码,可以自定义配置文件,在/data/mysql-config/my.cnf写入:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci启动后进入容器验证:
docker exec -it mysql8 mysql -uroot -p有个常见的坑:MySQL 8.0默认使用caching_sha2_password认证插件,有些老版本的客户端工具连接会报错“Authentication plugin 'caching_sha2_password' cannot be loaded”。解决方法是登录后执行一遍:
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'YourPassword';实际开发时我更推荐新建专用用户,而不是直接用root开放远程访问,安全性和可追踪性都会好很多。
4.2 Redis主从复制容器化
Redis主从复制是热词我根据经验补全的常见需求。先用Docker网络把它们串起来:
docker network create redis_net启动master节点:
docker run -d --name redis-master \ --network redis_net \ -p 6379:6379 \ -e TZ=Asia/Shanghai \ redis:7.0 redis-server --appendonly yes启动两个slave节点,分别映射到6378和6377端口,并用--replicaof指定master:
docker run -d --name redis-slave1 \ --network redis_net \ -p 6378:6379 \ -e TZ=Asia/Shanghai \ redis:7.0 redis-server --appendonly yes --replicaof redis-master 6379 docker run -d --name redis-slave2 \ --network redis_net \ -p 6377:6379 \ -e TZ=Asia/Shanghai \ redis:7.0 redis-server --appendonly yes --replicaof redis-master 6379启动后进入任意节点验证:
docker exec -it redis-slave1 redis-cli info replication看到role:slave和master_link_status:up说明主从已经建立。这里把主从节点放在同一个自定义网络里,关键点在于slave可以通过容器名redis-master访问master,不需要关心master的IP地址。
4.3 数据持久化与备份
容器最怕的就是一删就丢。MySQL那边已经通过volume挂载解决了,Redis这边用--appendonly yes开启AOF持久化,同时也要挂载数据目录:
-v /data/redis:/data这样Redis的数据文件会写到宿主机/data/redis下。备份MySQL的话,我习惯用docker exec直接执行mysqldump:
docker exec mysql8 sh -c 'mysqldump -uroot -pYourPassword --all-databases' > /data/backup/all_$(date +%F).sql容器化部署和传统部署并没有本质区别,只要目录挂载和持久化配置到位,随便删容器重建都能快速恢复。
5. 高频报错与修复实录
5.1 Docker API连接失败与socket权限错误
很多人在Ubuntu上刚装完Docker,执行docker ps就报错:
Got permission denied while trying to connect to the Docker daemon socket这是用户不在docker组导致的。我已经在2.3节写了修复方法,这里补充一种情况:如果你已经加入docker组,但新终端仍然报错,别急着重启系统,先执行newgrp docker重新加载用户组。另外,如果连的是远程Docker,也有可能是DOCKER_HOST环境变量配置错误,检查一下:
echo $DOCKER_HOST设置错误的话,清除环境变量就行。这个坑在调试脚本时特别容易遇到。
5.2 daemon.json配置错误导致Docker启动失败
我见过最典型的启动失败案例:修改了/etc/docker/daemon.json,里面少写了一个逗号或者多加了一个花括号,然后执行systemctl restart docker,直接起不来。排查方法:
systemctl status docker journalctl -xeu docker.service | tail -50如果是JSON语法错误,日志里会明确提示unable to configure the Docker daemon with file /etc/docker/daemon.json。修复后重新加载:
sudo systemctl daemon-reload sudo systemctl restart docker这里分享一个操作习惯:每次修改daemon.json之前,先备份一份,改完用python3 -m json.tool /etc/docker/daemon.json验证格式,格式正确再重启服务。
5.3 拉取镜像超时与加速器配置
Ubuntu上docker拉镜像超时,十次有八次是网络原因。在国内环境,我强烈建议配置镜像加速器。修改daemon.json:
{ "registry-mirrors": ["https://docker.m.daocloud.io"] }保存后重启Docker:
sudo systemctl daemon-reload sudo systemctl restart dockerdocker.m.daocloud.io是社区常用的公共加速地址,理论上比直接连官方仓库稳定很多。配置后验证:
docker info | grep -A 2 "Registry Mirrors"如果你的内网有公司代理,也可以换成内网镜像地址。注意加速器只对拉取公共镜像有帮助,已经下载到本地但重复拉取的镜像不会有明显提升,此时优先用tag管理。
5.4 磁盘占用检查与清理
容器日志增长是Ubuntu Docker最常见的磁盘杀手。默认日志大小没有限制,长年运行后单个容器的日志文件涨到几十GB是常事。我一般先用这个命令看整体占用:
docker system df然后清理悬空镜像、停止的容器、无效数据卷:
docker system prune -a --volumesprune会帮你清掉所有未被使用的资源,谨慎使用。要限制单个容器日志大小,在daemon.json里配置:
{ "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "5" } }这个配置强烈建议在一开始部署时就设置好,生产服务器上如果等到磁盘告警再去清理,很多时候已经晚了。
6. 用Docker Compose编排多容器项目
6.1 为什么需要Compose
手动一个个docker run,容器数量少时没问题,但一旦涉及多个服务——一个Python后端、一个MySQL、一个Redis、一个Nginx——输入命令就变成灾难。Compose的作用是把完整的容器编排写进一个YAML文件,执行docker compose up -d就能一次性创建并启动所有服务。这也是微服务项目在开发环境快速启动时最常用的方式。
6.2 一个Python + MySQL + Nginx的正向案例
在项目目录下创建docker-compose.yml:
version: "3.8" services: mysql: image: mysql:8.0 container_name: demo_mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: demo TZ: Asia/Shanghai volumes: - ./mysql_data:/var/lib/mysql ports: - "3306:3306" networks: - demo_net backend: build: ./backend container_name: demo_backend restart: unless-stopped environment: DB_HOST: mysql DB_USER: root DB_PASSWORD: rootpass DB_NAME: demo depends_on: - mysql networks: - demo_net nginx: image: nginx:latest container_name: demo_nginx restart: unless-stopped ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - backend networks: - demo_net networks: demo_net: driver: bridge这个配置里最关键的是depends_on和networks。depends_on保证服务启动顺序,demo_net让后端可以连接MySQL,Nginx可以反向代理到后端。后端代码里数据库地址直接填mysql,不填IP,因为Compose会自动做DNS解析。
6.3 常用命令与启动技巧
在项目目录下执行:
docker compose up -d docker compose ps docker compose logs -f docker compose down注意:down默认不会删除volume,加了-v才会连数据卷一起清理。这个参数我用得很谨慎,除非确定要重置数据库,否则不会随手写docker compose down -v。
通过Compose跑微服务项目,是我在Ubuntu上用得最舒服的Docker姿势。相比逐个容器手敲命令,维护成本低很多,配置也能跟着项目一起走,换台机器clone下来直接就能跑。
个人使用Docker这么几年,最大的体会是:遇到问题先冷静看日志。Ubuntu上几乎所有Docker异常都能在journalctl -u docker或者容器日志里找到线索,不要凭感觉乱重启。另一个建议是养成“配置先备份、改动后验证”的习惯,尤其是涉及到/etc/docker/daemon.json和docker-compose.yml的时候。Docker本身不难,难的是把很多小细节都照顾到,这份经验如果能让你少踩几个坑,就是我写这篇内容最大的价值了。