
Docker装好之后很多人就急着docker run结果跑了没几天就出各种问题容器一重启数据全没了、日志把磁盘塞满、两个容器之间死活连不上、拉个镜像慢得怀疑人生。这篇Linux系统下Docker配置最佳实践是系列的第2篇接着第1篇继续往下写从daemon全局配置讲到存储、网络、安全加固全部是Linux服务器上实测落地过的经验。如果你已经装好了Docker准备让它稳定、安全地跑生产或半生产业务这篇文章就是给你看的。无论你是开发自用、小团队搭建压力不大的服务器还是运维初上手照着下面这些配置思路走一遍能避开大半的坑。1. Docker Daemon配置装完Docker后第一件该做的事1.1 为什么要主动配置daemon.json很多发行版的Docker安装完之后daemon运行的就是全默认参数。默认参数意味着什么容器默认不开资源限制任意一个进程就能把宿主机CPU打满日志默认不设轮转跑一个长期不重启的容器json日志文件能轻松膨胀到几十GB磁盘大小默认不设置Docker的root占比和系统盘的分区直接抢在一起。这些默认值不是不能用而是不适合长期稳定运行。生产环境里最怕的不是功能不够而是“没有屏障”。我接手过一台跑了好几个容器的服务器/var/lib/docker就占了95%的磁盘排查下来罪魁祸首就是谁都没管过的json-file容器日志。所以说装完Docker的第一件事不是急着run而是先把daemon配置捋一遍。1.2 daemon.json里值得写的核心字段daemon的配置文件默认位于 /etc/docker/daemon.json没有这个文件就自己新建一个。下面这段是我在Linux服务器上常用的基础配置每项都有注释说明。{ data-root: /data/docker, exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 }, max-concurrent-downloads: 5, max-concurrent-uploads: 3, storage-driver: overlay2, iptables: true, live-restore: true }我逐项说明一下为什么这么写>sudo systemctl daemon-reload sudo systemctl restart docker # 或者如果只想让部分配置生效可以用 sudo kill -SIGHUP $(pidof dockerd)第一条命令重载systemd自身对docker服务的定义第二条重启docker守护进程让配置真正生效第三条SIGHUP信号是dockerd自己支持的平滑重载用来加载配置变化。但要注意有些配置项的变更必须重启平滑重载并不能覆盖所有字段比如存储驱动和data-root的变更一定要先备份再做千万别在已有大量容器的机器上直接改data-root那属于自找麻烦的典型操作。改完配置后用 docker info 检查确认sudo docker info | grep -E Docker Root Dir|Storage Driver|Logging Driver|Cgroup Driver如果输出里Docker Root Dir已经指向新目录说明配置生效了继续跑业务容器之前再顺手确认Storage Driver是overlay2、Cgroup Driver是systemd这几项对上号了后续才不容易因为基础配置不一致出现奇怪问题。2. 镜像源与私有仓库从拉取缓慢到秒级下载的实操2.1 为什么拉个镜像都那么慢镜像拉取慢绝大多数原因就一个默认的Docker Hub源在境外国内网络访问它稳定性不够好。这个问题属于真实普遍的存在我自己第一次在Linux上拉一个几百MB的镜像时曾经卡到怀疑网络断掉。针对这个问题业内通用的做法有三类使用云厂商提供的镜像加速器阿里云容器镜像服务等都有。在公司内网自建Registry私有仓库。使用与业务区域更接近的公共仓库源。注意一个细节很多人以为配置加速器需要额外工具其实不需要Docker原生支持registry-mirrors配置改一行JSON就能生效。2.2 配置镜像加速器的方法在daemon.json里增加registry-mirrors字段{ registry-mirrors: [ https://your-mirror.example.com ] }这里的地址替换成你在云厂商控制台拿到的专属加速地址。每家云厂商的操作路径不同但基本都是在容器镜像服务控制台里找到“镜像加速器”复制给你的专属地址即可。配置完之后sudo systemctl restart docker docker info | grep -A 2 Registry Mirrors看到Registry Mirrors下面列出了你配置的地址说明生效了。这里我要多说一句踩过的坑网上有不少公开的、蹭流量性质的加速地址没准哪天突然就失效了配置在daemon里不声不吭就影响整个开发环境。我现在的习惯是优先用云厂商控制台里的专属地址其次用公司自建的Harbor来路不明的公开地址一律不配到生产机器上。2.3 自建私有Registry仓库的配置如果公司内网或者你自己有好几台服务器强烈建议搭一个私有仓库镜像推上去内网拉取速度直接起飞。我常用的方案是跑一个Registry容器配合Harbor做权限管理。起个最简版docker run -d --name registry \ -p 5000:5000 \ -v /data/docker-registry:/var/lib/registry \ --restartalways \ registry:2然后在你需要拉取镜像的机器上把daemon.json里的insecure-registries字段加上内网仓库地址{ insecure-registries: [registry.internal.example.com:5000] }提示如果私有仓库用了HTTPS证书就把这个配置替换成证书路径而不是直接跳过验证。生产环境千万别图省事裸奔。推送一个镜像试试docker tag myapp:latest registry.internal.example.com:5000/myapp:latest docker push registry.internal.example.com:5000/myapp:latest仓库跑起来后常备两个小命令确认健康状态curl -X GET http://registry.internal.example.com:5000/v2/_catalog curl -X GET http://registry.internal.example.com:5000/v2/myapp/tags/list第一个命令列出仓库里已存在的镜像列表第二个命令查看某个镜像的标签列表两个请求都能返回正常的JSON说明Registry服务在正常工作。生产环境的私有仓库一定要做好访问控制别把敏感业务的镜像裸奔在内网里。3. 数据持久化配置MySQL实战与存储卷管理3.1 三种挂载方式怎么选容器删除之后容器内部写的数据跟着一起消失这是让很多人第一次使用Docker时猝不及防的坑。要解决数据不丢失得把宿主机目录或存储卷挂载进容器Docker常见的方式有三种。第一类是绑定挂载直接把宿主机目录挂进容器简单直观但配置成宿主机路径依赖跨机器复用性差。比如docker run -v /home/user/data:/app/data nginx。第二类是命名卷用docker volume create创建由Docker统一管理位置跨容器共享方便备份也简单。第三类是tmpfs挂载数据存在内存里容器停止即清空适合放缓存、临时文件不适合放重要数据。我的选择建议就一句话数据库、中间件、配置文件这类必须长期保留的用命名卷想看着真实路径图个方便的就用绑定挂载临时缓存才用tmpfs。3.2 部署MySQL不丢数据的标准写法以MySQL为例很多人第一次docker run mysql用完才发现重启后数据全无问题就出在没挂载存储。正确操作如下。先创建独立的网络和数据卷网络配置下一节细说这里先把卷建好docker network create app-net docker volume create mysql-data docker volume create mysql-conf然后拉取并运行MySQL容器docker run -d --name mysql \ --network app-net \ -e MYSQL_ROOT_PASSWORDYourStrongPassword \ -e MYSQL_DATABASEtestdb \ -v mysql-data:/var/lib/mysql \ -v mysql-conf:/etc/mysql/conf.d \ -p 3306:3306 \ --restartalways \ mysql:8.0几个关键选项-e MYSQL_ROOT_PASSWORD 设置root密码。8.0版本默认认证插件是caching_sha2_password连接老客户端时可能要兼容处理这是新手常踩的坑。-v mysql-data:/var/lib/mysql 是数据目录挂载容器重建后老数据还在。-v mysql-conf:/etc/mysql/conf.d 是自定义配置挂载目录想调字符集、调慢查询参数时把配置文件丢进去重启容器就行。--restartalways 保证机器重启后容器自动拉起这是生产最简单的一层自愈能力。想验证数据确实持久化做法很直接往MySQL里写一张表或几条记录然后docker stop mysql docker rm mysql再用同样参数docker run一个新容器查数据是否还在。实测验证过只要卷挂载正确数据完好无损。3.3 存储卷的管理、备份与迁移命名卷在Docker主机上的实际位置是 /var/lib/docker/volumes/卷名/_data但直接去操作这个目录不是好习惯。管理卷要用Docker自带命令docker volume ls docker volume inspect mysql-data docker volume prune备份一个卷最省事的方式是临时起一个容器挂载该卷将卷目录打包docker run --rm \ -v mysql-data:/source \ -v /backup:/target \ alpine tar czf /target/mysql-data-$(date %Y%m%d).tar.gz -C /source .恢复也容易把备份包解压到新挂载的卷目录即可。要注意MySQL在线状态下直接拷数据目录文件不是合格的备份方式最好在容器停止或用mysqldump导出逻辑备份物理备份和逻辑备份要分清楚用途。4. 网络配置详解端口映射与自定义网络的正确姿势4.1 Docker默认网络和它们各自的脾气安装完Docker后机器上默认会创建三个网络bridge、host、none。bridge是最常用的一种所有容器默认加入这个网络容器之间通过私有网段互相访问外部要访问容器就得靠端口映射。host网络让容器直接共享宿主机网络栈没有NAT隔离性能和兼容性较好但端口冲突和网络隔离也随之消失。none网络就是完全无网络极少场景用。随着使用深入你会发现默认bridge有个很烦人的限制容器之间只能靠IP地址访问不能靠容器名DNS直接通信容器重建一次IP还变了。要解决这个问题就得用自定义网络这也是Docker官方推荐的生产用法。4.2 创建自定义网络让容器用名字互通创建网络并让多个容器加入后容器之间可以直接用容器名访问Docker内置的DNS会解析容器名到对应的IP。这比靠固定IP硬编码可靠得多。docker network create --driver bridge app-net docker run -d --name web --network app-net nginx docker run -d --name backend --network app-net my-backend现在在backend容器里直接ping web或者用Web请求的Hostname填web就能解析到对方容器。容器重建后只要名字不变其他容器访问不受影响。提示自定义bridge网络不仅解决DNS问题还有独立的网络隔离边界。一个业务的所有组件放一个网段里另一个业务放另一个网段里互不相通比全堆在默认bridge里干净得多。4.3 端口映射的注意事项端口映射的基本语法是-p 宿主机端口:容器端口。写的时候有四个细节值得说说。第一尽量显式绑定宿主机IP比如-p 127.0.0.1:3306:3306表示只有本机可以访问这个端口而不是暴露到所有网卡安全上立刻上一个台阶。第二不要把常见的DB端口直接映射到公网网卡上MySQL、Redis默认端口一旦暴露出去弱口令爆破的脚本几秒钟就会扫到。第三反向代理场景里应用容器可以不映射端口只暴露在内部bridge网络里由Nginx容器对外提供服务。第四改端口映射必须删容器重建所以建容器前就先规划好端口避免后来反复折腾数据不变的容器。5. 资源限制与日志轮转防止容器拖垮宿主机5.1 给容器设置CPU和内存限额不限制资源的容器相当于一个没有刹车的客运车。曾经就有同事在一个共享服务器上跑了个未限内存的Java应用直接触发OOM把同机器上其他人的容器一起带崩了。所以正式业务里资源限制不是可选项是必选项。docker run时通过以下参数限制docker run -d --name nginx \ --memory512m \ --memory-swap512m \ --cpus1.0 \ nginx--memory限制内存上限--memory-swap把swap也锁死为同一值防止容器把内存压力转嫁到磁盘swap导致整个系统卡死--cpus限制容器最多使用1个CPU核心。关于这两个参数的取舍我的经验是内存限制一定要设因为内存超限的行为是即时的CPU限制则看场景如果是IO密集或计算密集任务建议严格设如果是平时没什么负载的Web服务设宽一点也没关系。5.2 日志轮转磁盘空间的好朋友前面daemon.json里已经讲了全局日志配置如果你有某个容器需要单独覆盖在docker run时也能指定docker run -d --name app \ --log-driver json-file \ --log-opt max-size20m \ --log-opt max-file3 \ nginx这套配置让单个容器最多保留三个20MB的日志文件总量60MB封顶。生产环境里我建议全局配置统一个别日志量巨大的服务单独调大max-file但一定保留max-size限制。日志文件不用手动删Docker会根据max-file自动滚动老文件自动覆盖。有一个特殊情况值得提醒如果用ELK等日志采集方案容器日志可能不需要在本地保留太多可以直接把日志驱动换成gelf、syslog、fluentd这类带转发能力的驱动让日志直接流向采集端本地不落盘省磁盘又安全。5.3 用自带的docker stats做第一层监控最简单的资源监控是docker stats不用装任何额外组件docker stats --no-stream看到的是所有运行容器的实时CPU、内存、网络、磁盘IO、进程数。我一般会在容器异常时先敲一条docker stats看看是不是资源撞墙了再往下排查。如果机器上容器很多命令后面可以跟容器ID或容器名比如docker stats web backend只显示关心的两个容器。持续监控则建议上cAdvisor或Prometheus全家桶但那是后话了不属于Docker配置的基础范畴。6. 安全加固最佳实践非root、Capabilities与只读文件系统6.1 别用root身份运行容器默认情况下容器内进程以root身份运行这跟宿主机root权限之间存在一整套复杂的权限翻译机制一旦容器被攻破攻击者拿到root权限后配合错误配置可能直接影响宿主机。降低风险的核心思路是给容器指定非root用户运行。最佳实践是在Dockerfile里就声明FROM nginx:stable RUN useradd -r -u 1001 appuser USER appuser这样容器内进程以普通用户身份运行即使被攻破权限也被牢牢限制在容器内部受限空间。我见过很多镜像不这样做其实改起来成本很低收益却很实在。如果用的是第三方镜像不方便改Dockerfile也可以在docker run时通过-u参数指定用户docker run -d --name myapp -u 1001:1001 myimage6.2 Capabilities与只读根文件系统Docker容器不是必须拥有Linux全部capabilities才能工作默认就给了一长串能力里面很多对业务毫无用处。生产环境建议在docker run时显式加固docker run -d --name app \ --cap-drop ALL \ --cap-add NET_BIND_SERVICE \ --read-only \ --tmpfs /tmp \ myimage--cap-drop ALL表示先剥夺容器所有能力然后通过--cap-add反馈你真正需要的例如一个Web服务只需要监听低端口小于1024端口需要NET_BIND_SERVICE就只给它这一个。--read-only让容器的根文件系统变成只读任何写文件操作都会被拒绝防止攻击者在容器里落地恶意文件如果应用确实要写临时文件就把对应目录挂tmpfs例如上面的--tmpfs /tmp。这项配置落地后很多常规的攻击路径会被直接掐断。代价是应用对文件系统的写入行为需要提前梳理会花点时间但我觉得这是全篇最值得照着抄的安全配置。6.3 用户命名空间与SELinux/AppArmor进一步加固可以打开用户命名空间映射把容器内的root用户映射成宿主机上的普通用户这样即使容器内用root干了坏事宿主机看到的UID也是普通用户权限。配置位置还是在daemon.json{ userns-remap: dockremap }改完重启Docker后容器内外用户会被重新映射。启用这个特性后要注意卷挂载的权限管理方式会变化因为容器内看到的UID和宿主机的真实UID不一样很多人第一次开这个特性后发现数据卷文件权限不对了就是忘掉了这层映射。SELinux或AppArmor是Linux内核提供的强制访问控制机制。Ubuntu/Debian用AppArmorCentOS/RHEL系用SELinux。开启状态下Docker会自动为容器加载默认策略进一步缩小攻击面。如果遇到容器权限报错而系统提示SELinux正在拦截不要急着setenforce 0而是给容器加--security-opt labeltype或者调整相关策略保证系统安全性的前提下解决问题。7. 联合部署Docker Compose配置一个Nginx/PHP开发环境7.1 为什么用Compose而不是一长串docker run业务稍微复杂点服务通常不止一个Nginx做反代后端跑PHP数据库放在MySQL里缓存用Redis。用docker run命令逐条启动几十行参数敲下来容易出错还难维护。Compose把服务编排写进一个YAML文件一条docker compose up -d就能全部拉起配置还可以走版本控制这属于配置最佳实践里最不该省的一步。Compose本身是一个独立插件新版Docker一般已内置检查docker compose version没装的就用包管理器装一下Ubuntu系可以用 apt install docker-compose-pluginCentOS系用 dnf install docker-compose-plugin也可以直接安装独立二进制docker-compose。7.2 一个完整可用的docker-compose.yml实例下面是我常用的一个Web开发环境编排文件version: 3.8 services: nginx: image: nginx:stable ports: - 80:80 - 443:443 volumes: - ./nginx/conf:/etc/nginx/conf.d:ro - ./www:/var/www/html:ro depends_on: - php networks: - app-net php: image: php:8.2-fpm volumes: - ./www:/var/www/html environment: TZ: Asia/Shanghai networks: - app-net mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: change-me MYSQL_DATABASE: appdb volumes: - mysql-data:/var/lib/mysql networks: - app-net redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis-data:/data networks: - app-net volumes: mysql-data: redis-data: networks: app-net: driver: bridge这个文件把Nginx、PHP-FPM、MySQL、Redis四个服务串了起来亮点有三处一是PHP和Nginx通过绑定挂载共享同一个代码目录改代码不用重建容器二是MySQL和Redis分别用命名卷持久化数据三是depends_on解决启动顺序。值得说明一点depends_on只能保证服务启动先后顺序并不能保证MySQL真正就绪所以应用层代码最好加上数据库连接重试逻辑这是新手最容易忽略的问题。7.3 常用Compose操作与升级回滚启动和停止docker compose up -d docker compose ps docker compose logs -f nginx docker compose downdown命令带-v的话会一起删除volume数据就没了操作时眼睛睁大点。升级某个服务镜像后重新拉取docker compose pull php docker compose up -d --no-deps php第一次执行up的时候如果没有对应镜像Compose会自动拉取日常用起来其实比docker run的交互思路顺滑不少。回滚操作也很简单把某个服务改回旧镜像版本再up一次就行关键是要养成给镜像打tag的习惯别都用latest回滚时会欲哭无泪。8. 常见故障排查速查表启动失败、端口冲突、权限问题8.1 高频问题与解决思路遇到问题第一件事永远是看日志docker logs 容器名是定位一切的起点。下面整理了我这两年遇到频率最高的几类问题。现象排查顺序常见原因容器启动后立刻退出docker logs 容器名命令或入口点错误、环境变量缺失、依赖服务未就绪端口映射不生效ss -lntp 检查宿主机端口宿主机端口被占用或只监听了127.0.0.1拉取镜像超时docker info 检查Registry Mirrors未配置镜像加速或加速地址失效容器内无法访问外网docker run --dns 8.8.8.8 测试宿主机DNS配置问题或容器DNS解析失败磁盘空间告急docker system df日志未轮转、悬空镜像过多权限报错Permission deniedgetenforce或ls -Z 检查策略SELinux/AppArmor策略拦截或卷目录权限不对8.2 排查实操一个MySQL容器启动失败的完整过程举一个真实案例复现整个排查流程。现象是docker run mysql之后容器状态一直显示Exited直接看日志docker logs mysql 21 | tail -n 30日志里出现“Cant connect to local MySQL server through socket /var/run/mysqld/mysqld.sock”第一反应是MySQL进程没有正常启动。再往下翻几行如果看到“--initialize specified but the data directory has files in it”原因基本就能确认这是一个数据目录初始化冲突。容器第一次启动前如果旧目录里已有数据或者挂载了一个非空的卷MySQL初始化步骤会因目录非空而放弃。解决方法也直白把名字改了换一个新卷重建容器或者用确认备份之后的方式处理旧目录。不要试图直接删数据目录除非明确数据已备份。排查逻辑其实是固定套路启动失败先看日志日志不清不楚就看系统dmesg和事件状态再不行进容器里复现命令。沿着日志层、资源层、网络层、数据层逐层往下大多数问题30分钟内都能定位。最后说点我在实际操作里的体会。Docker配置这件事很多时候不是“不会配”而是“没意识到该配”。默认配置能用但远不够好等出了问题再去查方案成本远高于一开始花半天把所有配置捋一遍。我每次接手新服务器都会先把daemon.json、数据目录、存储驱动、日志轮转这几样基础项过一遍再跑业务容器后面省下的大量排查时间都是这笔初始投入还的利息。如果你现在手上正好有一台刚装完Docker的Linux机器别急着docker run先把上面这些配置逐项验证一遍再往上搭你的应用。