做运维这些年,被“环境问题”折腾的次数,比被业务问“怎么又挂了”的次数还多。新同事入职第一周,光搭开发环境就能搭两天;测试说功能没问题,一到生产就报依赖缺失;老板让扩容,新机器从头装JDK、装Nginx、调配置,一搞就是一下午。这些事情多了以后,我几乎逢人就说:该把环境本身也当成代码来管了,而Docker,正是把这句话落到实处的关键工具。这篇文章不打算抄官方文档,我就围绕运维日常,从安装部署、常用命令、生产实战、网络排错到企业级落地,把Docker这条线完整梳理一遍。适合刚接触Docker的运维新人,也适合已经在用但想补齐知识盲区的老手。
1. 先搞清楚Docker到底解决了运维的什么痛点
1.1 环境一致性:从“在我这好好的”到“换台机器也一样”
先聊一个所有运维都经历过的场景:开发在本地跑得好好的,发到测试环境就报缺少某个动态库;测试环境验证通过,上生产又因为Nginx配置、JDK版本不一致导致服务起不来。传统做法是写一份部署文档,从操作系统版本到依赖包版本一行行列清楚,然后让执行的人照着敲。问题是文档会过时,人也容易漏步骤,机房新机器跟老机器系统版本还不一样,最后排查出来的问题五花八门。
Docker镜像把所有东西一次性打包进去:代码、运行时、系统库、配置文件、启动命令,全在一个不可变单元里。镜像到哪,应用环境就到哪,不再有“换台机器就崩”的玄学问题。打一个通俗的比方:以前是散装搬家,锅碗瓢盆、衣服书报混在一起,到新家发现少了个勺子;现在是把所有东西分类装进标准集装箱,搬到哪里都是同一个箱子,内容物完全一致。这个“一致性”是Docker对运维最大的价值,也是我向团队推Docker时最常讲的一点。
1.2 为什么容器比虚拟机轻:内核共享与隔离机制
很多运维刚接触容器时会问:这不就是轻量级虚拟机吗?其实原理完全不同。虚拟机是在宿主机上通过Hypervisor虚拟出一整套硬件,每个虚拟机里都有一个完整的Guest OS,启动时要经历完整的BIOS、内核引导过程,所以慢,资源占用也大。容器则直接共享宿主机内核,通过Linux的namespace做隔离、cgroup做资源限制。
namespace隔离了进程、网络、挂载点、主机名、IPC等资源,所以容器里的进程看起来像在一个独立系统里;cgroup限制CPU、内存、IO等资源,防止某个容器把宿主机资源吃光。用房子来类比:虚拟机是各自独立的一栋房子,地基、墙体、水电全要重新建;容器是同一栋楼里的精装单间,公共基础设施(内核)共享,但房间之间互不干扰。正是因为共享内核,容器启动才是秒级,一台物理机可以同时跑几十个容器而不觉得吃力。
还有一个知识点容易被忽略:镜像的分层机制。构建镜像时,每一行指令会生成一个只读层,最终容器运行时在上层加一个可写的容器层。多个镜像如果底层基础镜像相同,它们就共享这些底层,不需要重复拉取和存储。这也是为什么拉完一个ubuntu镜像,再拉基于它的业务镜像时会快很多的原因。
2. 从零部署:Linux和Windows两条路线的实操记录
2.1 Linux安装Docker Engine:从换源到开机自启
我在生产环境遇到最多的是Ubuntu和CentOS。先说Ubuntu。不建议直接apt install docker.io,因为仓库里的版本往往比较旧。我习惯走官方docker-ce仓库,如果服务器在境内网络拉不到官方源,就换成国内的镜像源。步骤大概是:
- 卸载可能存在的旧版本:
apt-get remove docker docker-engine docker.io containerd runc - 安装依赖:
apt-get update && apt-get install ca-certificates curl gnupg lsb-release -y - 添加GPG key和仓库(以阿里云镜像源为例):
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg - 写入仓库:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null - 安装:
apt-get update && apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y - 启动并设置开机自启:
systemctl enable --now docker
CentOS对应的是yum install yum-utils、yum-config-manager --add-repo,然后yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin。
装完马上做三件事。第一,配置镜像加速。编辑/etc/docker/daemon.json,写入registry-mirrors,我一般用阿里云或腾讯云的加速地址,然后systemctl restart docker。第二,检查存储驱动,docker info看到Storage Driver是overlay2就没问题,如果还是老的devicemapper建议趁早换,overlay2性能和稳定性都好很多。第三,如果根分区空间紧张,改>[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-time-zone=+08:00 max_connections=500
然后启动命令:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -v /opt/mysql/data:/var/lib/mysql \ -v /opt/mysql/conf:/etc/mysql/conf.d \ -v /opt/mysql/init:/docker-entrypoint-initdb.d \ -e MYSQL_ROOT_PASSWORD='此处替换为强密码' \ -e TZ=Asia/Shanghai \ --restart unless-stopped \ mysql:8.0解释三个关键点。第一,/docker-entrypoint-initdb.d目录下的.sql脚本只在数据目录首次初始化时执行,所以建库建表的初始化脚本放这里最合适,但注意如果data目录已经有数据,脚本不会重复执行。第二,时区要双保险,既有容器环境变量TZ,又有MySQL的default-time-zone,不然查NOW()还是返回UTC时间。第三,MySQL 8.0默认认证插件是caching_sha2_password,旧版客户端连接会报错,要么改客户端驱动,要么启动时加--default-authentication-plugin=mysql_native_password,但现在还是建议推驱动升级而不是降级插件。
验证的时候进容器跑一下:docker exec -it mysql8 mysql -uroot -p,然后执行show variables like '%character%';和select now();,确认字符集和时区都对。数据持久化验证也很简单:docker stop mysql8 && docker start mysql8,数据还在就说明卷挂载没问题。
4.2 用Docker Compose搭建Redis主从:不写命令也能编排
单个容器用docker run还凑合,一旦涉及多容器协作,比如Redis主从,写一堆run命令又长又容易漏参数。Compose的作用就是用YAML把服务定义下来,一整个项目可以版本化管理,换台机器git clone下来docker compose up -d就起来了。
我常用的目录结构:
redis/ ├── docker-compose.yml ├── master/ │ └── redis.conf └── slave/ └── redis.conf主节点redis.conf核心配置:bind 0.0.0.0、protected-mode yes、requirepass 密码、appendonly yes。从节点多一行replicaof redis-master 6379和masterauth 密码。注意这里replicaof写的是服务名redis-master而不是IP,这是Compose网络里容器间自动DNS解析的好处。
docker-compose.yml核心内容:
services: redis-master: image: redis:7 container_name: redis-master ports: - "6379:6379" volumes: - ./master/redis.conf:/etc/redis/redis.conf command: ["redis-server", "/etc/redis/redis.conf"] redis-slave: image: redis:7 container_name: redis-slave depends_on: - redis-master volumes: - ./slave/redis.conf:/etc/redis/redis.conf command: ["redis-server", "/etc/redis/redis.conf"]启动后验证从节点:docker exec -it redis-slave redis-cli -a 密码 info replication,看到master_link_status:up就说明主从同步正常。这个部署方式我用了很久,胜在配置可视化、可控、可复制。踩过的坑也提醒一下:从节点配置里如果忘了masterauth,主从一建立就一直处于down状态,日志里能看到NOAUTH Authentication required;主节点如果设了bind 127.0.0.1,从节点也连不上,初期调试时bind 0.0.0.0是最省事的。
5. 网络问题是运维的隐形杀手:docker网络不通排查实录
5.1 网络模式与端口映射原理
Docker网络出问题,很多运维第一反应就是重启Docker,结果问题依旧。要靠谱排查,先得理解Docker网络的底层逻辑。
默认的bridge网络,宿主机上有个docker0网桥,每个容器创建时会分配一对veth虚拟网卡,一头连着容器的eth0,另一头插在docker0上。容器之间通过网桥互通,容器访问外网时,宿主机内核做NAT转发(MASQUERADE)。外部访问容器映射端口时,靠的是iptables的DNAT规则把宿主机端口转发到容器IP和端口。所以-p 3306:3306的本质不是直接开个洞,而是加了一条DNAT规则。
Docker提供了好几种网络模式:bridge(默认)、host(直接用宿主机网络栈,没有独立IP,端口直接监听在宿主机上)、none(无网络)、container(共享另一个容器的网络栈)。还有一种用户自定义bridge网络,docker network create mynet,容器加入后可互相用容器名解析,解决了默认bridge网络下容器不能通过名字互相访问的问题。
5.2 高频故障与排查手段
我整理过一份自查手册,列几个高频网络事故:
容器内访问外网失败。先ping网关、再nslookup确认DNS是否正常。常见原因是宿主机防火墙把FORWARD链搞乱了,或者iptables -F把DOCKER链清了。见过一次因为执行了systemctl stop firewalld导致Docker网络全部瘫痪的案例,firewalld停止时会重刷iptables规则,Docker的NAT规则被清掉,重启Docker才恢复。
外部访问容器端口不通。先确认服务监听在0.0.0.0:进入容器netstat -tlnp看看。如果容器里监听的是127.0.0.1,外部自然访问不到。接着看宿主机iptables -t nat -L DOCKER,确认DNAT规则存在。再检查云安全组,很多云厂商的安全组没有放行对应端口,这是最容易忽略的一环。
容器之间不通。分别确认两个容器是否在同一个自定义网络里,docker network inspect 网络名看Containers列表。跨网络通信需要手动连接:docker network connect mynet container名。
容器重启后IP变了。这是默认bridge网络下常见问题,容器每次启动分配的IP都可能变。解决办法是创建自定义网络加入容器,或者在自定义网络里固定IP:docker run --ip 192.168.100.10,但前提是网络创建时指定了subnet。
排查工具上,docker inspect看IP和网络配置;nsenter -t 容器PID -n进入容器网络命名空间用宿主机工具排错,比在容器里装iproute2方便;宿主机上用tcpdump -i docker0抓包定位丢包位置。网络问题别靠猜,抓包和看iptables规则往往一抓一个准。
6. 从能用走向好用:企业级落地要补哪些功课
6.1 镜像仓库与版本管理:私有仓库和tag规范
在一个团队里,如果没有统一的镜像仓库,所有人各拉各的公共镜像,版本乱得没法查。企业级落地第一步就是搭私有仓库。Harbor是我用得最多的,功能完备程度明显高于裸的Registry:带项目管理、访问控制、漏洞扫描、镜像复制。
部署Harbor用官方安装包加一条docker-compose命令即可,生产上建议用外部数据库和对象存储,别把数据存在Harbor容器里。镜像tag规范要提前定死,我团队现在用的是组件名:环境-日期-构建号,比如gateway:prod-20250116-8f2a1c3,既能快速定位出包时间,又能对应到具体代码commit,回滚时直接改tag重新拉取。
镜像瘦身是另一个效率点。多阶段构建非常实用,拿Go项目举例,第一阶段的Golang基础镜像编译出二进制,第二阶段用alpine或distroless只拷贝二进制和必要资源,最终镜像体积可能从几百MB降到几十MB。Dockerfile里记得写.dockerignore,把.git、node_modules这些无效文件排除,能减少不少构建上下文传输时间。
6.2 资源限制、日志与监控:别让容器失控
生产环境最怕发生的事:容器不受控把宿主机内存吃光,导致整个服务器上所有服务一起挂掉。所以run的时候必须限制资源,我是把单容器内存限制在2G、CPU限制在2核起步,核心服务再按压力调:
docker run --memory=2g --cpus=2 --pids-limit=200 ...cgroup会强制约束这些限制,超出的内存会被OOM Killer杀掉容器而不是拖垮宿主机。这个限制带来的副作用是如果业务确实需要更多资源,容器会频繁重启,所以资源不是拍脑袋定的,要结合压测数据来。
日志策略也要提前规划。Docker默认的json-file日志驱动会无限增长,容器日志把磁盘打爆是生产事故高发项。创建容器时加日志轮转配置:--log-driver json-file --log-opt max-size=50m --log-opt max-file=3,或者直接在daemon.json里全局配置,省得每个容器单独加。日志集中化我用Filebeat加ELK,Filebeat以DaemonSet或systemd服务方式跑在宿主机上采集/var/lib/docker/containers下的日志,统一进Elasticsearch。
监控方面,docker stats只能应急看,要长期保留需要上cAdvisor加Prometheus加Grafana这套组合。cAdvisor采集容器CPU、内存、网络、磁盘指标,Prometheus负责存储和告警规则,Grafana展示面板。初期接入成本不高,但对提前发现资源容量问题帮助很大。
6.3 安全基线:能用和安全是两回事
很多团队Docker用得顺手,安全基本没意识。这里列几条最基础的安全基线,够用了。
不要以root运行应用,Dockerfile里写USER 1000:1000,给应用一个普通用户。不要给容器加--privileged,这个参数等于把宿主机的全部权限都交给容器,生产上几乎永远不需要。不要轻易挂载/var/run/docker.sock进容器,一旦容器被攻破,等于拿到了宿主机的Docker控制权。不在启动命令里明文写密码,用env_file或密钥管理,因为docker inspect能看到环境变量,明文密码等于裸奔。镜像扫描用Trivy,拉镜像后扫一遍再上线,高危漏洞限期升级。
6.4 从Compose到编排平台:K8s是方向,但不是唯一方向
企业规模上来以后,单机Compose管理几十个容器已经很吃力,会想上编排平台。Docker Swarm逐渐边缘化,K8s是主流。但我想说实话:如果你们服务器规模在几十台以下,没有特别强的调度需求,先把Compose用利索,再配合CI/CD和监控,已经很能打。盲目上K8s只会带来额外的运维负担。
真要往K8s走,运维要补的知识点是Deployment和StatefulSet怎么选、Service与Ingress的流量路径、ConfigMap和Secret的配置管理、PV/PVC的存储抽象、Helm的包管理。迁移思路上一开始不要全量搬,挑无状态服务先容器化验证,再逐步处理有状态组件。这个演进过程是梯次推进的,不是一把梭。
最后聊几句心里话。Docker这东西,用起来不难,难的是把运维该想的兜底都想好:磁盘、日志、网络、安全、版本、资源限制,每一个都在线上炸过。我在团队里推Docker时反复强调一句话:别把容器当虚拟机用,镜像和编排才是它的灵魂。先把基础命令跑熟,再把生产兜底补完,最后再谈K8s,这条路走下去,运维工作会轻松很多,也会有很多时间做真正有价值的优化。