1. 为什么我劝每个开发者都学一学Docker
先说个经常遇到的场景:本地跑得好好的代码,同事一拉下来就报错;你开发用的是Windows,线上服务器是Linux,一到部署就各种环境问题;新同事入职第一天,光搭开发环境就搭了半天。
这些问题的根源只有一个——环境不一致。而Docker解决的恰恰就是这件事:把你的应用和它依赖的环境一起打包,做到一次构建,到处运行。
Docker这个词这几年只要聊到后端、部署、运维,基本绕不开。它本质上是个容器化平台,利用操作系统级的虚拟化技术,把进程隔离在一个个互不干扰的“容器”里。和虚拟机不同,容器不虚拟一套完整的操作系统,而是直接复用宿主机的内核,只打包应用本身和它需要的依赖,因此启动快、资源占用低、移植性强。
这篇文章不是讲API文档,而是从我的实际操作经验出发,带你走一遍Docker从入门到能自己干活的全过程:安装、核心概念、常用命令、写Dockerfile、用数据卷、玩转Compose,最后附上我踩过的坑和排查问题的思路。
无论你是前端、后端、测试,还是刚接触服务端部署的新人,只要能把这篇跟下来,Docker就能成为你日常开发里一个特别顺手的工具。
2. Docker到底解决了什么问题
2.1 环境不一致这个老大难
没有Docker之前,我们在部署一个应用时,要手动装运行时、装依赖库、改配置文件。今天装个MySQL,明天装个Redis,每个版本还有各自的依赖要求,稍有不慎就冲突。更要命的是,开发环境和生产环境的操作系统、软件版本不一致,代码明明测试通过了,一到服务器上就崩。
Docker的思路是:把应用和它所需要的环境打包成一个镜像,这个镜像是不可变的,里面装了什么就是什么。任何一台装了Docker的机器,拉取这个镜像运行,得到的都是完全一致的环境。开发时用这个镜像,测试时用这个镜像,上线运行也用这个镜像。
2.2 一个类比帮你理解镜像和容器
想象你要搬家。传统方式是:把家具一件件拆开、打包、搬运、到了新家再一件件组装,遇到尺寸不合还得现场改。Docker的方式是:把所有家具连同摆放位置、装修方式,直接装进一个标准的集装箱里,到了新家,整个集装箱放进去就能住。
这个集装箱就是镜像,集装箱运行起来、里面的人开始生活,那个状态就是容器。
镜像是一个只读的模板,它定义了环境里有什么;容器是镜像运行后的实例,可以启动、停止、删除,就像从同一个安装包打开的不同软件窗口,彼此独立、互不干扰。
2.3 隔离带来的额外红利
除了解决环境一致性,Docker还带来了进程级别的隔离。容器与容器之间默认是隔离的,有各自独立的文件系统、网络栈、进程空间。你可以同时在一台机器上跑多个不同版本的MySQL,只要端口映射做得好,它们谁也不影响谁。
资源利用效率也高很多。虚拟机需要给每个系统预留固定的内存和CPU,而容器共享宿主机内核,按需使用资源。一台普通配置的服务器,用虚拟机可能最多跑三四个实例,用Docker跑十几个甚至几十个都没问题。
3. 安装和第一个容器的完整过程
3.1 安装前的认知准备
Docker本身是CS架构,有客户端和服务端。你平时敲的命令是客户端,真正干活的是一堆后台服务。Linux上安装Docker最顺滑,Mac和Windows上现在都有Docker Desktop,安装包点下一步就行。
生产服务器建议用Linux发行版安装Docker Engine,不要在服务器上装Docker Desktop,桌面版是为本地开发设计的,多了很多不需要的组件。
3.2 Linux环境安装步骤
以我常用的Ubuntu/Debian系为例,安装过程就几步:
sudo apt update sudo apt install -y ca-certificates curl gnupg 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 echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin装完后启动服务并设置开机自启:
sudo systemctl enable --now docker验证是否安装成功:
docker version看到Client和Server两段信息都显示版本号,就说明安装成功了。
注意:很多人在这一步发现docker version能显示Client信息,但Server部分报错,通常是因为Docker服务没启动,或者当前用户没有权限访问Docker守护进程。
3.3 给当前用户加权限
安装完成后,每次敲docker命令都要加sudo实在太烦了。把当前用户加入docker组,重启终端后就可以免sudo使用:
sudo usermod -aG docker $USER newgrp docker这里有个安全点要说清楚:加入docker组就相当于给了这个用户root权限,因为Docker守护进程本身是以root身份运行的。如果你在多人共用的服务器上,别随便给账号加docker组权限。
3.4 运行第一个容器
环境就绪后,用一条经典的命令验证整个链路:
docker run hello-world如果镜像本地不存在,Docker会先去仓库拉取hello-world这个只有几KB的测试镜像,然后创建一个容器运行它。看到一段Hello from Docker!的输出就说明整个流程打通了。
至此你已经完成了Docker的安装和最基本的使用。接下来需要系统理解Docker的几个核心概念,否则后面用起来会一直觉得别扭。
4. 核心概念串讲与常用命令实操
4.1 镜像、容器、仓库三者关系
可以把镜像理解成一个只读模板,容器是模板的实例,仓库是存放镜像的地方。
拉镜像是从仓库把镜像下载到本地,运行镜像是基于模板创建一个容器。一个镜像可以创建多个容器,每个容器相互独立。修改容器里的文件不会影响镜像,除非你用docker commit把容器当前状态重新打包成新镜像。
删除镜像前要先把基于它的所有容器都删掉,否则会报错说镜像正被容器使用。
4.2 镜像管理命令
平时最常用的几个镜像命令:
# 搜索仓库里的镜像 docker search nginx # 拉取镜像,不写版本号默认拉latest docker pull nginx docker pull nginx:1.25 # 查看本地镜像列表 docker images # 查看镜像详细信息 docker inspect nginx # 删除镜像,-f 表示强制删除 docker rmi nginx这里建议养成指定镜像版本的习惯。latest标签是滚动更新的,今天拉的和三个月后拉的可能完全是两个东西。生产环境一定要锁定版本号,否则哪天别人重新部署,拉到的镜像已经变了,问题排查起来非常痛苦。
4.3 容器生命周期管理
容器相关的命令是整个Docker使用频率最高的部分:
# 运行容器,-d表示后台运行,--name指定容器名,-p映射端口 docker run -d --name my-nginx -p 8080:80 nginx # 查看运行中的容器 docker ps # 查看所有容器,包括已退出的 docker ps -a # 停止容器 docker stop my-nginx # 启动已停止的容器 docker start my-nginx # 重启容器 docker restart my-nginx # 进入容器内部,-it表示交互式终端 docker exec -it my-nginx /bin/bash # 查看容器日志 docker logs -f my-nginx # 删除容器,-f强制删除运行中的容器 docker rm -f my-nginx-p 8080:80这个参数特别关键。冒号左边是宿主机端口,右边是容器内端口。外部访问宿主机的8080端口,请求会被转发到容器内的80端口。如果宿主机端口被占用,可以换个端口,比如-p 8081:80。
4.4 端口映射和资源限制的细节
端口映射在docker run时用-p指定,它的原理是借助iptables或防火墙规则做端口转发。如果没有做端口映射,容器内的服务外部是访问不到的。
生产环境部署时,还要给容器加资源限制,防止某个容器吃光整台机器的内存:
docker run -d --name my-app -p 5000:5000 --memory=512m --cpus=1.5 my-app-image--memory限制容器最大内存,--cpus限制容器占用的CPU核数。这个习惯一定要养成,否则线上会出现一个容器把整个宿主机拖垮的情况。
5. 数据卷:让容器里的数据活起来
5.1 为什么容器数据存不住
容器本身是临时性的。删掉容器,容器里所有文件都没了。如果你在容器里创建了数据库文件,docker rm一执行,数据就彻底消失。很多新手在这里吃过亏。
解决方案是数据卷。它本质上是宿主机上的一个目录,通过挂载的方式与容器内的目录建立映射。容器写入的数据实际落在宿主机上,即使容器被删掉,数据依然还在。
5.2 绑定挂载与命名卷
绑定挂载是直接把宿主机的某个目录映射到容器内:
docker run -d --name my-nginx -p 8080:80 -v /data/nginx/html:/usr/share/nginx/html nginx这条命令把宿主机的/data/nginx/html目录映射到容器内的/usr/share/nginx/html。修改宿主机目录下的文件,容器内立刻生效。我平时用这种方式做前端项目调试,代码改了刷新页面就是新内容。
命名卷由Docker管理存储位置,适合保存数据库等不需要直接关注具体路径的数据:
docker volume create mysql-data docker run -d --name mysql -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=secret mysql:8.0-v里如果冒号前是一个简单名字,就是命名卷;如果是绝对路径,就是绑定挂载。两者区别在于操作的透明程度。
5.3 数据卷的权限问题
挂载目录时最容易踩的坑是权限。容器内进程通常以非root用户运行,挂载目录的属主和权限不合适,容器启动就会报错。
nginx的镜像一般用户是nginx,目录权限需要让nginx用户能读写;而MySQL官方镜像则默认以mysql用户运行数据目录。解决思路是先以root方式进入容器查看用户ID,或者直接在宿主机上设置目录属主为容器内用户对应的UID。
更简单粗暴的临时做法是给目录打满权限,但生产环境我不建议这样,数据安全问题不是小事。
提示:挂载单个文件时也可以用-v,比如把宿主机上的nginx.conf映射到容器内的/etc/nginx/nginx.conf。这样修改配置后,执行docker exec nginx -s reload就能让配置生效,不需要重新构建镜像。
6. 自己写一个Dockerfile打包应用
6.1 Dockerfile的基本写法
公共镜像拉下来就能用,但真正实际的项目,需要自己构建应用镜像。Dockerfile就是构建镜像的配方文件。
我以一个简单的Python Web应用为例:
# 指定基础镜像 FROM python:3.11-slim # 设置工作目录 WORKDIR /app # 先复制依赖文件,利用缓存提升构建速度 COPY requirements.txt . # 安装依赖 RUN pip install --no-cache-dir -r requirements.txt # 复制项目代码 COPY . . # 暴露端口 EXPOSE 8000 # 指定容器启动后的执行命令 CMD ["python", "app.py"]在项目根目录执行:
docker build -t my-web-app:v1 .-t给镜像打标签,.表示构建上下文是当前目录。构建完成后,用docker images能看到这个镜像。
6.2 多阶段构建的技巧
对于编译型语言,多阶段构建能极大缩小镜像体积。以Go项目为例:
# 第一阶段:编译 FROM golang:1.21 AS builder WORKDIR /src COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o app . # 第二阶段:运行 FROM alpine:3.19 WORKDIR /root/ COPY --from=builder /src/app . EXPOSE 8080 CMD ["./app"]最终镜像里只有编译好的二进制的文件和运行时需要的东西,没有任何源代码和编译工具链。从几百兆缩到几十兆,部署速度和安全性都大幅提升。
6.3 .dockerignore文件
用Docker构建时,docker build会把目录下所有文件都发给守护进程。如果项目里有node_modules、.git、缓存目录,这些文件会拖慢构建速度,有时还会导致构建上下文超过限制。
在项目根目录创建.dockerignore文件:
.git node_modules *.log Dockerfile .dockerignore这样构建上下文会小很多,构建速度明显加快。
6.4 构建缓存的使用
Docker构建会自动利用缓存。判断逻辑是:Dockerfile里每一条指令如果没变化,就沿用之前的缓存层;一旦某条指令变了,后面的层全部重新执行。
因此我把COPY requirements.txt单独放在前面,依赖安装指令就不会因为代码变化而重新执行。如果先COPY全部代码再RUN pip install,每次代码变动都会触发完整的依赖重装,慢得要死。
6.5 镜像体积优化思路
除了多阶段构建,还有几个很实用的优化方向:
- 基础镜像尽量选-slim或alpine版本,体积更小。
- 合并RUN指令,在一个RUN里做完整件事,减少镜像层数。
- 清理安装后的缓存文件,比如apt-get clean、pip的--no-cache-dir。
- 不需要的调试工具不要装进生产镜像,安全性和体积都受益。
7. 用Docker Compose一键编排多容器服务
7.1 Compose的价值
实际项目中,很少有单容器跑到底的服务。一个Web应用背后可能依赖MySQL、Redis、Nginx多个组件。每个组件一个docker run命令,管理还勉强能接受,但加环境变量、网络、依赖关系,命令越来越长,维护成本飙升。
Docker Compose用YAML文件定义整个服务栈,一条命令启动所有服务,每个服务在独立的网络里相互通信,配置全部集中在同一个文件里。
7.2 一个完整的Compose文件
以Web应用+MySQL+Redis为例:
version: "3.9" services: web: build: . ports: - "5000:5000" environment: - DB_HOST=mysql - REDIS_HOST=redis depends_on: - mysql - redis mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD=rootpass - MYSQL_DATABASE=appdb volumes: - mysql-data:/var/lib/mysql redis: image: redis:7-alpine volumes: mysql-data:在文件所在目录执行:
docker compose up -d所有服务依次启动,web服务里的DB_HOST直接写mysql就行了,Compose会创建默认网络,服务名即主机名。
7.3 Compose常用命令
日常维护最常用的几个:
# 启动服务 docker compose up -d # 查看服务日志 docker compose logs -f web # 停止服务 docker compose down # 停止并删除数据卷(连同数据一起删,谨慎使用) docker compose down -v # 重新构建镜像后启动 docker compose up -d --build7.4 环境差异的管理
本地开发、测试、生产环境,配置往往有差异。Compose支持多个配置文件叠加:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d基础文件放通用配置,prod文件覆盖接口地址、日志级别、资源限制等生产差异。这样一套基础配置可以适配多个环境。
7.5 Compose实战部署一个完整项目
我常用Compose部署个人博客系统:前端Nginx、后端API、数据库三个服务。
前端镜像挂载构建产物,由Nginx托管;后端API容器连接数据库,通过环境变量注入数据库连接信息;MySQL数据用命名卷持久化,保证重建容器后数据不丢。
整个部署流程就三步:写好docker-compose.yml、docker compose up -d、等待启动完成后检查日志。相比以前手动安装依赖、配置服务、写启动脚本那一堆操作,效率提升不是一点半点。
8. 网络模式与容器间通信
8.1 默认的bridge网络
Docker安装后会创建几个默认网络:bridge、host、none。默认情况下,docker run创建的容器都挂在bridge网络上,容器之间通过IP可以互相访问,但跨主机访问就不是默认支持的了。
同一台机器上的多个容器通信,最推荐的方式还是通过自定义网络加容器名。在自定义网络里,Docker内置了DNS解析,直接用容器名代替IP地址,彻底解决容器重建后IP变化的问题。
docker network create my-network docker run -d --name app1 --network my-network my-app1 docker run -d --name app2 --network my-network my-app2在app1容器里访问app2,直接访问http://app2:8080就行,不需要查IP。
8.2 host和none模式
host模式让容器直接使用宿主机的网络栈,性能和端口映射方面更直接,但失去了网络隔离性。如果某个容器需要监听大量端口,host模式会方便,但生产环境不建议大量使用。
none模式让容器不连接任何网络,适合做一些纯计算类任务或者对安全性要求极高的场景。
8.3 端口映射的深入学习
-p参数除了映射单个端口,还有几种变体:
# 随机分配宿主机端口 docker run -d -P nginx # 指定IP和端口映射,只允许通过特定IP访问 docker run -d -p 127.0.0.1:8080:80 nginx # 映射多个端口 docker run -d -p 8080:80 -p 8443:443 nginx映射的是宿主机端口,容器内端口由镜像定义决定,改不了也没必要改。
9. 常见问题排查与避坑实录
9.1 镜像拉取超时的解决思路
新环境上docker pull官方镜像经常卡住。国内网络访问官方仓库不稳定,除配置镜像加速源外没有更稳妥的办法。镜像加速配置文件在/etc/docker/daemon.json:
{ "registry-mirrors": ["https://你的镜像加速地址"] }修改后重启Docker:
sudo systemctl restart docker配置完先拉个小镜像测试一下,确认加速生效,再拉大镜像,几秒钟就能感受明显差异。如果换了多台服务器,每个服务器都要配一遍。
9.2 容器一直在重启
docker ps发现容器STATUS列是Restarting,代表容器在反复启动退出。排查思路:
# 查看容器详细状态 docker ps -a # 查看容器日志 docker logs 容器名常见原因有三个:启动命令写错、依赖的服务还没就绪就启动了、资源限制设太小导致进程被杀。前面两个看日志基本能定位,最后一个要检查docker inspect里OOMKilled字段是否为true。
9.3 磁盘被镜像占满
长期使用Docker,/var/lib/docker会越来越大。清理方式:
# 清理悬空镜像和停止的容器 docker system prune # 清理所有未使用的资源,包括缓存 docker system prune -a --volumes第一行是常规清理,第二行会删掉所有没在用的镜像和数据卷,执行前一定确认没有需要保留的数据。我在本地开发时习惯每周清理一次,生产环境只清理悬空镜像,不碰数据卷。
9.4 端口冲突怎么办
启动容器时报端口已经被占用,先找出占用端口的进程,再决定是换端口还是停掉冲突的进程:
# 查看宿主机端口占用 sudo netstat -tulpn | grep 8080 # 查看Docker里有没有容器占用了这个端口 docker ps -a | grep 8080停掉容器之后端口会释放,但有时容器明明停了,端口还没释放,这通常是容器内的进程没被杀干净。确认docker rm -f之后再检查端口,实在不行重启Docker服务。
9.5 容器退出后日志丢失
容器的日志默认由Docker管理,容器删除后日志一般也删除了。如果应用日志写入的是宿主机挂载的目录,那日志会持久化。最佳实践是让应用把日志写到挂载的数据卷里,再用logrotate或外部日志工具做集中管理。想看历史日志,直接去挂载目录翻文件就行。
9.6 时区和中文乱码问题
官方镜像默认时区是UTC,和国内相差8小时。解决方案是挂载宿主机时区文件:
docker run -d -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro my-app日志里中文乱码,多半是镜像环境变量没设UTF-8。可以在docker run时加:
-e LANG=C.UTF-8 -e LC_ALL=C.UTF-8或者在Dockerfile里设置ENV LANG=C.UTF-8。Java应用还要额外处理文件编码和字体,细节不少。
9.7 基于实际排查思路的速查表
| 现象 | 排查方向 | 常用命令 |
|---|---|---|
| 镜像拉不下来 | 网络、镜像源配置 | docker pull 测试小镜像 |
| 容器启动即退出 | 启动命令、报错日志 | docker logs 容器名 |
| 容器反复重启 | 资源限制、依赖未就绪 | docker inspect 查看OOMKilled |
| 端口访问不到 | 端口映射、防火墙 | docker port 容器名 |
| 数据一删就没 | 未挂载数据卷 | docker inspect 查看Mounts |
| 磁盘越来越满 | 镜像和日志堆积 | docker system df |
10. 从入门到上手的成长路线建议
Docker刚上手时,我建议大家按这个顺序练习,每一步都有明确目标:
先别急着写Dockerfile,用现成镜像跑几个服务,把docker run、ps、logs、exec这些基础操作练熟,理解端口映射和数据卷怎么用。
然后尝试把一个自己写的项目容器化,写好Dockerfile,构建镜像,用数据卷挂载代码目录做热更新调试。这个阶段你能理解镜像层、构建缓存、多阶段构建这些概念的意义。
接着用Docker Compose把项目+数据库+缓存组织起来,体会编排带来的便利。
最后关注镜像体积和构建速度优化,顺便了解Kubernetes等更上层的编排工具。但千万别跳过前面的基础步骤直接学Kubernetes,没有扎实的容器操作经验,遇到问题会寸步难行。
我自己在真实项目里最深的体会是,Docker的价值不在于某一个命令有多酷,而在于它把部署这件反复踩坑的事情变成了一个可复制的标准化流程。团队里任何人拉下来项目,docker compose up -d就能跑起来,这种体验是任何一个开发者都值得拥有的。
最后再分享一个小技巧:写Dockerfile和配Compose时,刻意养成写注释的习惯。Dockerfile虽然不是业务代码,但它决定了整个团队的开发体验和部署稳定性,值得像写生产代码一样认真对待。