最近在帮团队做项目环境交付时,反复踩到同一个坑:本地开发环境一切正常,换到测试服务器或新同事电脑上,不是缺依赖,就是版本对不上,环境配置能折腾大半天。后来把整套项目环境用 Docker 打包后,效果立竿见影:一条命令启动,所有依赖、配置、运行时版本完全一致。本文就从实际使用角度出发,梳理 Docker 的核心概念、images 与容器的日常操作,以及如何用 Dockerfile 将项目环境完整打包,希望能帮你从“会查命令”进阶到“能交付环境”。
本文适合刚开始接触 Docker 的开发者,也适合想把自己的项目改造成容器化部署的后端、运维和测试同学。学完后你将掌握:Docker 的镜像与容器关系、常用命令、写一份可用的 Dockerfile、将镜像导出并部署到其他机器,以及常见报错的排查思路。
1. Docker 能解决什么问题
1.1 为什么需要 Docker
传统开发流程中,环境配置是最容易被低估的工作。一个 Web 项目可能依赖特定版本的 JDK、Node.js、MySQL、Redis,还可能涉及系统库、环境变量、权限配置。开发环境、测试环境、生产环境的操作系统不同,基础软件版本不同,导致的后果是“在我电脑上是好的”。
Docker 把应用及其依赖打包到一个标准化的镜像中,这个镜像可以在任何安装了 Docker 的机器上运行,从而解决环境不一致问题。
1.2 Docker 的三大核心概念
要理解 Docker,必须先分清三个概念:
- 镜像(Image):镜像是一个只读模板,里面包含操作系统基础层、代码、运行时、依赖库、配置文件的完整快照。可以理解为一个“可分发”的环境快照。
- 容器(Container):容器是镜像运行时的实例。镜像是静态文件,容器是动态运行态。同一个镜像可以启动多个容器,容器之间相互隔离。
- 镜像仓库(Registry):镜像仓库用于存储和分发镜像,常见的有 Docker Hub、私有仓库 Harbor、云厂商容器镜像服务等。可以理解成镜像的“Git 仓库”。
简单来说,镜像类似程序文件,容器类似运行中的进程。Docker 的日常操作可以归纳为:从仓库拉取镜像,根据镜像创建并运行容器,进入容器检查状态,最后把自定义镜像推送到仓库或导出文件。
1.3 Docker 与虚拟机的关键区别
常见误区是把 Docker 和虚拟机混为一谈。虚拟机包含完整操作系统,占用资源大,启动慢;Docker 容器直接使用宿主机内核,只打包应用层依赖,因此镜像体积更小、启动更快。
| 对比项 | 虚拟机 | Docker 容器 |
|---|---|---|
| 隔离级别 | 操作系统级 | 进程级 |
| 镜像大小 | GB 级 | MB 到几百 MB 级 |
| 启动速度 | 分钟级 | 秒级 |
| 资源占用 | 高 | 低 |
| 内核 | 自带完整内核 | 共享宿主机内核 |
这个区别意味着 Docker 更适合快速交付、弹性伸缩和 CI/CD,但它不提供完整的内核隔离,安全性边界需要额外配置(用户权限、内核 Capabilities、资源限制等),这在生产环境需要格外注意。
2. 环境准备与安装
2.1 安装方式选择
Docker 的安装方式根据操作系统不同而不同。Windows 推荐使用 Docker Desktop,macOS 也可以使用 Docker Desktop,Linux 服务器建议直接通过包管理器安装 Docker Engine。
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
Windows 环境安装 Docker Desktop 后,需要在 Settings 中启用 WSL 2 后端,这样容器性能更好,且可以同时使用 Linux 容器。安装后建议立即在 Docker Desktop 的 Settings 中配置镜像加速器,避免后续拉取镜像超时。
Linux 环境以 Ubuntu 为例,安装 Docker Engine 的基本步骤是:更新 apt 索引、安装依赖包、添加 Docker 官方 GPG 密钥和仓库、最后安装 docker-ce。安装完成后,将当前用户加入 docker 组,避免每次执行 docker 命令都加 sudo。
安装完成后,验证是否成功:
docker --version docker info如果 docker info 能正常显示系统信息,说明 Docker 已启动。
2.2 常用运行环境速查
- Linux(Ubuntu / CentOS):使用 Docker Engine,占用资源最低,适合服务器部署。
- Windows / macOS:使用 Docker Desktop,适合本地开发调试。
- 远程服务器:建议安装 Docker Engine + Docker Compose,通过编排文件启动整组服务。
不同操作系统的安装细节差异较大,建议安装前先确认当前系统的架构和版本,再参考 Docker 官方文档操作。
2.3 建议的项目目录结构
使用 Docker 后,推荐为项目创建一个固定目录,包含 Dockerfile、docker-compose.yml、代码目录和环境变量文件:
my-project/ ├── app/ │ └── 项目代码 ├── docker-compose.yml ├── Dockerfile ├── .dockerignore └── .env.dockerignore 的作用类似 .gitignore,把 node_modules、target、.git 等不需要进入镜像的目录排除掉,能显著减少构建上下文大小。
3. 镜像与容器的核心操作
当 Docker 安装完成后,建议先掌握一组高频命令。下面按“镜像管理”和“容器管理”两条线展开。
3.1 镜像相关命令
拉取镜像:
docker pull nginx:1.25-alpine如果不指定 tag,默认拉取 latest。生产环境强烈建议指定 tag,避免后续 rebuild 时基础镜像版本漂移。
查看本地已有镜像:
docker images输出会包含仓库名、标签、镜像 ID、创建时间和大小。镜像 ID 是 SHA256 的前 12 位,删除时可以直接使用。
构建自定义镜像:
docker build -t myapp:v1.0 .其中 -t 指定镜像名称和标签,末尾的点表示构建上下文为当前目录。
删除镜像:
docker rmi myapp:v1.0 docker image prune删除前需要先停止并删除依赖该镜像的容器。
3.2 容器相关命令
运行容器:
docker run -d --name my-nginx -p 8080:80 nginx:1.25-alpine参数说明:
- -d:后台运行。
- --name:容器名称。
- -p 8080:80:宿主机 8080 端口映射到容器 80 端口。
- -v 宿主机路径:容器路径:挂载数据卷,保留数据和配置。
查看运行中的容器:
docker ps docker ps -aps 只显示运行中的容器,ps -a 会包含已退出容器。排查问题时这个命令非常关键。
进入容器内部:
docker exec -it my-nginx /bin/sh如果容器是基于 Alpine 镜像,只有 /bin/sh;基于 Ubuntu 镜像的是 /bin/bash。进入后可以查看进程、日志、配置文件。
查看容器日志:
docker logs -f my-nginx停止与删除容器:
docker stop my-nginx docker rm my-nginx3.3 使用 volumes 保存持久化数据
容器是临时状态,容器被删除后内部写入的数据也会丢失。对于 MySQL、Redis、应用上传文件等需要持久化的场景,必须使用数据卷。
命名卷方式:
docker volume create mysql-data docker run -d --name mysql8 \ -v mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=root123 \ mysql:8.0bind mount 方式:
docker run -d --name my-nginx \ -v /opt/html:/usr/share/nginx/html \ -p 8080:80 \ nginx:1.25-alpinebind mount 更适合本地开发:直接修改宿主机文件,容器内即时生效。生产环境建议优先使用命名卷,把数据路径交给 Docker 管理。
4. 实战:使用 Docker 打包项目环境
下面以一个简单的 Nginx 静态网站为例,演示如何编写 Dockerfile、构建镜像、启动容器,并将镜像导出到其他机器运行。这个流程同样适用于 Spring Boot 打包、Python 项目、Node.js 项目等业务系统,核心思路一致。
4.1 创建项目目录与文件
先在本地创建如下结构:
docker-demo/ ├── web/ │ └── index.html ├── Dockerfile └── README.mdweb/index.html 内容:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Docker 打包演示</title> </head> <body> <h1>Hello Docker</h1> <p>这是一个通过 Dockerfile 打包并运行的静态页面。</p> </body> </html>4.2 编写 Dockerfile
Dockerfile 是构建镜像的脚本,每一行指令都会生成一个镜像层。下面是一个基础但完整的示例:
# 基础镜像 FROM nginx:1.25-alpine # 作者信息(非必需,但推荐保留) LABEL maintainer="your-email@example.com" # 将本地静态文件复制到 Nginx 默认站点目录 COPY web/ /usr/share/nginx/html/ # 对外暴露端口 EXPOSE 80 # 使用 Nginx 前台模式启动 CMD ["nginx", "-g", "daemon off;"]各指令作用:
- FROM:指定基础镜像。这里使用带 alpine 的轻量版本,镜像更小。
- COPY:把构建上下文中的 web 目录复制到容器内 Nginx 的网页目录。
- EXPOSE:声明容器运行时监听的端口。注意这只是声明,实际端口映射还是要靠 docker run 的 -p 参数完成。
- CMD:容器启动时执行的命令。Nginx 默认会以 daemon 方式启动,如果不加 daemon off,容器会立即退出。
构建镜像:
cd docker-demo docker build -t web-demo:v1.0 .构建过程中 Docker 会逐行执行 Dockerfile。看到 Successfully tagged web-demo:v1.0 表示构建成功。
4.3 运行容器并验证
docker run -d --name demo-web -p 8081:80 web-demo:v1.0 docker ps浏览器访问 http://localhost:8081,如果能看到 Hello Docker 页面,说明容器运行正常。
这里有个常见误区:容器启动后立即退出,通常原因是主进程没有保持前台运行。比如直接把 CMD 写成启动后台服务,容器没有存活进程就会退出。在 Docker 中,主进程是否存活决定容器是否运行。
4.4 将镜像导出并部署到其他机器
项目环境打包后,可以通过 save 命令把镜像保存为 tar 文件,拷贝到目标机器后用 load 导入。
导出镜像:
docker save -o web-demo.tar web-demo:v1.0在目标机器上导入:
docker load -i web-demo.tar导入后执行 docker images 应能看到 web-demo:v1.0,然后正常 docker run 即可。
对于多台服务器或团队协作场景,推荐使用镜像仓库而不是 save/load 文件。推送流程是:
docker tag web-demo:v1.0 my-registry.example.com/demo/web-demo:v1.0 docker push my-registry.example.com/demo/web-demo:v1.0在目标机器上:
docker pull my-registry.example.com/demo/web-demo:v1.0 docker run -d -p 8081:80 my-registry.example.com/demo/web-demo:v1.0企业内部建议使用私有镜像仓库,一方面便于统一管理镜像版本,另一方面可以结合仓库做镜像安全扫描和访问控制。
4.5 完整示例:Spring Boot 项目容器化
静态网站比较简单,这里额外演示一个 Spring Boot 项目容器化场景,方便后端开发者参考。
# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 运行阶段 FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]这段 Dockerfile 采用多阶段构建:
- 第一阶段用 Maven 镜像完成编译打包。
- 第二阶段只保留 JRE 和打好的 jar 包,镜像体积大幅缩小。
- 使用 ENTRYPOINT 而不是 CMD,更明确指定容器主进程。
运行命令:
docker build -t demo-springboot:v1.0 . docker run -d --name demo-boot -p 8080:8080 demo-springboot:v1.0健康检查在生产环境建议加在 Dockerfile 中:
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD wget -q -O - http://localhost:8080/actuator/health || exit 1这样 Docker 可以自动检测容器内服务是否健康,接入编排平台后能自动重启异常容器。
4.6 多服务编排:docker compose 简介
实际操作中,一个项目往往需要多个容器配合,例如 Web 服务依赖 MySQL 和 Redis。逐个 docker run 很容易出现参数混乱、启动顺序问题。此时推荐使用 Docker Compose。
docker-compose.yml 基础示例:
version: '3.8' services: mysql: image: mysql:8.0 container_name: demo-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: demo ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql networks: - demo-net redis: image: redis:7-alpine container_name: demo-redis ports: - "6379:6379" networks: - demo-net web: build: . container_name: demo-web depends_on: - mysql - redis ports: - "8080:8080" networks: - demo-net volumes: mysql-data: networks: demo-net:启动所有服务:
docker compose up -d查看状态:
docker compose ps停止服务:
docker compose down注意容器之间通过服务名(mysql、redis)在同一个 network 内互相访问,不需要再写 localhost。Web 服务内连接数据库的地址应为 jdbc:mysql://mysql:3306/demo。
5. 常见问题与排查思路
使用 Docker 过程中,下面的报错和坑命中率很高。
5.1 ERROR: pull access denied 或镜像下载超时
现象:docker pull 镜像时提示 pull access denied,或者长时间卡住不动。
可能原因:本地没有该镜像且没有登录对应镜像仓库权限;或者是镜像源网络连接不稳定导致拉取超时。
解决思路:
- 检查镜像名称拼写是否正确。
- 如果是私有仓库,先执行 docker login。
- 为 Docker 配置可访问的镜像加速器。在 Docker Desktop 的 Settings -> Docker Engine 中配置 registry-mirrors,然后重启 Docker;Linux 下在 /etc/docker/daemon.json 中配置后重启 docker 服务。
需要提醒的是,镜像加速配置要基于你所在网络环境实际可访问的公共镜像源,不要相信任意来源的镜像源地址,避免拉取到被篡改的镜像。
5.2 端口绑定失败 bind: address already in use
现象:docker run 时提示端口被占用。
排查步骤:
docker ps # 查看是否有容器已占用端口 # Linux 下查看端口占用 netstat -tunlp | grep 8080解决:更换宿主机的映射端口,例如把 -p 8081:80;或者先停止占用端口的容器/进程。
5.3 容器启动后立刻退出
现象:docker run 后,docker ps 看不到容器,docker ps -a 显示 Exited。
排查步骤:
docker logs <容器名或ID>常见原因:
- 前台进程未保持:主进程启动后台服务后退出。例如直接 CMD ["sh", "start.sh"],而 start.sh 里启动的是后台进程。
- 应用本身启动时报错:端口被占、数据库连接失败、配置文件找不到。
解决方案:修改 Dockerfile 的 CMD/ENTRYPOINT,确保主进程在前台运行;同时先通过 docker logs 查看应用日志修复应用问题。
5.4 无法进入容器,exec 报错
现象:docker exec -it 容器 /bin/bash 报错。
原因:基础镜像中可能没有 bash,比如 alpine 镜像只有 /bin/sh。
解决:
docker exec -it 容器 /bin/sh如果还是进不去,先 docker ps 确认容器确实在运行。
5.5 exec 权限不足或无法写入文件
现象:容器内使用非 root 用户时,写文件提示 Permission denied;或者容器内 root 用户操作受限。
原因:镜像本身没有声明非 root 用户,或者宿主机安全策略限制了容器能力。
解决思路:在 Dockerfile 中创建应用用户,并使用 USER 指令切换。例如:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser在宿主机上,尽量为项目文件设置合理权限。生产环境优先使用容器内部非 root 用户运行服务,减少提权风险。
5.6 镜像构建缓慢,体积过大
现象:docker build 耗时过长,镜像动辄几个 GB。
优化建议:
- 依赖缓存优先:在 Dockerfile 中先 COPY pom.xml、package.json 等依赖声明文件,执行依赖安装缓存,再 COPY 源代码。
- 使用多阶段构建:编译阶段和运行阶段分离。
- 使用体积较小的基础镜像:如 alpine、slim 版本。
- 书写 .dockerignore,避免把日志、node_modules、target 等传入构建上下文。
6. 最佳实践与工程建议
6.1 镜像版本管理
生产环境不要使用 latest 标签。基础镜像和业务镜像都应该使用明确的版本号,例如 nginx:1.25-alpine、web-demo:v1.0.1。否则后续重新构建时基础镜像的检测机制变化、依赖版本变动,都会导致环境不可复现,违背容器化的初衷。
6.2 通过环境变量区分环境
同一个镜像可以通过环境变量适配开发、测试、生产环境,而不需要为每个环境单独构建镜像。
docker run -d --name demo-web \ -e APP_ENV=production \ -e DB_URL=jdbc:mysql://mysql:3306/demo \ -p 8080:8080 \ web-demo:v1.0代码中通过读取环境变量获取配置,例如 Java 中可以使用 Spring 的 @Value 或 Environment 抽象,Python 中可以使用 os.environ。
6.3 数据卷与日志处理
容器内尽量避免保存重要数据。MySQL 数据、上传文件等必须使用 volume 持久化。日志建议输出到 stdout/stderr,由 Docker 日志驱动统一收集,而不是写入容器内文件。这样日志可以在宿主机通过 docker logs 查看,也可以交给日志系统集中采集。
6.4 镜像安全注意事项
镜像中如果包含 root 权限的进程、弱密码、未更新的系统库,都会变成安全隐患。建议按下面要求执行:
- 镜像内使用非 root 用户运行应用。
- 不在 Dockerfile 或环境变量中硬编码密码和密钥。
- 定期扫描镜像漏洞,尽量使用已修复安全漏洞的基础镜像版本。
- 私有仓库用于存储企业镜像,配置访问控制并与内部认证体系打通。
6.5 容器资源限制
容器默认不限制资源使用,单个容器可能占满宿主机的 CPU 或内存。生产环境建议加上资源限制:
services: web: build: . mem_limit: 512m cpus: '1.0'6.6 健康检查与优雅停机
重要服务建议在 Dockerfile 中配置 HEALTHCHECK,编排系统会根据健康状态决定是否重启容器。同时,应用代码应该监听 SIGTERM 信号,在进程退出前完成清理操作。
6.7 正确使用 .dockerignore
.dockerignore 是容易被忽略但非常重要的文件。以 Java 项目为例:
target/ .git/ .idea/ *.iml docker-data/忽略无关文件后,镜像构建速度会更快,构建上下文更小,同时避免把本地敏感文件带入镜像。
7. 总结与下一步学习路线
本文从 Docker 的概念入手,推理了镜像与容器的关系,并梳理了日常使用频率最高的命令。随后以一个静态网站和一个 Spring Boot 项目为例,完整演示了如何通过 Dockerfile 将项目环境打包成镜像、运行容器,以及如何将镜像导出到其他机器部署。
回顾一下,核心需要掌握的知识点有:
- 镜像是只读模板,容器是镜像的运行实例。
- Dockerfile 中 FROM、COPY、RUN、CMD/ENTRYPOINT、EXPOSE 等指令的用途。
- 容器是临时状态,持久化数据必须使用数据卷。
- 通过 -p 端口映射将容器服务暴露到宿主机上。
- 多服务项目使用 docker compose 统一管理。
- 镜像保存与导入命令 docker save / docker load,适用于离线交付。
下一步建议按以下顺序继续练习和实践:
- 将你的本地项目(Java、Python、Node.js 均可)写成 Dockerfile,构建出可运行镜像。
- 使用 docker compose 编排一个 Web 服务 + MySQL + Redis 的完整项目。
- 在测试服务器上部署容器,并尝试配置健康检查和资源限制。
- 了解容器化改造中业务系统遇到的问题,比如日志收集、配置文件注入、定时任务容器化等。
如果在动手过程中遇到报错,优先执行 docker logs、docker ps -a、docker inspect 查看容器状态和日志,大多数问题都能从这三条命令中找到线索。可以先从复制文中的示例开始,亲手构建一次镜像,体验一条命令交付项目环境的效率提升。