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 小时前
Dockerfile 编写与优化完全指南:从入门到生产级实践
docker·docker file
ziqi5224 小时前
Docker compose 和共享数据
运维·docker·容器
泓博5 小时前
Macbook Docker Compose不识别
运维·docker·容器
susu10830189115 小时前
windows系统的WSL的Ubuntu安装docker
linux·ubuntu·docker
Riu_Peter6 小时前
【技术】Docker 部署 MySQL
mysql·adb·docker
木雷坞7 小时前
Jellyfin Docker Compose 媒体库为空排查:volume、PUID/PGID 和挂载路径
docker·docker-compose·jellyfin
杨浦老苏8 小时前
开源服务器监控工具Checkmate
运维·docker·群晖·网站监控
ℳ₯㎕ddzོꦿ࿐8 小时前
实战指南:使用 Docker Compose 优雅部署 MongoDB 并自动初始化用户
mongodb·docker·容器
一坨阿亮8 小时前
Docker 离线部署
java·spring cloud·docker