☰
Docker新手入门:容器化部署从跑通到可用,一篇讲透镜像与容器
2026/10/12 14:22:40 网站建设 项目流程

简介:这是一份面向 Docker 新手与运维初学者的容器化技术入门 PDF 文档,从零讲解镜像、容器、仓库等核心概念,并对比传统虚拟机在启动速度、资源占用、隔离性等方面的差异,帮助读者快速建立容器化认知。资源共 1 个 PDF 文件,大小约 760KB,内容紧凑、便于随时查阅。文档以 Ubuntu 为例演示 Docker Engine 安装与用户组配置,系统梳理镜像拉取、容器生命周期、日志调试等常用命令,并通过 docker-compose.yml 部署 WordPress 的完整案例,串联多容器编排与端口映射流程。数据管理部分涵盖数据卷与绑定挂载两种持久化方案,安全章节涉及容器运行原则与关键配置,适合实践导向学习。目前已有 1057 人学习下载,尤其适合希望从零上手 Docker、想通过实战项目巩固技能的开发与运维人员。

1. Docker 到底是什么:先跑起来一个容器再说

Docker 新手入门最容易被"容器化"三个字劝退,但实际它解决的是一个很朴素的问题:你的代码在你自己电脑上跑得好好的,换台机器就挂。环境不一致、依赖冲突、部署步骤繁琐,这三件事几乎每个开发者都遇到过。Docker 把应用连同它的运行环境一起打包成镜像,用镜像创建容器来运行,"在我机器上能跑"就变成了"在任何装 Docker 的机器上都能跑"。

这篇文章按"安装 → 跑第一个容器 → 写 Dockerfile 构建镜像 → 数据持久化 → 踩坑 → 进阶"这条路径走,所有命令都可以直接复制到终端验证,不需要先读一堆理论。适合刚接触 Docker 的前端、后端、运维从业者,也适合想把手动部署流程改成容器化流程的团队。读完你不仅能跑通一个容器,还能自己写出可用的 Dockerfile,并知道哪些坑是大概率会踩的。

2. 安装与第一个容器:把 Docker 跑在本地的最小路径

2.1 选对安装方式:不同系统的 Docker 环境差异

先说明一个反复出现的问题:很多新手在安装环节就卡住了。原因不是你操作有问题,而是 Docker 在不同系统上的安装方式差异很大,用错了方式后面全是坑。

Linux 上我一般用系统包管理器装。Debian/Ubuntu 系是apt install docker.io,RHEL/CentOS 系是dnf install docker-ce。装完要把当前用户加进 docker 组,否则每条命令都要 sudo,非常影响体验。macOS 和 Windows 桌面版的 Docker 本质上是跑在一个轻量虚拟机里的 Docker 引擎,安装包一路下一步就行,但需要系统开启虚拟化支持,Windows 10 以上一般用 WSL2 后端,稳定性比旧版 Hyper-V 方案好不少。

# Ubuntu/Debian 系的参考做法 sudo apt update sudo apt install -y docker.io # 把当前用户加入 docker 组,避免每次敲 sudo sudo usermod -aG docker $USER # 重新登录终端后验证客户端与引擎 docker --version docker info | grep -i "server version"

第二行usermod的作用是把当前用户加入 docker 用户组。Docker 守护进程以 root 权限运行,加入 docker 组等于给了这个用户调用 Docker 的完整权限,所以这行命令适合本机开发环境,生产服务器上不要图省事这样配。docker info里的 Server Version 字段如果正常显示,说明引擎在跑;如果只显示 Client 版本,大概率是守护进程没启动,先查systemctl status docker。

安装完成后建议跑一下官方最小的验证镜像:

docker run --rm hello-world

--rm表示容器退出后自动删除,适合这种一次性验证。能看到一段欢迎文字说明拉取镜像、创建容器、运行容器这条链路已经打通了。这一小步能帮你把"安装问题"和"后面的使用问题"隔离开,后面出了问题不用再怀疑是 Docker 本身没装好。

2.2 用 docker run 跑起第一个 Nginx:命令拆解

安装验证通过后,第一个正式容器建议用 Nginx 跑,它启动快、默认就有页面、报错直观。不要一上来就跑自己写的应用,先把"拉镜像→建容器→访问服务"这条链路走通,后面排查自己应用的问题时才有参照系。

# 拉取 Nginx 镜像并启动一个容器 docker run -d --name my-nginx -p 8080:80 nginx:alpine # 确认容器状态 docker ps # 访问容器里的 Nginx 默认页面 curl http://localhost:8080

docker run这行拆开看:-d表示后台运行,不加的话终端会被容器日志占住;--name my-nginx给容器起个名字,后面所有操作都用这个名字而不是容器 ID;-p 8080:80把宿主机 8080 端口映射到容器内的 80 端口,冒号左边是宿主机、右边是容器;nginx:alpine是镜像名,alpine 标签表示基于 Alpine Linux 的瘦身版本,体积比默认标签小很多。

docker ps输出里如果 STATUS 是 Up,说明容器在运行。curl能返回 HTML 说明端口映射通了。新手最常困惑的是:容器内 Nginx 监听的是 80,为什么访问要带 8080?因为-p把宿主机的 8080 转发到了容器的 80,你在宿主机访问localhost:8080等于访问容器里的localhost:80。

想看容器里发生了什么,用日志和交互式终端两个命令:

# 查看容器日志,-f 是持续跟随输出 docker logs -f my-nginx # 进入容器的 shell,方便查看容器内部文件结构和进程 docker exec -it my-nginx sh

docker logs -f和tail -f行为类似,排查问题时先看日志是最快的定位手段。docker exec -it中的-i表示交互、-t分配伪终端,两个参数要一起用,进去之后可以ls /usr/share/nginx/html看看页面文件在哪。用exit退出容器,不影响容器运行。

2.3 镜像、容器、仓库:新手最容易搞混的三个概念

新手经常把镜像和容器当成一个东西,这里用一句话拆开:镜像是只读的模板,容器是镜像运行后的实例。你可以把镜像理解成安装包,容器理解成安装包装好之后正在运行的程序。同一个镜像可以启动多个容器,每个容器之间互相隔离,改一个容器里的文件不影响其他容器,也不影响镜像本身。

仓库是存放镜像的地方,最常用的是 Docker Hub。docker pull nginx:alpine就是从仓库把镜像拉到本地。镜像的命名规则是"仓库地址/命名空间/镜像名:标签",仓库地址没写时默认走 Docker Hub,标签不写时默认 latest。这个命名规则后面拉公司内部镜像时会用到,提前记住能少踩一次坑。

对比项镜像容器
性质只读模板镜像运行后的实例
生命周期长期存在可创建、停止、删除
数据构建时固化运行时可写
标识IMAGE IDCONTAINER ID
# 查看本地已有的镜像 docker images # 查看所有容器,包括已停止的 docker ps -a # 删除一个不再使用的容器(先停止再删) docker stop my-nginx docker rm my-nginx

docker ps -a这个命令特别值得记。docker ps默认只显示运行中的容器,但新手经常遇到"容器跑了一下就退出"的情况,此时docker ps什么都看不到,误以为容器不存在。加上-a就能看到所有容器以及它们的退出码和退出时间,这是排查"容器秒退"问题的第一步。

另外,docker images列出的 IMAGE ID 和运行中的容器 CONTAINER ID 不是一回事,一个是镜像的标识,一个是容器实例的标识。后面排查问题时,别拿镜像 ID 去docker rm,会直接报错。容器删掉之后镜像还在,可以随时用同一个镜像再起一个新容器,这也是 Docker 的工作流里"容器随便删、镜像要小心管理"的原因。

3. 用 Dockerfile 构建自己的镜像:从零写到可用的完整过程

3.1 Dockerfile 核心指令:FROM、RUN、COPY、CMD 的用法

跑现成镜像只是第一步,Docker 真正有价值的地方是把自家应用打包成镜像。这需要写一个 Dockerfile,它本质上是一份构建脚本,Docker 按里面的指令逐行执行,最终产出一个镜像。

一个最简单的 Python Web 应用的 Dockerfile 长这样:

# 基础镜像:Python 3.11 的 slim 变体 FROM python:3.11-slim # 设置工作目录,后续命令都在这个目录下执行 WORKDIR /app # 先复制依赖清单并安装依赖,充分利用构建缓存 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 再复制项目代码 COPY . . # 声明容器运行时监听的端口 EXPOSE 8000 # 容器启动时执行的命令 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

每条指令都有讲究。FROM是必选项,指定基础镜像,选 slim 或 alpine 变体可以显著减小体积;WORKDIR设置工作目录,避免后面到处写绝对路径。两个COPY的顺序不是随手写的,第一个只复制 requirements.txt 是为了单独走依赖安装这一步,这样改代码时不会触发重新安装依赖,后面 3.3 会展开讲。EXPOSE只是声明端口,真正暴露端口还得靠运行时的docker run -p。CMD用 JSON 数组写法,不经过 shell 解析,避免特殊字符被错误处理。

新手最容易把RUN和CMD搞混。记住一个区分:RUN是构建镜像时执行的,比如装依赖、编译代码,它的结果会写进镜像层;CMD是容器启动时执行的,定义的是默认运行命令。还有环境变量相关的两个指令:ENV设置运行时的环境变量,ARG只在构建时有效,比如ARG VERSION=0.1可以在docker build --build-arg VERSION=0.2时覆盖。

3.2 构建并验证:docker build 与 docker run 的完整闭环

写好了 Dockerfile,在项目根目录执行构建:

# 构建镜像,-t 指定名字和标签,最后的 . 是构建上下文路径 docker build -t my-app:0.1 . # 查看构建出的镜像 docker images | grep my-app # 启动容器并验证服务 docker run -d --name my-app-test -p 8000:8000 my-app:0.1 curl http://localhost:8000

-t my-app:0.1里的 0.1 是标签,建议每次构建都用递增的版本号,不要一直用 latest,否则回滚时想不起哪个镜像是能跑的版本。最后的点号表示构建上下文是当前目录,构建时 Docker 会把当前目录的文件发给守护进程,所以项目里要放.dockerignore,把node_modules、.git、__pycache__这类不必要的大目录排除掉,否则构建会非常慢,甚至把敏感的配置文件打进镜像。

验证这一步,curl能返回业务页面就说明链路完整。如果返回连接拒绝,先别急着改代码,用下面的顺序看问题出在哪一层:

# 看容器是否在运行、退出码是多少 docker ps -a | grep my-app-test # 看应用日志 docker logs my-app-test # 看容器状态细节 docker inspect my-app-test --format '{{.State.Status}} {{.State.ExitCode}}'

这个排查顺序能区分三种情况:容器没起来是启动命令问题;起来又退出是应用崩溃;一直在运行但访问不通是网络或监听地址问题。日志里通常能直接看到 Python 的 traceback 或端口被占用的提示。

3.3 镜像分层与构建缓存:为什么改一行代码重建很快

Dockerfile 里的每条指令都会生成一个只读的镜像层,层与层之间有缓存。构建时如果某条指令没有变化,Docker 会直接复用之前的层,不重新执行。这正是 3.1 里先把 requirements.txt 单独复制的原因:你只改业务代码时,pip install那一步的缓存还能命中,构建只重新执行COPY . .之后的步骤,速度会快很多。

# 修改代码后重新构建,观察构建输出 docker build -t my-app:0.2 . # 输出里出现 CACHED 的层就是命中了缓存 # 如果发现代码改完镜像行为没变,重点检查这里

构建日志里每一行开头如果是CACHED,说明这层没有重新执行。新手经常遇到一个诡异现象:改了代码,重新 build 之后容器里跑的还是旧代码。原因多半是COPY . .这层被错误复用,常见诱因有三个:一是.dockerignore把代码目录误排除掉了;二是复制路径写错,Docker 认为源文件没变化;三是构建标签没有更新,启动容器时用的还是旧镜像。

镜像分层机制还带来一个关键认知:不要在 Dockerfile 里写"删除中间文件"的步骤。比如RUN apt-get install xxx && rm -rf /var/lib/apt/lists/*,这样写是有效的,因为它把安装和清理放在同一层;但如果你分两条 RUN 写,前一层装完的文件仍然占据体积,后面的删除并不会让镜像变小。要控制体积,正确做法是把相关操作合并到同一个 RUN 里,或者用后面章节会讲的多阶段构建。

4. 数据卷与端口映射:让容器和外面的世界安全通信

4.1 端口映射:-p 参数的细节与常见误区

容器默认是隔离的,外部无法直接访问容器里的端口。-p参数就是在宿主机和容器之间打通一条通道。前面用的-p 8080:80是最常见的写法,实际还有几个变体值得知道。

# 只指定容器端口,宿主机端口随机分配 docker run -d -p 80 nginx:alpine # 绑定到指定 IP,避免暴露到所有网卡 docker run -d -p 127.0.0.1:8080:80 nginx:alpine # 一个容器同时暴露多个端口 docker run -d -p 8080:80 -p 8443:443 my-web:0.1 # 查看端口映射关系 docker port my-web

随机分配端口适合临时调试,但不适合固定访问场景;绑定127.0.0.1适合只想本机访问的场景,比如数据库管理面板,不应该暴露到局域网。docker port会列出容器所有端口映射,排查"服务在跑但访问不了"时先跑这个命令确认映射是否真的生效。

还有一个经常踩的坑:容器内应用监听的地址要写 0.0.0.0,不能只写 127.0.0.1。很多应用默认监听 localhost,容器里的 localhost 是容器自己,宿主机通过映射端口访问会直接连接失败。前面 Dockerfile 示例里 uvicorn 用--host 0.0.0.0就是这个原因,改成 127.0.0.1 后从宿主机就访问不到了,这不是 Docker 的 bug,而是监听地址的问题。

4.2 数据卷与 bind mount:容器删除后数据不丢的正确姿势

容器是临时性的,删除容器后容器内写的文件会跟着消失。数据库、上传文件这类必须持久化的数据,要放到宿主机上。Docker 提供两种方式:数据卷(volume)和绑定挂载(bind mount)。

对比项数据卷绑定挂载
管理方式Docker 管理,位置透明直接使用宿主机目录
适合场景数据库、生产环境开发调试、配置文件覆盖
备份迁移需要通过容器操作直接操作宿主机目录
权限控制Docker 分配,相对规范完全继承宿主机权限
# 创建数据卷并挂载到容器内的 /var/lib/mysql docker volume create app-data docker run -d --name my-db \ -v app-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=devpass \ mysql:8 # 绑定挂载:把宿主机的目录直接映射进容器 docker run -d --name my-web \ -v $(pwd)/html:/usr/share/nginx/html \ nginx:alpine

-v的格式是"宿主机路径或卷名:容器内路径"。左边是卷名时用 Docker 管理的数据卷,实际存放位置由 Docker 决定,但你不关心它在哪,删了容器卷还在,下次用同一个卷名挂载数据就回来了。左边是绝对路径或$(pwd)/html时是绑定挂载,宿主机上改文件容器里立即生效,开发场景特别好用。

绑定挂载有一个权限坑:容器内进程以什么用户运行,挂载目录的权限就要匹配。MySQL 镜像默认以 mysql 用户运行,挂载的宿主机目录如果权限过严或过松,容器可能直接拒绝启动,日志里能看到权限相关的报错。解决办法是让宿主机目录的属主和容器内用户 UID 一致,或者在挂载后手动chown修正权限。

4.3 网络模式选型:bridge、host、none 各在什么场景用

Docker 默认的 bridge 网络会给每个容器分配独立的 IP,容器之间通过 IP 或自定义网络别名通信。--network host让容器直接使用宿主机的网络栈,没有端口映射这层转化,性能好,但容器间端口容易冲突。none模式只有回环接口,用于需要完全隔离的场景。三个模式里,开发和生产用得最多的是自定义 bridge 网络。

# 创建一个自定义网络 docker network create my-net # 两个容器接入同一个网络 docker run -d --name app-a --network my-net my-app:0.1 docker run -d --name app-b --network my-net -p 8080:80 nginx:alpine # 在 app-b 容器里用服务名访问 app-a docker exec app-b curl http://app-a:8000

自定义网络里容器可以直接用--name指定的名字互相访问,Docker 内置了 DNS 解析。这比用容器 IP 通信靠谱得多,因为容器重建后 IP 会变,服务名不变。新手经常手动查容器 IP 再写进配置,容器一删重建配置就失效,改成服务名才是多容器协作的正确姿势。

多容器项目还有一个更省事的做法,就是用 Docker Compose 管理。Compose 文件里 network 配置会自动处理,服务名就是容器名,比手敲一连串docker run可维护得多。但 Compose 的底层逻辑还是这几种网络模式,先把这里搞透再用 Compose,出问题时不至于一头雾水。

5. Docker 新手最常见的 5 个翻车现场:现象、原因与解决

5.1 镜像拉取失败或超时

现象:执行docker pull时连接超时,或者进度条长时间卡住不动。

原因:默认仓库的访问链路不稳定,也可能系统代理环境变量配置错误,导致 Docker 客户端连接仓库失败。

解决:配置镜像加速器是最常见做法,在 Docker 守护进程配置里添加 registry-mirrors 后重启。公司内部有镜像仓库的话,也可以直接拉取内部地址。

# 编辑 Docker 守护进程配置 sudo vim /etc/docker/daemon.json # 配置内容示例 { "registry-mirrors": ["https://你的加速器地址"] } # 重启 Docker 使配置生效 sudo systemctl restart docker

配置后可以用docker info查看 Registry Mirrors 是否生效。如果配置了还是慢,检查终端里的 HTTP_PROXY 和 HTTPS_PROXY 环境变量,代理地址不可达时会表现成"网络不通",而且这种问题只影响拉镜像不影响本地操作,很容易被忽略。

5.2 容器启动后立刻退出

现象:docker run执行后容器马上退出,docker ps看不到,docker ps -a里 STATUS 是 Exited。

原因:容器里没有长期运行的前台进程。Docker 的设计是启动一个进程,进程退出容器就退出。很多新手在容器里跑一个脚本,脚本执行完,容器就结束了,这不是故障,是机制。

解决:确保容器里有前台进程。Web 服务本身监听端口就属于前台进程;工具类任务用tail -f /dev/null保持存活。

# 让容器保持运行,方便进去调试 docker run -d --name debug-box \ -v $(pwd):/app \ ubuntu:22.04 \ tail -f /dev/null # 进入容器交互式调试 docker exec -it debug-box bash

如果用的镜像是 ubuntu 这类基础镜像,直接docker run ubuntu会秒退,因为默认命令bash在没有终端的情况下直接结束。挂一个tail -f /dev/null是常见的保持存活的土办法,适合临时调试;正式服务不应该这么做,而是要确保应用进程在前台运行。排查时先用docker logs看退出前输出,再用docker inspect看 ExitCode,非零退出码通常能对应到应用自身的报错。

5.3 端口被占用导致容器起不来

现象:容器启动报错,提示port is already allocated或bind: address already in use。

原因:宿主机端口被其他容器或宿主机进程占用。

解决:先查占用情况,再决定换端口还是清掉占用进程。

# 查看哪个进程占用 8080 端口 sudo lsof -i :8080 # 或者用 ss 命令 ss -ltnp | grep 8080 # 查看所有容器占用的端口 docker ps --format "table {{.Names}}\t{{.Ports}}"

注意两种报错的区别:port is already allocated是 Docker 层面的冲突,常有另一个容器占着端口,用docker ps就能看到;bind: address already in use是宿主机进程占用,比如本地开发服务器没关。杀进程前确认是不是你自己启动的服务,避免影响同机协作的其他成员。

5.4 容器删了数据就没了:忘记挂载数据卷

现象:容器运行期间写入的文件还在,docker rm删除容器后文件全部消失,数据无法找回。

原因:容器写入的文件在容器自己的可写层里,容器删除,可写层一并清理。没有挂载宿主机目录或数据卷的数据,生命周期和容器绑定。

解决:对需要持久化的目录提前规划挂载。数据库数据目录、应用上传目录、日志目录都要通过-v挂载出来。这个规划要在启动容器之前做好,容器跑起来之后可以再加卷,但那需要重建容器,已经产生的数据在重建前要先拷贝出来。

# 挂载时指定多个目录 docker run -d \ --name my-app \ -v app-data:/var/lib/mysql \ -v $(pwd)/uploads:/app/uploads \ my-app:0.1

这个坑一句话归纳:容器里能写,不代表数据安全。数据卷是容器化应用里的持久化底座,所有有状态的应用都要先想清楚"哪些目录的数据删了不能丢",把这些目录提前挂载出去。没有任何工具能帮你找回已删除容器的未挂载数据,后悔药不存在,只能靠习惯。

5.5 构建缓存导致代码更新不生效

现象:改了代码,重新docker build后启动容器,应用还是旧版本。

原因:COPY . .前面某层缓存命中,新代码没有被复制进镜像;或构建标签没更新,启动容器时用的还是旧镜像。

解决:先看构建日志里是否有CACHED层。如果代码变更没有触发 COPY 层重建,检查.dockerignore是否把项目代码目录误排除,或 COPY 的源路径写得不精确。定位问题可以临时强制无缓存构建:

# 强制不缓存重新构建一次 docker build --no-cache -t my-app:0.3 . # 确认新镜像的创建时间 docker images | grep my-app

--no-cache只是定位问题的手段,不要作为每次构建的默认参数,否则镜像层的缓存优势就浪费了。确认问题在缓存后,调整 Dockerfile 的指令顺序或.dockerignore配置,让代码所在的层能够被稳定重建。另外养成每次构建递增标签的习惯,能从根上避免"镜像没变"的误判。

6. 进阶技巧:镜像瘦身、多阶段构建与调试习惯

6.1 多阶段构建:把构建环境和运行环境分离

构建过程往往需要完整的编译工具链,但运行时根本用不到这些。多阶段构建允许你在第一阶段用完整环境编译,第二阶段只复制运行需要的产物,最终镜像只保留第二阶段的内容。我见过不少生产镜像里有 gcc、完整 SDK,体积轻松超过 1GB,改用多阶段构建后能降到两三百兆。

# 第一阶段:构建环境 FROM golang:1.22 AS builder WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /app/bin ./... # 第二阶段:运行环境 FROM alpine:3.20 WORKDIR /app COPY --from=builder /app/bin . CMD ["./bin"]

AS builder给第一阶段起名,第二阶段的COPY --from=builder只把编译产物复制过来,golang 镜像的体积不会进入最终镜像。这个技巧对各类带编译步骤的语言都适用:前端项目用 Node 镜像构建静态文件,再用 Nginx 镜像跑;Java 项目用完整 JDK 编译,运行时只复制 JAR 包。镜像小带来的直接收益是拉取快、启动快、磁盘占用少,团队多人共用一台构建机时体感尤其明显。

6.2 用 docker inspect 和 docker logs 定位问题

运行中的容器不是黑匣子,但前提是知道从哪里看。docker logs看标准输出,docker inspect看配置和状态。遇到"容器起不来"或"起来后行为异常",我一般按这个顺序排查:先docker ps -a看退出码,再docker logs看应用日志,最后docker inspect看底层状态,大多数问题两步内就能暴露。

# 查看容器完整状态信息 docker inspect my-app-test # 只看关键的几个字段 docker inspect my-app-test --format '{{.State.Status}} {{.State.ExitCode}}' docker inspect my-app-test --format '{{json .Mounts}}'

--format用 Go template 语法提取字段,比直接看整段 JSON 高效得多。排查"宿主机访问不到容器服务"时,除了看端口映射,还要确认容器内进程确实在监听:docker exec my-app-test netstat -tlnp,或者进容器里自己 curl 一下服务地址,能立刻区分是应用没起来还是网络层的问题。

我的个人习惯是给排障定一条固定工作流:先看全局,再看日志,后看细节,不跳步。这条工作流带过几次新成员之后,他们遇到问题第一反应就不再是盲目重启容器,而是按顺序收集信息。这个习惯也算这几年用 Docker 攒下的经验,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询