简介:一份面向初学者的Docker基础入门指南,聚焦容器技术的核心概念与实操技能,适合有一定计算机基础、希望通过容器实现快速部署和环境一致的开发者。内容从Docker与虚拟机的差异讲起,详细说明镜像作为只读模板、容器作为运行实例、仓库用于存储镜像的三大核心对象,并分别介绍在Windows、macOS和Ubuntu(以Ubuntu为例)上的安装步骤。基础操作部分覆盖拉取镜像、运行容器、查看容器状态、进入容器内部以及停止删除容器等常用命令;实战环节通过部署Nginx服务器演示Web应用容器化,再以编写Dockerfile构建Python应用镜像展示自定义镜像流程,并附权限设置、配置国内镜像源、数据卷持久化等常见问题解决方案。资源含1个PDF文档,仅约696KB,内容紧凑便于随时查阅。目前已有126人学习,容器零基础者可按章节顺序动手实践,后续可进一步学习多容器编排与生产环境部署。
1. Docker 入门指南:把“在我机器上能跑”变成“到哪都能跑”
每个做开发和运维的人,大概率都经历过同一场尴尬:代码在本地跑得好好的,发到测试环境就崩,崩的原因还不是逻辑错了,而是环境不对。某个系统库版本不一样、某个依赖没装、某个配置文件路径写死。这时候你会觉得,软件最脆弱的部分不是代码本身,而是代码赖以运行的整套环境。Docker 就是冲着这个问题去的——把应用连同它的运行环境一起打包成一个标准单元,这个单元在谁的机器上跑,行为都应该一样。Docker基础入门指南要解决的,正是“我知道 Docker 很火,但不知道从哪下手”的处境。适合谁?刚接触容器概念的开发者、被环境问题折磨的测试和运维、需要统一交付物格式的项目团队,都算。这篇笔记会沿着安装、镜像、容器、数据、排错这条线往下走,每一段都是可以直接照着敲的命令,读完你能自己把第一个容器跑起来,也能看懂别人项目里的 Dockerfile 和启动脚本在做什么。
2. 先把环境立起来:安装、验证和第一个容器
2.1 安装三步走:装引擎、配权限、验证守护进程
不同操作系统装 Docker 的方式差别不小,但核心三步是固定的:装引擎、把当前用户加进 docker 组、验证 docker 命令能通。以最常见的 Linux 环境为例,我一般会先检查系统里有没有残留的旧版本:
# 检查是否已安装过 docker which docker || echo "not installed" # 如果有旧版本,先卸载干净再装新的 sudo apt-get remove docker docker-engine docker.io containerd runc接着添加官方源并安装引擎。这一步用包管理器,别手动下载二进制,因为包管理器会帮你处理好依赖关系和后续升级:
# 添加 Docker 官方软件源(Debian/Ubuntu 系) sudo apt-get update sudo apt-get 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参数说明:install -m 0755 -d 是在创建 keyrings 目录并指定权限,0755 表示所有用户可读、仅 root 可写;gpg --dearmor 是把 GPG 公钥从 ASCII 格式转成二进制格式,apt 只认这种格式。写完源列表后,再执行安装:
sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugindocker-ce 是社区版引擎,containerd.io 是容器运行时,docker-compose-plugin 后面编排多容器时要用。装完之后别急着 sudo docker ps,先做权限配置:
# 把当前用户加入 docker 组,之后执行 docker 命令不用再加 sudo sudo usermod -aG docker $USER newgrp docker最后验证守护进程状态。注意,usermod 之后要重新登录终端,组权限才会真正生效。如果执行docker version报权限错误,多半是这一步没重新登录:
docker version docker psdocker version 能看到 client 和 server 两段信息。client 是本地的命令行工具,server 是后台守护进程 dockerd。如果 server 段报错或看不到,说明引擎没起来或权限不对,这条命令就是后续所有排错的第一道检查点。
2.2 第一次跑容器:hello-world 到底发生了什么
环境装好后的第一个动作,惯例是跑一个 hello-world 镜像:
docker run hello-world这条命令执行后,Docker 会先在本地镜像库里找有没有叫 hello-world 的镜像,没找到就自动去公共仓库拉取,拉下来后创建容器并运行,程序打印一段说明文字后退出。这个过程很多人跑完就完了,但我建议你多看一眼输出里“Docker daemon”那几行字——它实际在告诉你镜像从哪里来、容器为什么能启动、以及当前用户权限是否正常。
有一个细节容易被忽略:docker run是一个组合命令,它等于docker pull加docker create加docker start再加docker attach,只是 Docker 帮你串起来了。这也是入门时最容易混淆的概念。分开执行时,每一步的可观测性更强:
# 分开执行,观察每步输出 docker pull hello-world docker create hello-world docker start -a 容器ID参数说明:create 之后会返回一串容器 ID,不会立刻运行;start -a 表示附加到容器的标准输出上,把程序打印的内容显示到当前终端。如果 start 不加 -a,容器是在后台跑的,你什么都看不到。这个习惯值得养成——出问题时,把 run 拆开看,能快速定位是拉取阶段挂了,还是创建阶段挂了,还是运行阶段挂了。
2.3 网络环境差异导致的拉取慢问题
初学者最常见的挫败感来源,是 pull 镜像时卡住不动或超时。这不是 Docker 本身的问题,而是网络环境到公共镜像仓库之间链路质量的客观差异。处理思路是配置 registry-mirrors,让 Docker 从你所在网络能更快访问的镜像源拉取。
# 编辑 Docker 守护进程配置文件 sudo vi /etc/docker/daemon.json{ "registry-mirrors": ["https://你的可用镜像源地址"] }修改后必须重启守护进程才生效:
sudo systemctl daemon-reload sudo systemctl restart docker docker info | grep -A 1 "Registry Mirrors"注意,docker info 里能看到镜像源配置是否加载成功。这里不推荐照抄网上的某个固定地址,因为不同地区、不同运营商的访问速度差异很大,最好的做法是问一下你所在团队的同事“你们配的什么源”,那往往是经过实践验证的可用地址。另外,有些定位完网络问题的人会走偏,试图通过修改镜像源来解决所有拉取问题——镜像源只能改善镜像拉取链路,容器运行时访问外部服务的网络问题那是另一回事,别混为一谈。
3. 镜像与容器:理解 Docker 的两个核心对象
3.1 镜像不是一个大文件,是一摞只读层
很多人第一次看到 docker images 列表时,以为镜像就是一个压缩包式的完整文件。这个理解会导致后续很多困惑——为什么删掉容器后重新运行还是原来的状态?为什么两个镜像能共享几百兆的空间?Docker 的底层设计是分层文件系统,镜像由一层层只读层叠加而成,每一层对应 Dockerfile 里的一条指令。运行时,Docker 在只读层之上再挂一个可写层,所有写入操作都在可写层完成。容器删除时,销毁的是可写层,底层只读镜像层原封不动。
# 查看镜像分层信息的经典命令 docker history 镜像名:标签 # 查看镜像在磁盘上的实际占用 docker system dfdocker history 输出里每一行代表一个镜像层,能看到每层的大小和创建方式。docker system df 会列出一组数据:镜像占了多少、容器占了多少、可回收的是哪些。如果发现系统盘空间紧张,这个命令就是排查的入口。理解分层之后,有几个操作习惯会自然改掉:不要为了调试往容器里装一堆工具然后把容器 commit 成新镜像,因为 commit 会把可写层整个拍进去,产生大量冗余层。
3.2 用 Dockerfile 构建自己的镜像:最小可复现
拉取现成镜像只是入门,真正让 Docker 发挥价值的,是把你自己的应用打包成镜像。一个最小但完整的 Dockerfile 长这样:
# 基础镜像,尽量选官方且带具体版本号的 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 myapp:v1.0 . docker run -d -p 8000:8000 myapp:v1.0参数说明:build -t 指定镜像名和标签,最后的 . 是构建上下文目录,Docker 会把整个目录打包发送给守护进程用于构建。所以构建上下文里别放无关的大文件——例如 node_modules 或 .git 目录,会让构建过程变得奇慢。我一般会配合 .dockerignore 文件,把不需要的文件排除掉,规则和 .gitignore 类似。COPY requirements.txt 放在 COPY . . 之前,利用的是 Docker 的层缓存机制:只要 requirements.txt 没变,pip install 那一步就会直接命中缓存,重建镜像能省掉大量重复安装时间。
3.3 容器生命周期:创建、启动、停止、删除
容器生命周期管理是最容易踩坑的环节,别想当然。下面的表把常用命令按状态整理清楚:
| 操作 | 命令示例 | 作用阶段 |
|---|---|---|
| 运行新容器(前台) | docker run -it ubuntu bash | 创建并进入容器 |
| 运行新容器(后台) | docker run -d nginx | 创建并在后台运行 |
| 列出运行中容器 | docker ps | 查看活动容器 |
| 列出所有容器 | docker ps -a | 查看包含已退出的容器 |
| 停止容器 | docker stop 容器ID | 发送停止信号 |
| 强制杀掉 | docker kill 容器ID | 立即终止 |
| 删除容器 | docker rm 容器ID | 移除容器文件 |
| 删除镜像 | docker rmi 镜像ID | 移除镜像及引用 |
初始化参数的差异值得单独说。-it是两个参数连写,-i 保持标准输入打开,-t 分配一个伪终端。跑交互式程序(比如 bash、调试脚本)时这两个必须一起用,少一个都会发现进不去命令行界面。而跑守护型服务(比如 nginx、数据库)时用-d后台运行,启动后终端不会卡住。
删除容器的顺序有讲究。我曾经因为直接 docker rm 一个运行中的容器被报错,原因是我没先 stop。正确的习惯是:stop 等容器优雅退出,再 rm 清理。如果是调试完的临时容器,可以加 -f 参数强制删除,省一步,但业务环境的容器别这么干——强制删除会跳过优雅退出流程,有些服务可能没来得及把缓冲数据落盘就断了。
4. 数据持久化与端口映射:让容器真正可用
4.1 挂载数据卷:容器删了,数据还在
镜像的只读层和容器的可写层特性,带来一个绕不开的问题:容器一旦删除,可写层里的数据就跟着没了。这对跑数据库、存日志、存上传文件的场景是致命的。解决方案是数据卷(volume)或绑定挂载(bind mount)。
# 方式一:匿名卷,由 Docker 管理存储路径 docker run -d -v /var/lib/mysql mysql:8.0 # 方式二:命名卷,方便复用和备份 docker volume create mysql_data docker run -d -v mysql_data:/var/lib/mysql mysql:8.0 # 方式三:绑定挂载,直接把宿主机目录映射进容器 docker run -d -v /home/user/mysql_data:/var/lib/mysql mysql:8.0三种方式的差异值得拎清楚。匿名卷适合临时验证,你不需要关心数据存在哪;命名卷适合正式环境,Docker 负责管理存储位置,你只需要记住卷名;绑定挂载适合需要直接查看或备份数据的场景,因为数据就在你指定的目录里。但绑定挂载有一个权限坑:宿主机目录的 UID/GID 会直接映射进容器。如果容器内进程以 root 运行写文件,你会看到宿主机目录里冒出一堆 root 权限的文件,后续清理都费劲。这个问题我会在避坑章节展开。
查看容器挂载情况用 inspect 命令:
docker inspect 容器ID | grep -A 5 "Mounts"Mounts 字段里能看到 Source(宿主机路径)、Destination(容器内路径)和 Type(卷或绑定挂载)。多容器共享同一个卷也是常见做法,两个容器用同一个卷名映射不同目录即可。注意,多个容器同时写同一个文件会有并发问题,这类问题不归 Docker 管,得靠应用自己解决。
4.2 端口映射:把容器里的服务暴露出来
容器默认处在隔离网络里,宿主机访问不到容器内监听的端口,需要显式做映射。命令格式是 -p 宿主机端口:容器端口:
# 把容器内 80 端口映射到宿主机 8080 docker run -d -p 8080:80 nginx # 只绑定到指定 IP,避免暴露到所有网卡 docker run -d -p 127.0.0.1:8080:80 nginx # 随机分配宿主机端口,适合不想管端口冲突的场景 docker run -d -P nginx参数说明:-p 前面是宿主机侧,后面是容器侧,方向写反了会直接连不通。第一次跑这个命令的人最常见的错误就是记反顺序。 -P(大写)会把容器内所有 EXPOSE 的端口随机映射到宿主机高位端口,配合 docker port 容器ID 可以查到实际映射关系。生产环境我倾向于显式写 -p 指定固定端口,因为随机端口不利于防火墙规则和负载均衡配置。
局域网内其他机器想访问容器服务时,访问的是宿主机 IP 加映射端口,不是容器 IP。容器 IP 是 Docker 内部网络的地址,每次重建容器都可能变,依赖容器 IP 做访问是不推荐的。另外注意,只 bind 在 127.0.0.1 的端口只能本机访问,跨机器访问必须绑定到宿主机实际网卡 IP 或 0.0.0.0。
4.3 环境变量:同一个镜像跑出不同配置
镜像是构建时固定的,但不同环境(开发、测试、生产)的配置不同。常见的做法不是打三个镜像,而是一个镜像传不同的环境变量。Docker 内环境变量的机制如下:
# 在 docker run 时传入环境变量 docker run -d -e MYSQL_ROOT_PASSWORD=mysecret -e MYSQL_DATABASE=appdb mysql:8.0 # 从文件批量读取环境变量(推荐,避免敏感信息出现在命令行历史里) docker run -d --env-file ./env.list mysql:8.0env.list 文件每行一个 KEY=value,以 # 开头的行会被忽略:
# 环境变量文件示例 MYSQL_ROOT_PASSWORD=mysecret MYSQL_DATABASE=appdb MYSQL_USER=appuser MYSQL_PASSWORD=apppass注意,.env 文件不要提交到版本仓库里,里面通常有密码或密钥。维护多个环境时,我一般会为每个环境准备一份 env 文件:env.dev、env.test、env.prod,启动脚本里按环境变量选择加载哪一份。这里有不少血泪经验:曾经有人在命令行直接 -e 传密码,shell history 里留了明文,后来整个团队的凭据轮换了一轮。能用 --env-file 就别用 -e 传敏感信息。
另外一个常见认知偏差是:在容器内修改配置文件再 commit 成新镜像,这种做法建议尽量避免。配置应该通过环境变量或挂载文件注入,而不是固化进镜像。否则每改一次配置就多一个镜像版本,镜像仓库会变得难以维护。
5. 避坑指南:入门阶段最常见的 6 个翻车现场
5.1 容器时间不对,日志时间戳全乱套
现象:容器内 date 显示 UTC 时间,和宿主机差 8 小时,应用日志排查问题时对不上时间线。原因:基础镜像默认时区是 UTC,容器不会自动继承宿主机的时区设置。解决:
# 方式一:运行时挂载宿主机的时区文件 docker run -d -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro 你的镜像 # 方式二:在 Dockerfile 里预设时区(更干净) ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone推荐方式二,镜像一旦打出来,不管在哪台机器上跑,时区都是一致的。方式一依赖宿主机环境,换到 macOS 或 Windows 环境时表现不稳。
5.2 容器内文件是 root 属主,宿主机删不掉
现象:绑定挂载目录里出现一堆 root 属主文件,当前用户想删删不掉,只能 sudo。原因:容器内进程以 root 运行,写入挂载目录的文件属主映射到了宿主机 root。解决:运行容器时显式指定用户:
# 指定以当前用户 UID 运行容器内进程 docker run -d --user $(id -u):$(id -g) 你的镜像但这里有一个新坑:容器内进程如果不是 root,可能没权限写容器内的某些目录(例如 /var/log)。所以更稳妥的做法是在 Dockerfile 里先用 USER 指令切换,并提前把需要写的目录 chown 给该用户。这个问题的本质是 UID 映射理解不到位,建议在项目里尽早约定:运行用户是什么、哪些目录需要写权限、挂载目录权限如何规划。
5.3 容器启动后立刻退出,docker ps 看不到
现象:docker run 后容器状态一直显示 Exited,docker logs 什么都没有或只有一行报错。原因:容器内主进程退出,容器就结束,这是 Docker 的运行机制。很多服务型镜像需要前台运行,不能像在宿主机上那样 daemon 化。解决:先看日志定位真正原因:
docker logs 容器ID docker inspect 容器ID | grep -A 10 "State"State 字段里包含 ExitCode 和 Error,ExitCode 非 0 说明主进程崩溃;0 说明是正常退出,只是进程没有保持前台运行。某些服务在 Dockerfile 里 CMD 写了启动脚本但不加前台参数,就会直接退出。例如 nginx 需要 nginx -g 'daemon off;',redis 需要 redis-server --daemonize no。
5.4 docker run 时报端口被占用
现象:docker run 报 bind: address already in use,换一个端口又好使了。原因:宿主机端口已被其他进程或其他容器占用。解决:
# 查谁占了端口 sudo lsof -i :8080 # 或 sudo netstat -tlnp | grep 8080确认占用者后,要么停掉冲突进程,要么把映射端口改掉。但这里建议改映射端口而不是盲目停进程——线上环境的端口占用可能来自别的关键服务,停掉代价太大。还有一个容易忽略的点:Docker 网桥 IP 段和局域网冲突时,也可能出现奇怪的通不了网的问题,那是网络层的事,和端口占用是两个排查方向。新手遇到端口冲突就去停进程,不是好习惯。
5.5 镜像越拉越多,磁盘空间告急
现象:docker system df 显示可回收空间很大,但宿主机磁盘就是要满。原因:docker build 过程中的中间层镜像、悬空镜像(dangling images)、停止的容器、无用的匿名卷都会占空间,不自动清理。解决:
# 清理悬空镜像(没有标签且没有被容器引用的镜像) docker image prune -f # 清理停止的容器 docker container prune -f # 清理所有未使用的卷(谨慎!匿名卷里的数据会丢) docker volume prune -f # 一次性清理全部无用资源(最彻底) docker system prune -a --volumesdocker system prune -a 会连没有被容器使用的镜像一起删,下次再用就得重新拉取,所以执行前确认一下是否有依赖旧镜像的环境。这里最危险的是 docker volume prune -f,它只提示卷会被删除,但不会逐卷确认是否还有用。我见过有人把还挂着旧版本数据的卷一次性清掉,后悔药是没有的。加 --volumes 清理离线数据前,先跑一遍不带 --volumes 的清理,确认后再处理卷。
5.6 容器内 curl 不通外网,但宿主机没问题
现象:宿主机 curl 外网正常,进容器后 curl 超时,报 DNS 或连接相关错误。原因:容器默认网络模式是 bridge,DNS 配置继承自宿主机的 /etc/resolv.conf,如果宿主机用了特定的 DNS 配置或容器内没有对应的 DNS 工具,就会出问题。解决分两层:
# 临时进入容器排查网络 docker run -it --rm 你的镜像 bash cat /etc/resolv.conf # 指定 DNS 启动容器 docker run -d --dns 8.8.8.8 你的镜像这里也有一个常见的坑:基础镜像是 slim 或 alpine 版时,可能没装 curl、ping 这些排查工具,进容器后连最基本连通性测试都做不了。这类问题我一般先查 resolv.conf,再查路由(如果有 ip 命令),思路和排查物理机网络一样。如果你确认是镜像精简导致工具缺失,可以用宿主机的网络模式做对照测试,确认是 DNS 问题还是容器网络隔离问题。
6. 进阶验证:用一个完整小项目把 Docker 串起来
前面各章都是单点知识点,最后用一个综合练习把镜像构建、容器编排、数据持久化、环境变量、日志查看全部串起来。我选一个最常见的场景:一个 Flask 应用,搭配 Redis 做缓存,数据落盘到宿主机。不用 docker compose 之前,完整命令是这样的:
# 第一步:构建应用镜像 docker build -t flask-demo:v1 . # 第二步:先启动 Redis,因为它要被应用依赖 docker run -d --name redis-cache -v redisdata:/data redis:7-alpine # 第三步:启动 Flask 应用,链接 Redis,映射端口 docker run -d --name flask-app \ -p 8080:5000 \ -e REDIS_HOST=redis-cache \ -v applogs:/app/logs \ flask-demo:v1然后验证整个链路是否正常:
# 看两个容器的运行状态 docker ps --filter "name=redis" --filter "name=flask" # 看应用日志 docker logs -f flask-app # 进入应用容器内部,验证能否连通 Redis docker exec -it flask-app sh -c "curl http://redis-cache:6379/hi"验证通过后你会接触到一个 Docker 内置的魔力:容器名就是可解析的 DNS 名称。在上面的命令里,Flask 应用里连接 Redis 时直接写 redis-cache 这个主机名就能连上,不用关心 Redis 容器每次启动后的 IP 是多少。这是因为同一台机器上的容器默认加入同一个 bridge 网络,彼此可以通过容器名互相访问。这是 Docker 网络模型里最实用的一个机制。
这个练习跑通之后,建议你自己再变体测试一下:停掉 Redis 容器,应用是否还能启动但报连接错误?删掉 Flask 容器重新跑一个,数据卷和日志卷是否还在?改一个环境变量重启容器,配置是否生效?这四种情况覆盖了入门阶段 80% 的操作面。我的一个习惯是每学一个新命令,就把它的关键参数文档读一遍,然后用 docker inspect 看这个命令实际改了什么——很多看似玄学的问题,其实是某个参数没吃透。这套路径和方法帮了不少人,希望帮到你。
本文还有配套的精品资源,点击获取