
引言为什么要用Docker我在Ubuntu 18.04上折腾Docker已经有一段时间了。从最开始连镜像和容器都分不清到现在能熟练地用一条命令把MySQL、Redis、Nextcloud这类服务跑起来中间踩过的坑确实不少。这篇笔记不是官方文档的翻译也不是什么高深理论就是我把Ubuntu 18.04上从零玩转Docker的完整过程、命令、配置和教训整理出来希望能帮你少走几个弯路。Docker能解决什么问题最直白的一句话把你需要的软件和它的运行环境一起打包成一个集装箱扔到哪台机器上都能跑。以前我在服务器上装MySQL要处理依赖、版本冲突、卸载残留折腾半天现在一条docker run就搞定了而且想换版本随时换。适合谁来参考刚接触容器方向的学生、负责服务器环境部署的运维/开发同学以及所有想在Ubuntu 18.04这台经典LTS系统上快速把服务跑起来的折腾党。注意我基于的是Ubuntu 18.04bionicDocker版本随时间变化较大文中命令在2024-2025年前后亲测可用但配置路径和命令参数建议以官方文档为准。虚拟机或云服务器均可只要内核满足要求。1. 环境准备与Docker安装1.1 开始前的环境检查很多人拿到一台新服务器第一件事就是急着敲安装命令结果后面各种问题。我在第一步就吃过亏所以现在装机前一定会先做三个检查。系统版本检查。确认操作系统是64位的Ubuntu 18.04用cat /etc/os-release看系统代号。Docker对32位系统支持很不好如果手头是32位的老机器建议直接放弃或者换系统。顺便用uname -a看一下内核版本Docker要求内核版本不低于3.10Ubuntu 18.04默认内核是4.15完全满足。检查是否装过旧版本。很多云服务器镜像里自带了旧版Docker比如docker.io如果不卸载干净后面装新版时会冲突。检查命令是which docker和docker --version如果发现系统里有旧版直接卸载sudo apt remove docker docker-engine docker.io containerd runc这一步做完docker命令应该找不到了说明清理干净。检查用户权限。如果安装完后每次用Docker都要加sudo会很麻烦。我建议先看一下当前用户是不是有管理权限因为整个安装过程都需要sudo。更优雅的做法是装完以后把当前用户加入docker组后面第6节详细说这样日常操作就不用天天输密码了。1.2 用官方源安装Docker Engine在Ubuntu 18.04上装Docker常见的有两条路一条是直接用sudo apt install docker.io简单省事但装的是Ubuntu仓库里的老版本另一条是添加Docker官方源装最新稳定版。我的建议是都2025年了请走官方源。原因是老版本经常缺功能而且和docker compose配合时容易出兼容性问题。官方源安装步骤一步步来# 1. 更新系统软件源 sudo apt update # 2. 安装依赖软件 sudo apt install apt-transport-https ca-certificates curl software-properties-common # 3. 添加Docker官方GPG密钥 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - # 4. 添加稳定版仓库 sudo add-apt-repository deb [archamd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable # 5. 再次更新源 sudo apt update # 6. 安装Docker引擎 sudo apt install docker-ce docker-ce-cli containerd.io安装完成以后验证一下是否成功sudo systemctl status docker docker --version sudo docker run hello-worldhello-world是Docker官方提供的最小测试镜像能跑起来输出一段欢迎语说明Docker核心功能正常。我第一次看到那句Hello from Docker!的时候还挺有成就感的。心得第4步里$(lsb_release -cs)会自动解析出bionic也就是Ubuntu 18.04。如果你用了奇怪的方式改过系统代号这步就会报错此时手动写死bionic即可。1.3 配置镜像加速器Docker安装成功后你马上会面临一个现实问题从Docker Hub拉取镜像在国内网络环境下很慢。我首次拉一个ubuntu:latest镜像花了十几分钟中间还断过一次当时差点就放弃了。解决思路是给Docker配置一个镜像加速器registry mirror让Docker从加速地址拉镜像。具体操作是编辑/etc/docker/daemon.json这个文件默认不存在需要新建sudo mkdir -p /etc/docker sudo vim /etc/docker/daemon.json文件内容{ registry-mirrors: [https://你的加速器地址] }保存后重启Dockersudo systemctl daemon-reload sudo systemctl restart docker加速器地址怎么填我自己的经验是如果你有云服务商的账号直接在控制台找容器镜像服务相关的加速器地址注册后就会给一个专属地址如果用公开的加速地址注意看它是否还活着很多公开地址时效性不稳定。daemon.json是个很关键的配置文件后面配置日志大小、存储驱动也都要用建议养成习惯。2. Docker核心概念与镜像管理2.1 镜像、容器、仓库的关系一开始我把镜像和容器傻傻分不清后来用一个类比彻底想通了镜像Image就是装系统的ISO安装盘容器Container就是装好系统的电脑。你可以用同一个安装盘装出很多台电脑每台电脑都是独立的互不干扰。仓库Registry呢就是存放安装盘的网上盘Docker Hub是默认仓库但你可以切换成其它镜像源。再往深一层说镜像是只读的模板定义了文件系统、运行环境、启动命令。镜像分层存储每一层都是只读的构建镜像时可以复用公共层所以很多镜像看起来很大实际增量很小。容器是镜像的运行实例在镜像最上层加了一个可写层。你对容器做的任何修改装软件、改配置都只影响这个容器本身不会污染镜像。仓库类似于代码托管平台可以push上传、pull下载镜像。Docker Hub上官方镜像一般叫mysql、redis这种短名字个人的镜像会有用户名前缀。这个理解很关键因为后面你写Dockerfile、搞数据卷、做多容器编排时绕不开这些概念。2.2 镜像拉取与管理命令掌握镜像管理命令日常操作基本就够用了。我把常用的命令整理成一张速查表命令作用示例docker images列出本机镜像docker imagesdocker search在仓库中搜索镜像docker search mysqldocker pull拉取镜像docker pull mysql:8.0docker rmi删除镜像docker rmi mysql:8.0docker tag给镜像打标签docker tag nginx:latest mynginx:v1docker save保存镜像为tar文件docker save -o nginx.tar nginxdocker load从tar文件导入镜像docker load -i nginx.tar关于镜像Tag我要多说一句别只记latest。生产环境务必指定具体版本号比如mysql:8.0而不是mysql:latest因为latest会跟随仓库更新某天你重启容器拉到的镜像可能和预期不一致排查问题时会想哭。docker save和docker load是我在离线环境下的救命稻草。内网机器无法访问外网时我就在有网的机器上docker pull好镜像save成tar带走再到内网load进Docker。注意save保留镜像的层级结构export/import只保留文件系统层信息会丢失所以save更保险。2.3 容器的生命周期操作容器命令是每天的主力我同样整理成速查表命令作用示例docker run创建并启动容器docker run -d --name nginx -p 80:80 nginxdocker ps列出运行中的容器docker ps -a含已停止docker start/stop/restart启停容器docker restart nginxdocker exec -it进入容器执行命令docker exec -it nginx bashdocker logs查看容器日志docker logs -f nginxdocker rm删除容器docker rm -f nginxdocker inspect查看容器详细信息docker inspect nginxdocker run是核心中的核心参数稍微说详细一点-ddetached后台运行容器不会一直霸占终端。--name给容器起名字不然Docker会随机生成一个像happy_liskov这种名字运维时会很痛苦。-p [宿主机端口]:[容器端口]端口映射。容器是隔离的外部访问不到容器的端口必须通过宿主机端口转发比如-p 8080:80就是把宿主机的8080映射到容器内的80。-v数据卷挂载第3节讲。-e设置环境变量镜像的不同行为往往靠它配置比如MySQL的root密码。进入容器排查问题exec -it最常见。注意一定要加-i和-t-i表示交互模式-t表示分配伪终端缺一个都可能出现进去了但卡住或者没有命令行提示符的情况。有些精简镜像如alpine连bash都没有这时候用sh就行比如docker exec -it 容器名 sh。3. 数据持久化与网络配置3.1 数据卷Volume实战先讲一个我踩过的坑刚用Docker跑MySQL时我只执行了docker run -d mysql:8.0然后扔进去一堆测试数据。某天不小心执行了docker rm 容器ID数据全没了那一刻心情真的是崩溃的。容器删除后容器内写入的文件全部丢失因为可写层是临时的。解决方案就是数据卷Volume和绑定挂载Bind Mount。数据卷是Docker管理的一块存储空间不依赖容器生命周期绑定挂载则是把宿主机的目录直接映射进容器。两者各有适用场景方式优点典型场景数据卷docker volume由Docker管理portable不停留在容器层数据库数据文件、应用归档数据绑定挂载-v /宿主机路径:/容器路径直接读写宿主机文件方便备份查看配置文件、日志目录、网页静态文件以MySQL为例正确姿势是# 先用数据卷管理 docker volume create mysql-data # 启动时挂载数据卷 docker run -d \ --name mysql \ -e MYSQL_ROOT_PASSWORDroot123 \ -v mysql-data:/var/lib/mysql \ -p 3306:3306 \ mysql:8.0这样删掉容器再重建只要挂载同一个数据卷数据就还在。用绑定挂载也一样比如-v /home/ubuntu/mysql-data:/var/lib/mysql数据直接落在宿主机目录备份就把它打包带走就行。注意绑定挂载时宿主机的目录要提前建好并且注意权限。容器内的MySQL是以mysql用户运行的UID通常为999如果你挂载的宿主机目录权限不对MySQL可能启动不了日志里会报Permission denied。我处理过几次直接把宿主机目录的属主改成999:999或者chmod 777不推荐生产百试百灵。3.2 端口映射与网络模式Docker默认有几种网络驱动理解它们能让你的容器网络配置不再全靠猜。Bridge桥接网络是默认模式。装Docker时系统会自动创建一个名为docker0的虚拟网桥所有容器默认连在这个网桥上容器之间能互通但外界默认访问不到容器内部端口。-p映射就是通过宿主机上的端口转发规则把流量送进容器。Host主机网络模式下容器直接使用宿主机的网络栈监听端口就是宿主机的端口性能好但隔离性差。适合对网络性能要求极高、或者容器端口需要动态变化的场景。None就是完全隔离基本不怎么用。自定义网络是最推荐的方式尤其是多容器部署场景。用docker network create创建网络然后让多个容器都加入同一个网络它们之间就能用容器名互相访问而不是用IPIP会变很难维护。举个例子我部署Nextcloud时遇到一个问题Nextcloud容器要访问MySQL容器如果只用localhost或者宿主机IP要么不通、要么绕一圈。后来我把两者放在同一个自定义网络里直接用mysql这个容器名作为数据库主机名问题立刻解决。创建和使用如下docker network create app-net docker run -d --name mysql --network app-net -e MYSQL_ROOT_PASSWORDroot123 mysql:8.0 docker run -d --name nextcloud --network app-net -p 8080:80 nextcloud以后在Nextcloud容器里连接数据库主机名填mysql即可。网络隔离的最大好处是无关容器互相不可见攻击面减小也更符合微服务各组件独立演进的思路。4. 实战用Docker部署MySQL 8.0与Redis主从4.1 一条命令拉起MySQL 8.0纸上谈兵没意思直接上实战。第一单是MySQL 8.0典型需求就是跑起来、数据不丢、能被宿主机访问。docker pull mysql:8.0 docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyour_password \ -e TZAsia/Shanghai \ -v /data/mysql8/conf:/etc/mysql/conf.d \ -v /data/mysql8/data:/var/lib/mysql \ -v /data/mysql8/logs:/var/log/mysql \ mysql:8.0参数解释一下-e MYSQL_ROOT_PASSWORD设置root密码-e TZAsia/Shanghai设置容器时区不设置的话容器时间默认是UTC比北京时间慢8小时日志和SQL查询的NOW()都会对不上这个坑很隐蔽三个-v分别把MySQL的配置文件目录、数据目录、日志目录挂到宿主机以后想改配置、看日志都不用进容器了。启动完成后验证docker ps # 看看容器状态是否healthy docker logs mysql8 | tail -n 20 # 看看有没有报错 mysql -h127.0.0.1 -uroot -p # 宿主机如果没有mysql客户端用docker exec进去 docker exec -it mysql8 mysql -uroot -p用docker exec进入容器执行MySQL客户端相当于人在容器内操作这是排查问题的首选手段也是热词里出现频率极高的docker exec -it命令的标准用法。关于MySQL 8.0的字符集我额外说一点8.0默认字符集是utf8mb4比5.7时期的utf8更合理基本不用额外配置。但你如果想改排序规则或者默认存储引擎可以写配置文件然后挂载进/etc/mysql/conf.d我一般是写一个my.cnf[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci写到/data/mysql8/conf/my.cnf重启容器docker restart mysql8就会生效。配置一次以后这个MySQL实例基本上就是长期跑着的了换服务器时把/data/mysql8整个拷贝走再启一个容器挂上就好。4.2 搭建Redis主从复制Redis主从架构是缓存服务最常见的形态用Docker搭主从比传统方式省事很多。先拉镜像docker pull redis:7.0然后准备主从的配置文件。我习惯先把配置放在宿主机mkdir -p /data/redis/{master,slave}主节点/data/redis/master/redis.confport 6379 appendonly yes requirepass your_redis_password从节点/data/redis/slave/redis.confport 6379 appendonly yes requirepass your_redis_password replicaof redis-master 6379 masterauth your_redis_password关键就是replicaof这一行指定主节点的地址和端口。在Docker自定义网络里主节点地址可以直接写容器名redis-masterDocker的DNS会自动解析非常方便。然后创建自定义网络并启动两个容器docker network create redis-net docker run -d \ --name redis-master \ --network redis-net \ -p 6379:6379 \ -v /data/redis/master/redis.conf:/etc/redis/redis.conf \ redis:7.0 redis-server /etc/redis/redis.conf docker run -d \ --name redis-slave \ --network redis-net \ -p 6380:6379 \ -v /data/redis/slave/redis.conf:/etc/redis/redis.conf \ redis:7.0 redis-server /etc/redis/redis.conf注意从节点的宿主端口映射是6380:6379因为两个容器内部都是6379但宿主机不能同时占用6379所以从节点映射到6380。验证主从是否成功进入主节点写入从节点读取docker exec -it redis-master redis-cli -a your_redis_password set name hello docker exec -it redis-slave redis-cli -a your_redis_password get name如果从节点能读到name的值就说明主从复制已经跑通了。这时候你可以再验证一下从节点是只读的执行set key2 value2会报错提示READONLY这是正常的。心得Redis容器启动命令后面跟的redis-server /etc/redis/redis.conf会覆盖镜像默认的启动命令而且必须指定配置文件路径否则Redis不会加载你的配置。很多新手在这里卡住明明传了-v挂载配置但Redis不理会就是忘了在命令末尾手动指定配置文件。5. Docker Compose编排多容器5.1 安装Docker Compose当你需要同时管理MySQL、Redis、Nextcloud等好几个容器时再用一条条docker run命令启动既容易漏参数又不好维护。Docker Compose就是干这个的用一个YAML文件描述整套服务一条命令全部启动。Ubuntu 18.04上安装docker compose的标准方式sudo curl -L https://github.com/docker/compose/releases/download/v2.24.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose这个下载地址里的文件名$(uname -s)会解析为Linux$(uname -m)如果是64位机器就是x86_64拼起来就是docker-compose-Linux-x86_64。装完以后加了执行权限再验证sudo chmod x /usr/local/bin/docker-compose docker-compose --version如果网络下载GitHub文件很慢可以用pip安装pip install docker-compose但版本可能旧一些。我建议还是用官方二进制版本可控。5.2 编写docker-compose.yml一个典型的compose文件长这样我把上面MySQL和Redis主从的折腾整合成一个文件version: 3 services: mysql: image: mysql:8.0 container_name: mysql8 restart: always environment: MYSQL_ROOT_PASSWORD: your_password TZ: Asia/Shanghai ports: - 3306:3306 volumes: - /data/mysql8/data:/var/lib/mysql - /data/mysql8/conf:/etc/mysql/conf.d networks: - app-net redis-master: image: redis:7.0 container_name: redis-master restart: always command: [redis-server, /etc/redis/redis.conf] ports: - 6379:6379 volumes: - /data/redis/master/redis.conf:/etc/redis/redis.conf networks: - app-net redis-slave: image: redis:7.0 container_name: redis-slave restart: always depends_on: - redis-master command: [redis-server, /etc/redis/redis.conf] ports: - 6380:6379 volumes: - /data/redis/slave/redis.conf:/etc/redis/redis.conf networks: - app-net networks: app-net:启动只需要docker-compose up -d docker-compose ps docker-compose logs -fdepends_on只控制启动顺序不保证服务内部可用比如Redis从节点启动时主节点可能还在初始化不过Redis本身有重连机制问题不大。生产环境要求高的话可以用healthcheck做健康检查。restart: always很关键宿主机重启后容器会自动拉起来这对线上服务是刚需。Compose的好处是配置即代码新建一台服务器把这个docker-compose.yml和挂载目录拷过去docker-compose up -d整套环境就回来了。我之前迁移过一次服务器手动敲命令折腾了半个多小时后来改成Compose五分钟左右搞定这套流程比什么部署文档都靠谱。6. 常见问题与排查技巧实录6.1 权限错误提示与docker组Got permission denied while trying to connect to the Docker daemon socket——这句报错我见了太多次原因就是当前用户不在docker组里没有权限访问Docker的socket。解决办法sudo usermod -aG docker $USER # 然后重新登录shell或者执行下面的命令让组权限立刻生效 newgrp docker执行完再docker ps就不会要sudo了。这里说明一下安全取舍把用户加入docker组等同于给了这个用户对Docker的完全控制权因为Docker可以挂载宿主机任意目录来读写文件所以只把可信用户加入docker组不要图省事直接做。6.2 镜像下载慢前面配置过镜像加速器但还是慢的话有几个经验优先选择你所在网络环境最快的镜像源不同地方适合的源不同我一般会备两三个地址一个失效就换另一个。小镜像用docker pull大镜像比如几GB的机器学习镜像如果实在下载不动考虑在有公网带宽的机器上docker save后传文件。检查daemon.json语法是否正确改完配置必须sudo systemctl restart docker否则不生效。如果你发现docker pull报failed to fetch多半是镜像源地址连接不上把daemon.json里那个URL去掉再重启用默认Docker Hub试试虽然慢但至少能拉下来。6.3 Docker服务启动失败sudo systemctl start docker之后docker ps直接报cannot connect to the Docker daemon常见原因查看Docker服务状态sudo systemctl status docker如果看到Active: failed再用journalctl -u docker翻日志。我遇到过的几种情况daemon.json写坏了比如JSON格式少逗号导致daemon启动失败。用docker daemon --validate可以校验配置。/var/lib/docker磁盘满了。Docker的镜像层、容器可写层、数据卷都在这里长期不清理就会爆掉。我用sudo du -sh /var/lib/docker查占用清理的话用docker system prune -a它会删掉所有未被使用的镜像和容器执行前要确认哪些是不用的。内核模块问题导致的网络驱动失败比较罕见一般升级内核或者重启机器能解决。6.4 容器启动后立刻退出docker ps -a能看到容器状态是Exited (0)或Exited (1)说明容器启动就退出了。Docker容器不是完整的操作系统它只在主进程存活时运行主进程一退出容器就停了。所以排查思路是看主进程为什么退出docker logs 容器名常见原因和对应解法前台进程缺失比如有人直接docker run nginx但没指定启动命令nginx镜像有默认前台命令所以没事但如果你用自己的基础镜像想让它保持运行就需要写类似tail -f /dev/null这样的前台命令。数据库类容器配置错误如MySQL报Unknown database、Redis报配置文件权限问题都在日志里有明确线索。端口被占用宿主机端口被其它服务占用时容器启动失败日志一般会直接说bind: address already in use。换一个映射端口就行。心得排查容器问题第一反应永远是docker logs不要凭感觉瞎猜。docker logs -f 容器名可以像tail -f一样实时跟踪日志配合docker inspect查看容器详情比如挂载情况、环境变量、网络模式90%的问题都能定位。6.5 关于虚拟化检测的提醒很多人搜Docker问题时看到virtualization support not detected这类报错然后以为是Linux环境的问题。这里我多说一句这个报错几乎只出现在Windows/Mac上跑Docker Desktop的场景因为桌面版依赖Hyper-V或WSL2的虚拟化支持需要在BIOS里开启虚拟化。而我们这篇文章讨论的Ubuntu 18.04上安装的Docker Engine运行在Linux原生内核上不依赖额外虚拟化所以不需要担心这个报错。如果你在本地是Windows想用Docker要么开启虚拟化装Docker Desktop要么直接在虚拟机里装一个Ubuntu 18.04在Linux里用Docker Engine——后者反而更接近生产环境折腾起来也更顺手。至少我在Ubuntu 18.04上从未因为虚拟化问题翻过车。6.6 Docker空间清理与日常维护Docker越用越大是必然的。镜像、容器、数据卷、构建缓存堆在一起几百G说没就没。我给自己定了个维护习惯每个月跑一次清理# 清理已停止的容器、没用过的网络、悬空镜像和构建缓存 docker system prune -a -f --volumes # 只看不动先摸清占用 docker system dfsystem df会显示镜像、容器、本地卷各占多少空间非常直观。prune -a会把本地没有容器引用的镜像全部删掉包括docker pull拉下来但没在用的大镜像所以执行前一定确认没有需要的东西。如果只想删除悬空镜像none那种执行docker image prune就够了。另外我建议给Docker的日志加上自动切割否则一个写日志很猛的容器/var/lib/docker/containers下的json日志文件能涨到几十G。在daemon.json里加上{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }重启Docker后生效新容器每个日志文件最多10MB保留3个轮转旧的不受此限制。这个配置我是在踩过一次磁盘被日志塞满、服务全部Down之后才加上的现在想起来都后怕。写在最后的几点体会文章写到这里关于Ubuntu 18.04上折腾Docker的路径基本上完整了从装环境、理解镜像和容器、搞定数据持久化和网络到用Docker跑通MySQL和Redis主从再到用Compose把多个服务编排起来最后是那些我真实踩过的坑。我个人实际操作中的最大体会是Docker学习的关键不是背命令而是建立镜像-容器-数据-网络这四个维度的心智模型。命令忘了可以查模型乱了才真的寸步难行。所以建议你在学习时多动手、多破坏把容器删了重建把数据卷挂错路径看看报什么错这些都是最直观的学习过程。最后再分享一个小技巧如果你经常在一个容器里执行一组固定命令比如docker exec -it mysql8 mysql -uroot -p建议在~/.bashrc里加个别名比如alias dbdocker exec -it mysql8 mysql -uroot -p。省下的时间远比你想得多。Docker这套东西Linux、Windows、macOS上都能玩但我始终认为从Ubuntu这类的Linux发行版入门是弯路最少的一条路——因为容器技术本身就是为Linux设计的回到源头学习你会理解得更扎实。