Docker入门实战笔记:从镜像容器到数据持久化与网络配置 如果你刚开始接触 Docker多半会有这样一种体验教程看了不少名词背了一堆可真到自己搭环境的时候总觉得哪里没串起来。我最初折腾那阵就是这状态——服务在本机跑得好好的换台机器就起不来版本、依赖、系统库各掉各的链子。后来花了一周时间把镜像、容器、仓库、数据卷、网络这几块用实际命令一步步跑通才总算把 Docker 用顺了。这篇算是我自己的快速入门总结笔记偏实战、少废话。适合刚接触 Docker、想把核心概念一次搞清楚的同学也适合有一定基础但知识点比较零散、查过就忘的人当速查手册翻。我会先把最容易被搞混的底层逻辑讲明白再带你把第一个容器跑起来然后是写 Dockerfile、数据持久化、容器网络最后一节是我实际踩过的一堆坑建议直接收藏。1. 为什么镜像和容器总被搞混——先把最核心的模型讲清楚很多人第一次听 Docker 时最容易懵的就是镜像和容器到底啥关系。这里先说结论镜像是只读的模板容器是模板运行出来的实例。不理解这句话后面所有命令都是死记硬背换个场景就不会用了。1.1 镜像一份只读的“文件系统快照启动说明”镜像不是一台运行的机器它只是一堆静态文件的组合外加一些启动元数据。你可以把镜像理解成菜谱加半成品食材里面已经准备好了运行一个应用所需要的所有东西——基础操作系统文件、运行环境、依赖库、应用代码、默认配置。Docker 镜像还有一个很重要的特性分层存储。拉取一个镜像时你会发现它是一层一层下载的因为镜像是按层构建的。底层可能是一个精简的 Linux 文件系统上面叠加了运行环境的安装层再上面是依赖拷贝层最上面才是应用代码层。这样做有两个好处一是多个镜像可以共用底层节省磁盘空间二是再次拉取时如果本地已经有某一层这一层就不用重新下载。镜像的名字由“仓库地址/镜像名:标签”组成标签用于区分版本。比如nginx:1.25和nginx:latest是不同版本。如果不写标签默认取latest但生产环境我强烈建议锁定具体版本后面避坑清单还会提到。1.2 容器镜像运行后多出来的那一层“可写层”当你执行docker run时Docker 会在镜像之上叠一层可写层然后根据镜像里记录的启动命令把某个进程拉起来。这个“可写层 进程”的组合就是容器。平时改容器里的文件、写日志都是在可写层里进行的镜像本身永远不会变。所以容器删了之后你的修改也跟着没了但只要镜像还在随时可以再run一个全新的容器出来。这一点怎么强调都不过分很多人一开始没想明白后面才会被数据丢失吓一跳。容器本质上是宿主机上的一个被隔离的进程有自己的文件系统、网络空间和进程视图。进程结束了容器也就停了。比如跑一个hello-world镜像它打印完一段欢迎语就退出你会看到容器状态变成Exited这不是故障是它该干的事干完了。1.3 仓库镜像的“应用商店”镜像还有一个存放和分发的地方叫仓库。Docker 官方的公共仓库叫 Docker Hub里面可以 pull 到大量现成镜像比如 Nginx、MySQL、Redis。除了公共仓库还可以自己搭私有仓库团队内部经常这么干。平时跟仓库打交道的命令就两个docker pull是拉镜像到自己机器docker push是把自己构建的镜像推上去。刚开始学习只需要会pull就行。1.4 容器和虚拟机的关键区别这里补一个无数新手问过的问题容器和虚拟机到底差在哪虚拟机是在宿主机之上模拟了一整套硬件里面跑一个完整的操作系统容器则是直接共享宿主机的内核只做进程和文件系统层面的隔离。对比项虚拟机容器虚拟化层次硬件级虚拟化操作系统级隔离启动速度分钟级秒级镜像体积几个 GB 起步几十 MB 到几百 MB 常见资源占用高每台虚拟机都要独立分配低多个容器共享宿主机内核隔离强度更强相对弱但够用所以容器快、轻适合打包应用和微服务虚拟机更重但隔离更彻底适合多租户和强安全场景。两者不是替代关系实际基建里经常混着用。2. 新机器上从零跑通第一个容器——装好、验证、配置镜像源理论说太多容易困直接动手。2.1 安装与验证先确认两件事不同系统的安装方式差别很大我不展开写只给个通用思路Linux 上用系统自带的包管理器安装 Docker 引擎和命令行工具比如基于 apt 的发行版可以sudo apt install docker.io基于 yum/dnf 的发行版安装docker-ce相关包macOS 上直接装桌面版Windows 上建议先装好 WSL2再在 WSL 内部安装这样体验最顺。装完先别急着跑容器花十秒钟验证环境docker --version docker info第一条确认命令行工具装好了第二条确认 Docker 守护进程已经在跑。如果docker info报错大概率是服务没启动Linux 上可以试试sudo systemctl restart docker。2.2 把当前用户加进 docker 组省去每次 sudoLinux 用户很快会发现不带 sudo 跑docker会提示权限不足。原因是 Docker 守护进程默认以 root 运行它通过/var/run/docker.sock通讯。一个省心的办法是把当前用户加入docker组sudo usermod -aG docker $USER newgrp docker重新登录后再验证一次docker ps。这里必须提醒一句能操作 docker 的账号基本等于有了宿主机 root 权限因为容器可以挂载宿主机目录。所以个人电脑这么做没事公司的生产服务器上要慎重不能随便把开发者都加进 docker 组。2.3 拉镜像前处理一下镜像源问题官方镜像仓库在部分网络环境下的拉取速度很不理想甚至经常超时。推荐在开始学习前就配好镜像缓存仓库Docker 的术语叫 registry mirror意思是让 Docker 从你指定的镜像仓库地址拉取。编辑/etc/docker/daemon.jsonWindows 桌面版在设置界面填macOS 同理{ registry-mirrors: [https://mirror.example.com] }mirror.example.com换成你自己实际使用的镜像仓库地址通常在你使用的云服务商控制台里能找到一般叫“容器镜像服务”开通后会给你一个专属地址。改完重启 Dockersudo systemctl restart docker验证是否生效docker info | grep -A 2 Registry Mirrors能看到你配置的地址就说明生效了。不要乱填未经验证的地址宁可先不配也别让 Docker 连不上仓库。2.4 跑两个有代表性的容器第一个容器选最小的docker pull hello-world docker run hello-world会看到一串英文输出内容是 Docker 在教你“我的运行原理是这样的”。这个镜像只有几 KB适合验证整个链路能走通。它跑完就退出了因为容器里的进程本身只负责打印。第二个跑 Nginx这才像回事docker run -d --name web-demo -p 8080:80 nginx这里拆开解释一下-d后台运行不加的话终端会被绑住日志全程刷屏。--name web-demo给容器起个名字后续操作就不用记那一长串容器 ID。-p 8080:80端口映射宿主机 8080 端口转发到容器内 80 端口。等会curl的就是宿主机的 8080。nginx镜像名本地没有会自动 pull。跑完之后用docker ps看容器状态再打开浏览器访问http://localhost:8080看到 Nginx 欢迎页就说明通了你。因为容器里的 Nginx 进程是常驻的所以容器不会退出这正好和上面的 hello-world 形成对比。2.5 高频命令速查表新手最先要记住的就是下面这些不用背用多了自然就熟了命令作用常用配合参数docker ps查看正在运行的容器-a看所有含已退出的docker pull 镜像拉取镜像可加:tag指定版本docker run 镜像创建并启动容器-d后台、-p映射端口、--name命名docker stop 容器停止容器容器名或前几位 ID 都行docker start 容器启动已停止的容器重启容器不丢文件系统docker rm 容器删除容器删除后容器内可写层丢失docker rmi 镜像删除镜像有容器正在使用时会失败docker exec -it 容器 bash进入容器命令行-it是交互式加伪终端docker logs -f 容器跟随查看日志-f类似tail -fdocker system prune清理悬空资源-a会清理所有未使用镜像docker exec是调试神器容器里面出了任何问题先进去手动跑一遍命令看报错比在外面猜快得多。3. 自己做一个能访问的镜像——Dockerfile 每条指令为什么这么写用别人现成的镜像只是开始真正让 Docker 发挥价值的是把自己的应用打包成镜像。这一步的核心就是写 Dockerfile。3.1 一个完整可跑的最小示例我从一个简单的 Python Web 应用开始讲。项目目录里先放两个文件。requirements.txtflask3.0.0app.pyfrom flask import Flask app Flask(__name__) app.route(/) def index(): return Hello from Docker if __name__ __main__: app.run(host0.0.0.0, port8000)然后写 DockerfileFROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . ENV PYTHONUNBUFFERED1 EXPOSE 8000 CMD [python, app.py]构建并运行docker build -t my-flask-app . docker run -d -p 8000:8000 --name flask-demo my-flask-app curl http://localhost:8000能输出Hello from Docker说明整个流程通了。注意app.run里写的host0.0.0.0不能省否则 Flask 默认只监听容器内的 localhost宿主机通过映射端口访问时会失败。3.2 高频指令背后的逻辑Dockerfile 看着像一串命令其实每一行都有讲究。FROM python:3.12-slim指定基础镜像。选slim版本是为了体积可控它去掉了很多用不到的编译工具和文档。如果你是新手优先选官方镜像的slim变体别一上来就追求基于 Alpine 的极致瘦身Alpine 用的 C 库和常见发行版不一样有些依赖装起来会遇到兼容问题。WORKDIR /app设置工作目录。很多人习惯用RUN cd /app这是不对的因为 Dockerfile 里每条RUN都是独立 shellcd只对当前这条指令生效。WORKDIR是声明式的后续指令都会在这个目录下执行。COPY requirements.txt .优先拷贝依赖清单而不是直接COPY . .。这是利用 Docker 的构建缓存Docker 构建镜像是一层一层来的如果某一层没有变化后续会复用缓存。依赖文件不常变把它单独放在前面那么以后改业务代码时pip install这一层可以直接走缓存构建会快很多。RUN pip install --no-cache-dir -r requirements.txt负责装依赖。RUN每执行一条就会生成一个新层所以能合并的尽量合并但也不要为了减少层数把一堆命令硬塞进一行可读性同样重要。ENV PYTHONUNBUFFERED1设置环境变量。Python 应用经常遇到日志不输出的问题后面避坑清单会详细讲。EXPOSE 8000只是声明“我这个容器里的服务用 8000 端口”它不会自动映射到宿主机。真正做映射的是docker run -p 8000:8000。很多新手以为写了 EXPOSE 就能访问错就错在这。CMD [python, app.py]是容器启动后执行的命令。这里用的 JSON 数组格式叫 exec 形式会让python直接作为容器内的 1 号进程如果写成CMD python app.py这种 shell 形式实际会多套一层 shell后面处理停止信号时就容易出问题。3.3 CMD 和 ENTRYPOINT 怎么选这两个指令长得像新手经常混。简单说CMD是启动命令加默认参数可以被docker run后面的命令覆盖ENTRYPOINT是固定入口不容易被覆盖配合CMD时CMD的内容会作为参数传给ENTRYPOINT。写法效果适合场景CMD [python, app.py]启动时执行 python app.py可被 docker run 后的命令替换启动命令就是全部ENTRYPOINT [python]CMD [app.py]启动时执行 python app.pydocker run 后接参数会传给 python需要固定解释器/入口参数灵活ENTRYPOINT [python, app.py]完全固定docker run 后接的命令只是额外参数单入口应用不太需要覆盖大部分入门场景用CMD就够了等你想做“容器启动后可以传不同参数执行不同任务”时再研究ENTRYPOINT。3.4 别忽略 .dockerignore 和构建上下文执行docker build时Docker 会把当前目录作为一个“构建上下文”打包发送给守护进程。如果你的目录里有个几百 MB 的依赖目录构建过程会又慢又占空间如果目录里有.env、密钥文件还可能被一起拷进镜像造成泄露。所以每个项目都要建一个.dockerignore.git node_modules __pycache__ *.pyc .env dist tmp这个文件的作用和.gitignore类似告诉 Docker 在构建上下文中排除哪些路径。养成这个习惯等于从源头避免了镜像臃肿和密钥泄露两个问题。4. 容器数据为什么会丢——三种持久化方式的对比与数据库实操很多人的第一个“事故”是这样的用 Docker 跑了个带数据库的服务过两天docker rm把容器删了数据全没了。原因前面提过——容器删除时那个可写层跟着一起销毁。所以凡是需要保存的数据必须放到容器之外。4.1 先把三种持久化方式分清Docker 提供了三种挂载数据的方式bind mount、volume、tmpfs。傻傻分不清的话看下面这张表就够了。方式数据存在哪适合场景注意点bind mount宿主机指定目录开发调试、实时改代码路径写错会报错受宿主机权限影响volumeDocker 管理目录数据库、生产数据推荐docker volume系列命令可管理tmpfs宿主机内存临时缓存、敏感信息容器停止后数据消失bind mount 的写法是把宿主机路径和容器路径拼在一起比如docker run -d -p 8000:8000 \ -v $(pwd)/app:/app \ my-flask-app这样宿主机当前目录下的app会直接挂进容器的/app改代码不用重新构建镜像开发阶段非常方便。但它太“直白”生产环境更推荐 volume。volume 的玩法和命名磁盘类似创建一次就能复用docker volume create app-data docker run -d \ --mount sourceapp-data,target/data \ nginx--mount比-v的语义更清晰参数名也更好记。强烈建议新手直接习惯用--mount不要死记-v冒号语法。4.2 用 MySQL 容器演示一次完整持久化我用数据库举例因为数据库是最需要持久化的场景。先创建 volume再启动带 volume 的 MySQLdocker volume create mysql-data docker run -d --name mysql-db \ -e MYSQL_ROOT_PASSWORDyour-password \ -e MYSQL_DATABASEappdb \ --mount sourcemysql-data,target/var/lib/mysql \ -p 3306:3306 \ mysql:8.0解释两个点-e是向容器传入环境变量MySQL 官方镜像第一次启动时会读取这些变量来初始化 root 密码和默认数据库--mount sourcemysql-data,target/var/lib/mysql是把之前创建的 volume 挂到 MySQL 存放数据文件的目录。注意如果宿主机上已经装了 MySQL 并占用了 3306这里的-p 3306:3306会冲突改成-p 33060:3306就行。等容器启动完成后进去建一张表docker exec -it mysql-db mysql -uroot -p输入刚才设置的密码执行USE appdb; CREATE TABLE demo (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50)); INSERT INTO demo (name) VALUES (hello);然后退出删掉这个容器再用同样的参数重新启动一个新容器挂载同一个 volumedocker stop mysql-db docker rm mysql-db docker run -d --name mysql-db \ -e MYSQL_ROOT_PASSWORDyour-password \ -e MYSQL_DATABASEappdb \ --mount sourcemysql-data,target/var/lib/mysql \ -p 3306:3306 \ mysql:8.0进入容器查一下docker exec -it mysql-db mysql -uroot -p -e USE appdb; SELECT * FROM demo;能看到刚才的hello还在。这就是 volume 的价值卷在数据就在。4.3 顺手给个备份命令数据库容器最怕手滑删除备份其实一条命令就能做。利用--volumes-from挂载原容器的 volume再用一个小镜像把数据目录打包到当前目录docker run --rm \ --volumes-from mysql-db \ -v $(pwd):/backup \ alpine \ tar czf /backup/mysql-data.tar.gz /var/lib/mysql跑完当前目录下会多出mysql-data.tar.gz。以后想恢复就反向解压回原目录。--rm表示这个辅助容器用完就删不会留下垃圾进程。5. 容器之间怎么通信——端口映射、自定义网络与排查顺序跑起来了、数据也不丢了接下来你大概率会遇到另一个问题几个容器之间怎么互相访问尤其是有数据库和应用总不能每次都用宿主机 IP 去连。5.1 为什么容器默认“与世隔绝”每个容器默认有自己的网络命名空间相当于一个独立小网络。外部访问不到容器里的端口容器访问外部也要靠 NAT 转发。所以docker run nginx之后你在宿主机浏览器里是访问不到的必须加-p端口映射宿主机那边开一个口把流量转发进容器。-p的完整格式是-p 宿主机IP:宿主机端口:容器端口。最常用的是只写端口不写 IPdocker run -d -p 8080:80 nginx意思是宿主机所有网卡上的 8080 端口转发到容器内 80 端口。如果你是本地开发建议把访问范围限制在本地回环地址docker run -d -p 127.0.0.1:8080:80 nginx这样只有本机能访问局域网里其他机器访问不到安全一点。如果你想偷懒可以写-P大写Docker 会自动把容器里声明过的端口随机映射到宿主机的某个端口然后用docker ps去查实际映射结果。这种方式适合临时验证不太适合正式使用。5.2 端口冲突怎么查报错port is already allocated是最常见的。先看有哪些容器在跑哪个占用了端口docker ps找到占用的容器后要么改新容器的宿主机端口要么停掉旧容器。也可以用系统的端口查看命令确认谁在监听ss -tlnp | grep 8080我的建议是本地学习时宿主机端口统一用“容器端口前面加个数字”的方式比如 MySQL 3306 映射到 33060Nginx 80 映射到 8080Redis 6379 映射到 16379。这样一眼就能看出哪个容器对应哪个端口不容易冲突。5.3 自定义网络让容器用名字互访容器之间的 IP 不是固定的删掉重建就可能变所以不能写死 IP。解决办法是建立一个自定义网络让 Docker 的 DNS 自动解析容器名。先建网络docker network create app-net再把两个容器都加进去docker run -d --name mysql-db \ --network app-net \ -e MYSQL_ROOT_PASSWORDyour-password \ mysql:8.0 docker run -d --name backend \ --network app-net \ -p 8000:8000 \ my-flask-app这时在backend容器里可以直接用mysql-db这个名字访问数据库不需要知道它的 IPdocker exec -it backend ping mysql-db应用代码里配置数据库连接地址时直接写mysql-db:3306就行。这个便利只对自定义网络生效默认的bridge网络不支持容器名自动解析所以正规做法还是建一个网络而不是所有容器都丢在默认网络里。Docker 网络有三种常见模式bridge是默认的隔离网络适合单机多容器通信host让容器直接共享宿主机网络栈性能好但隔离弱而且-p会被忽略none表示无网络。初学阶段把自定义bridge用熟就够了其他模式等遇到具体需求再研究。5.4 端口通了但访问失败按这个顺序排查-p写对了浏览器还是打不开我教大家一套排查顺序别一上来就删容器重建。容器是否还在运行docker ps确认状态不是 Exited。端口是否真的映射了docker port 容器名看宿主机端口和容器端口对应关系。容器内服务是否在监听正确端口docker exec -it 容器名 bash进去用netstat -tlnp看监听地址。很多应用默认监听 127.0.0.1容器外部就访问不到必须监听0.0.0.0。宿主机防火墙是否拦截如果你在云服务器上跑还要去安全组放行对应端口。这一套流程走完80% 的访问问题都能定位。剩下的就是看应用日志比如是不是启动时连数据库失败了。6. 新手最容易踩的坑——我提前帮你踩了最后这部分算是我个人总结的高频问题清单每一个都有对应的解决思路建议直接收藏。6.1 容器时间永远不对日志差八小时新手第一个困惑往往是容器里date显示 UTC 时间日志和宿主机的对不上。原因很简单很多基础镜像默认使用 UTC 时区。解决方案有两种。第一种运行容器时挂载宿主机的时区文件docker run -d \ -v /etc/localtime:/etc/localtime:ro \ nginx第二种通过环境变量指定时区具体时区标识请按你自己所在位置设置docker run -d \ -e TZ你的时区标识 \ nginx如果你写的是自己的镜像建议在 Dockerfile 里也补上时区设置这样不管用的人有没有加-e默认都是对的时间。6.2 容器明明是活的但 docker logs 什么都没有Docker 的日志机制只收集进程输出到 stdout 和 stderr 的内容。很多应用默认把日志写到文件里于是你在docker logs里什么都看不到。解决办法是在应用层面开启标准输出。比如 Python 可以设置PYTHONUNBUFFERED1Java 可以关掉日志文件只留控制台Nginx 官方镜像本来就做了日志到 stdout 的软链。自己写镜像时也要注意把日志重定向到/dev/stdout或/dev/stderr。排查这类问题时先docker exec -it 容器 bash进去手动跑一下应用能看到报错就说明应用本身没问题只是输出位置不对。6.3 挂载的目录里全是 root 归属的文件宿主机用户改不动一个常见的现象是容器往挂载目录里写文件生成的用户是 root宿主机普通用户打不开也删不掉。原因是容器内进程默认以 root 运行而挂载是直通宿主机文件系统的所以写的文件归属会变成 root。最简单的临时方案是在运行容器时指定当前用户docker run -d \ --user $(id -u):$(id -g) \ -v $(pwd)/data:/data \ some-image更好的方案是在 Dockerfile 里创建一个专用用户用USER切换到它再启动应用。这不仅能解决权限问题还是一手安全习惯生产容器里不要用 root 跑应用进程万一被攻破影响面小很多。6.4 docker stop 总是要等十几秒像卡住了一样docker stop的正常流程是给容器的 1 号进程发 SIGTERM等它优雅退出倒计时十秒结束还没退就发 SIGKILL 强杀。如果你看到每次停止都要卡一二十秒多半是容器里的进程没有处理 SIGTERM。最典型的情况是 Dockerfile 里写成了 shell 形式CMD python app.py这会让 sh 作为 1 号进程而真正的 python 是 sh 的子进程信号被 sh 接住却没有转发给 python结果就是应用没法优雅退出只能等强杀。解决方法是改成 exec 形式CMD [python, app.py]或者直接在运行参数里加--init让 Docker 内置一个初始化进程来转发信号docker run --init --name flask-demo my-flask-app6.5 磁盘越来越小全是没人用的悬空镜像Docker 用久了本地会堆积大量无标签镜像、停止的容器和构建缓存。先用这个命令看看到底是谁占的空间docker system df然后定期清理docker system prune如果加了-a会把所有未被容器使用的镜像也一并清掉本地空间的回收效果更明显。但注意线上机器执行prune -a要小心本地一时没用到的镜像可能被误删重新拉一次又是一晚上工作量。6.6 几个提升幸福感的小习惯最后说几个零散但很管用的经验。第一不要用latest标签做长期固定依赖写死了版本号换台机器或者几个月后重新构建行为才可预期。第二密钥和密码不要写进 Dockerfile运行时用-e或专门的 secrets 机制注入。第三改完代码记得重新docker build很多人 run 了旧镜像半天查不出问题最后才发现是新代码根本没进容器。第四日志里出现奇怪的^M字符多半是脚本文件用了 Windows 换行符拉进 Linux 容器执行时就会报错写干净的文件编码能从根上避免。聊到这里基本的单机 Docker 操作应该已经够你用了。我自己学习时最大的体会是先别急着碰编排工具和复杂架构老老实实把docker run、docker exec、docker logs这些命令行玩熟把镜像构建和数据持久化的手感练出来后面再看编排配置就会觉得很轻松。另一个习惯是遇到任何异常先docker logs再看文档别一上来就docker rm -f删容器永远不是排查问题的第一步。希望这份笔记能帮你少踩我当初踩过的那些坑剩下的就靠你多跑几次命令了。