Docker 入门:从镜像、容器到项目部署

为什么需要 Docker

很多人第一次部署项目时,遇到的不是代码问题,而是环境问题:

  • 本地能跑,服务器跑不起来。
  • A 项目需要 Node.js 18,B 项目需要 Node.js 20。
  • 换一台机器,又要重新安装运行环境、配置环境变量、调整启动脚本。
  • 项目越多,服务器上的依赖、日志、进程和端口越难管理。

Docker 要解决的核心问题是:把应用和它依赖的运行环境一起打包,让它在不同机器上用尽量一致的方式运行。

可以把 Docker 理解成一种轻量的应用交付方式。它不会像传统虚拟机那样完整模拟一台操作系统,而是基于宿主机内核,把不同应用隔离在不同容器中。这样既能减少环境差异,又不会像虚拟机那样笨重。

先记住一句话:

Docker 不是为了让代码变得神奇,而是为了让代码运行环境变得可复制、可迁移、可管理。

没有 Docker 和有 Docker 的部署区别

假设服务器上要部署一个 Nuxt 应用,并且使用 Nginx 接收外部请求。

没有 Docker 时,常见结构是:

复制代码
Linux 宿主机
├─ Nginx(直接安装在宿主机)
└─ Nuxt 应用(直接运行在宿主机)

这种方式当然能用,但服务器会逐渐变成一个"手工配置现场":Node 版本、项目依赖、进程管理、日志目录、环境变量都散落在宿主机上。后续迁移、排查问题或部署新项目时,很容易出现环境不一致。

使用 Docker 后,可以有两种常见做法。

第一种是 Nginx 不容器化,只把应用容器化

复制代码
Linux 宿主机
├─ Nginx(直接安装在宿主机)
└─ Docker
   └─ Nuxt 容器

这种方式适合刚开始使用 Docker 的阶段。Nginx 仍然由服务器直接管理,Docker 只负责应用本身,理解成本比较低。

第二种是 Nginx 和应用都容器化

复制代码
Linux 宿主机
└─ Docker
   ├─ Nginx 容器
   └─ Nuxt 容器

这种方式更统一,所有服务都由 Docker 管理,通常会配合 docker compose 使用。正式项目中,如果后面还要加入数据库、Redis、后端服务、AI 服务等,多容器编排会更方便。

这两种方式没有绝对优劣。刚入门时,可以先让应用容器化;当项目服务变多、部署流程变复杂时,再把 Nginx、数据库、缓存等服务逐步纳入 Compose 管理。

镜像和容器

Docker 里最重要的两个概念是:镜像容器

镜像 Image 是静态的构建产物。它描述了应用运行需要的文件、依赖、环境和默认启动方式。可以把镜像类比成"软件安装包",也可以类比成面向对象里的"类"。

容器 Container 是镜像运行后的实例。它是动态的,是一个被隔离起来的运行环境。可以把容器类比成"安装并启动后的软件",也可以类比成面向对象里的"对象实例"。

简单说:

  • 镜像是构建结果。
  • 容器是镜像运行后的实例。
  • 一个镜像可以启动多个容器。
  • 删除容器不会自动删除镜像。
  • 删除镜像前,一般要先停止并删除依赖它的容器。

比如你构建出一个 my-blog:v1 镜像,就可以用它启动一个博客容器。将来升级版本时,可以重新构建 my-blog:v2,再用新镜像启动新容器。

Dockerfile 是什么

Dockerfile 是写给 Docker 的构建说明书。它告诉 Docker 如何一步一步生成镜像:使用哪个基础镜像、进入哪个工作目录、复制哪些文件、安装哪些依赖,以及容器启动时执行什么命令。

下面是一个入门版 Node.js 项目 Dockerfile:

sql 复制代码
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

这里每一行都对应一个构建或运行阶段的动作:

  • FROM node:20-alpine:选择一个已有的基础镜像,里面已经包含 Node.js 运行环境。
  • WORKDIR /app:指定容器内部的工作目录,后续命令默认都在这里执行。
  • COPY package*.json ./:先复制依赖描述文件,方便 Docker 利用构建缓存。
  • RUN npm ci --omit=dev:在构建镜像时安装生产依赖。相比 npm installnpm ci 更适合根据锁文件做可重复安装。
  • COPY . .:把项目其他文件复制到镜像中。
  • EXPOSE 3000:声明应用在容器内监听 3000 端口。注意它只是元信息,不会自动把端口暴露到宿主机。
  • CMD ["npm", "start"]:容器启动后默认执行的命令。

这里有两个时间点很容易混淆:

  • RUN 发生在构建镜像时,比如安装依赖、编译项目。
  • CMD 发生在容器启动时,比如启动 Web 服务。

真实项目中,还应该配合 .dockerignore 使用,避免把 node_modules、构建产物、日志、环境变量文件等内容复制进镜像:

bash 复制代码
node_modules
dist
.nuxt
.output
.env
*.log
coverage

如果项目需要先构建再运行,例如 Nuxt、Next.js、Vue、React 等前端项目,通常还会使用多阶段构建:第一阶段负责安装依赖和打包,第二阶段只保留运行所需文件。这样可以减少最终镜像体积,也能让镜像更干净。

下面是一个多阶段构建的例子:

sql 复制代码
# 阶段一:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 阶段二:运行
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output ./
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

构建和运行一个镜像

假设当前目录下已经有 Dockerfile,可以执行:

erlang 复制代码
docker build -t test-docker .

这条命令表示:使用当前目录的 Dockerfile 和项目文件构建一个名为 test-docker 的镜像。

构建完成后,可以启动容器:

css 复制代码
docker run -d \
  --name test-docker \
  -p 80:3000 \
  test-docker

这条命令的含义是:

  • docker run:根据镜像创建并启动一个容器。
  • -d:让容器在后台运行。
  • --name test-docker:给容器起一个明确的名字,后续可以直接用名字操作。
  • -p 80:3000:把宿主机的 80 端口映射到容器内部的 3000 端口。
  • test-docker:要运行的镜像名。

端口映射是初学 Docker 时最容易混淆的地方。80:3000 的左边是宿主机端口,右边是容器端口。访问服务器的 80 端口时,流量会被转发到容器内部的 3000 端口。

也可以这样理解:

markdown 复制代码
浏览器访问服务器 80 端口
        ↓
Docker 转发到容器 3000 端口
        ↓
容器里的应用处理请求

需要注意:默认情况下,发布端口可能会让外部网络访问到该服务。生产环境里,不要随手把数据库、Redis、内部管理服务直接映射到公网端口。如果只是想让本机 Nginx 反向代理访问应用,可以绑定到本地地址:

css 复制代码
docker run -d \
  --name test-docker \
  -p 127.0.0.1:3000:3000 \
  test-docker

这样外部用户不能直接访问容器端口,而是由宿主机上的 Nginx 统一接收请求后再转发。

一个更完整的项目结构

如果项目里有前端、后端、AI 服务,就可以给每个服务写自己的 Dockerfile,再用 compose.yamldocker-compose.yml 统一管理。

arduino 复制代码
ai-drug-discovery-workbench/
├─ compose.yaml
├─ backend/
│  └─ Dockerfile
├─ ai-service/
│  └─ Dockerfile
├─ frontend/
│  └─ Dockerfile
└─ nginx/
   └─ default.conf

这种结构的好处是:每个服务只关心自己的运行环境,整体启动则交给 docker compose。例如后端可以使用 Java 或 Node.js,AI 服务可以使用 Python,前端可以使用 Nuxt,它们不用把所有运行环境都堆在宿主机上。

一个简化的 Compose 文件可能长这样:

yaml 复制代码
services:
  frontend:
    build: ./frontend
    expose:
      - "3000"

  backend:
    build: ./backend
    expose:
      - "8080"
    environment:
      NODE_ENV: production

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - frontend
      - backend

在同一个 Compose 项目里,服务会加入默认网络,并且可以通过服务名互相访问。也就是说,Nginx 容器里可以把请求转发到 http://frontend:3000http://backend:8080,而不是写死某个容器 IP。容器 IP 可能会变化,服务名才是更稳定的连接方式。

常用 Docker 命令

初学阶段不需要背完所有命令,先掌握"看状态、看日志、进容器、停服务、清资源"这几类就够了。

查看容器

css 复制代码
docker ps
docker ps -a
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

docker ps 只查看运行中的容器,docker ps -a 会包含已经停止的容器。第三条命令可以用表格形式查看容器名称、状态和端口映射,更适合日常排查。

查看和删除镜像

复制代码
docker images
docker images your-app
docker rmi your-app:v1

docker images 用来查看本机已有镜像。docker rmi 用来删除镜像。如果某个镜像正在被容器使用,需要先停止并删除对应容器。

启动容器

perl 复制代码
docker run -d \
  --name my-app \
  -p 80:8080 \
  -v /data:/app/data \
  -e DB_HOST=db.example.com \
  --restart unless-stopped \
  your-app:v1.0

这是一条比较完整的启动命令:

  • --name my-app:给容器起一个名字,后续可以直接用名字操作它。
  • -p 80:8080:把宿主机 80 端口映射到容器 8080 端口。
  • -v /data:/app/data:挂载目录,用来持久化数据或提供配置文件。
  • -e DB_HOST=db.example.com:注入环境变量。
  • --restart unless-stopped:容器异常退出后自动重启,除非手动停止。
  • your-app:v1.0:指定镜像和版本标签。

这里不建议把 DB_HOST 写成 localhost,除非数据库就在同一个容器里。对容器来说,localhost 通常指容器自己,不是宿主机,也不是其他容器。多容器项目中,应该优先使用 Compose 服务名,例如 dbredisbackend

停止和重启容器

perl 复制代码
docker stop my-app
docker start my-app
docker restart my-app
docker stop $(docker ps -q)

容器既可以通过名称操作,也可以通过容器 ID 操作。日常使用时,建议给重要容器都设置明确的名字。

查看日志

perl 复制代码
docker logs my-app
docker logs -f my-app
docker logs --tail 100 my-app
docker logs --since 1h my-app

排查线上问题时,docker logs -f 很常用,它会持续输出日志,效果类似 tail -f

进入容器

perl 复制代码
docker exec -it my-app sh
docker exec -it my-app bash
docker exec my-app ls /app
docker exec my-app env

docker exec 可以在已经运行的容器里执行命令。基于 Alpine 的镜像通常只有 sh,Ubuntu、Debian 这类镜像通常可以使用 bash

删除容器和镜像

bash 复制代码
docker rm my-app
docker rmi your-app:v1

docker rm 删除容器,docker rmi 删除镜像。清理资源时一般先用 docker ps -a 找到停止的容器,再决定是否删除。

构建镜像时的 CPU 架构问题

构建镜像时还要注意 CPU 架构。很多开发者使用 Apple Silicon Mac,比如 M1、M2、M3、M4,这类机器通常是 linux/arm64 架构。而大多数云服务器、Intel/AMD 服务器通常是 linux/amd64 架构。

常见架构可以这样理解:

  • linux/arm64:ARM 64 位架构,常见于 Apple Silicon Mac、部分 ARM 服务器、树莓派等。
  • linux/amd64:x86_64 架构,常见于大多数云服务器和普通 Intel/AMD 服务器。

如果在 M 系列 Mac 上构建镜像,然后拿到 x86_64 云服务器上运行,就可能遇到架构不匹配的问题。部署到这类服务器时,可以显式指定目标平台:

bash 复制代码
docker build --platform linux/amd64 -t your-app:v1 .

如果需要同时支持多种架构,可以使用 docker buildx 构建多平台镜像,并推送到镜像仓库:

bash 复制代码
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t yourname/your-app:v1 \
  --push .

单平台构建适合个人项目或固定服务器架构;多平台构建适合要同时支持 x86_64 服务器、ARM 服务器或不同开发机器的项目。

Docker 和 Nginx 的关系

Docker 负责打包和运行应用,Nginx 更常见的角色是接收外部请求、处理域名和路径转发、统一配置 HTTPS。

没有 Nginx,服务器也可以部署多个项目,只要每个项目监听不同端口,外部就能通过 IP + 端口 访问它们。例如:

arduino 复制代码
http://服务器IP:3000
http://服务器IP:8080
http://服务器IP:9000

但是这种方式不适合正式网站。用户不会希望记住端口号,HTTPS 配置也会变得分散。

加入 Nginx 后,可以让外部统一访问标准的 80443 端口,再由 Nginx 根据域名、子域名或路径转发到不同项目:

rust 复制代码
blog.example.com  ->  frontend:3000
api.example.com   ->  backend:8080
ai.example.com    ->  ai-service:9000

所以更准确的理解是:Nginx 不是 Docker 的替代品。Docker 负责运行应用,Nginx 负责把外部请求转发到正确的应用。

如果 Nginx 直接安装在宿主机上,可以把容器端口绑定到 127.0.0.1,再让 Nginx 转发到本机端口。如果 Nginx 也在 Compose 里,则可以直接用服务名转发,例如 proxy_pass http://frontend:3000;

上传服务器的一种流程

如果不使用镜像仓库,也可以先在本地构建镜像,再把镜像文件上传到服务器。

整体流程是:

rust 复制代码
构建镜像 -> 保存镜像到文件 -> scp 上传到 Linux 服务器 -> 加载镜像 -> 运行容器

对应命令大致如下:

ruby 复制代码
# 本地构建镜像(注意指定目标平台)
docker build --platform linux/amd64 -t your-app:v1 .

# 保存镜像为文件
docker save your-app:v1 -o your-app-v1.tar

# 上传到服务器
scp your-app-v1.tar user@server:/tmp/

# 登录服务器
ssh user@server

# 加载镜像
docker load -i /tmp/your-app-v1.tar

# 运行容器
docker run -d --name your-app -p 80:3000 your-app:v1

这种方式适合个人项目、小项目或刚开始学习部署的时候。它的优点是直观,不需要先配置镜像仓库;缺点是手工步骤多,版本管理和回滚不够方便。

更正式的生产环境里,通常会把镜像推送到镜像仓库,例如 Docker Hub、GitHub Container Registry 或私有镜像仓库,然后让服务器直接拉取指定版本的镜像:

rust 复制代码
本地或 CI 构建镜像 -> 推送到镜像仓库 -> 服务器拉取镜像 -> 重启容器或 Compose 项目

这样做更利于版本追踪、自动化部署和回滚。

入门时最容易踩的坑

  1. localhost 理解错

    在容器内部,localhost 通常指容器自己。容器访问宿主机、其他容器或外部数据库时,要根据网络环境使用正确地址。

  2. 端口方向写反

    -p 80:3000宿主机端口:容器端口。如果应用在容器里监听 3000,右边就应该是 3000

  3. 把敏感服务暴露到公网

    数据库、Redis、内部管理后台不要随便 -p 到公网。能走内网或 Compose 网络的,就不要暴露到外部。

  4. 镜像里复制了太多无关文件

    忘记写 .dockerignore 会让镜像变大,也可能把 .env 等敏感文件打进镜像。

  5. 只会 docker run,不会看日志

    容器启动失败时,先看 docker ps -adocker logs 容器名,通常比反复重启更有效。

总结

Docker 入门可以先抓住这几件事:

  1. 镜像是静态的构建产物,容器是镜像运行起来后的实例。
  2. Dockerfile 是构建镜像的说明书,FROMWORKDIRCOPYRUNCMD 是最常见的指令。
  3. docker build 用来构建镜像,docker run 用来启动容器。
  4. 端口映射里,宿主机端口:容器端口,例如 80:3000
  5. docker compose 适合管理多容器项目,服务之间可以通过服务名访问。
  6. Nginx 不是 Docker 的替代品,它通常负责接收外部请求并转发到不同容器或服务。
  7. 正式部署时,把镜像构建、推送、拉取和重启流程标准化,比直接在服务器上手动安装依赖更可靠。

学 Docker 不需要一开始就把所有命令都背下来。先理解镜像、容器、Dockerfile、端口映射和日志查看,再拿一个真实项目从构建到部署跑通一遍,很多概念就会自然连起来。

参考资料

相关推荐
东莞市云毅网络有限公司2 分钟前
用 SQLite FTS5 给企业知识库做问答检索原型:中文二元切分与 BM25 排序
python·sqlite·自动化运维·geo·数据监测
putItInYourHand33 分钟前
Docker Compose 部署 Mosquitto MQTT Broker 完整实战指南
运维·docker·容器
考虑考虑9 小时前
cmd局部设置java变量
运维·后端·自动化运维
生活爱好者!9 小时前
哇!影视新玩法!docker部署NEOWATCH NAS
运维·docker·容器
cpolar技术支持13 小时前
Docker 容器启动失败怎么查?端口、日志、权限与网络排障完整教程
linux·docker·容器·cpolar·网络排障
一技安身15 小时前
【大模型】Win11 部署 DeepSeek+ RAGFlow0.27.2 (Ollama方案,断网可用)
docker·ai·语言模型
新时代牛马15 小时前
docker run 起不来?从dockerd、containerd 到runc 一条线讲透
运维·docker·容器
JavaPub-rodert16 小时前
Docker 安装 MySQL 完整教程:从零部署数据库,到生产环境持久化配置
数据库·mysql·docker
努力努力再努力wz17 小时前
【Docker 入门系列】:从 Namespace、CGroup 到 LXC:理解容器运行时、rootfs 与 LXC 实战
运维·docker·容器
JavaPub-rodert18 小时前
Docker 多阶段构建:为什么生产环境镜像可以小 90%
运维·docker·容器