# Docker 入门

Docker 是一个开源的容器化平台,它能够将应用程序及其依赖打包到一个“容器”中。这种容器可以在任何支持 Docker 的环境中一致地运行,不受操作系统和环境配置的影响。

# 作用

  • 环境一致性:无论是在开发、测试还是生产环境中,Docker 都能确保你的应用以相同的方式运行,避免“在我电脑上没问题”的情况。
  • 快速部署:通过镜像快速构建和部署应用,节省配置环境的时间。
  • 资源隔离:每个容器都是独立的单元,运行在隔离的环境中,不会互相干扰。
  • 易于扩展和集成:结合 Docker Compose、Kubernetes 等工具,Docker 适用于微服务架构和大规模部署场景。

# 安装

Docker 推荐安装 Docker Desktop (opens new window),它包含了完整的 Docker 运行环境(包括 Docker Engine、Docker CLI、Docker Compose 等)

安装 Docker Desktop (opens new window) 很简单,直接下载安装即可。安装 Docker Desktop (opens new window) 之后即可以使用桌面 App,也可以使用 CLI。

也可以只安装 Docker CLI。接下来重点介绍怎么在 Macbook 上只安装 Docker CLI。

首先安装 dockerdocker-composedocker-buildx

$ brew install docker docker-compose docker-buildx
1

docker-compose 用于编排多个容器

docker-buildx 用于高级构建,支持多架构

但 Docker Engine 不能直接在 macOS 上运行(macOS 不支持 dockerd),你还需要额外配置 Docker Daemon,可以使用 colima 或 rancher-desktop

$ brew install colima
$ colima start
1
2

运行运行 docker ps 来测试 Docker 是否正常运行。

$ docker ps
1

# colima 配置 HTTP 代理

在国内使用 Docker 首先需要配置 HTTP 代理,否则无法拉取镜像,我使用的是 colima,所以这里介绍 colima 怎么配置 HTTP 代理。

  1. 打开 colima 配置文件
 $ vim ~/.colima/default/colima.yaml 
1
  1. 添加或修改 env 字段(具体配置取决于您的代理工具)
env:
  ALL_PROXY: socks5://127.0.0.1:1081
  HTTP_PROXY: http://host.docker.internal:8001
  HTTPS_PROXY: http://host.docker.internal:8001
1
2
3
4
  1. 重启 colima
$ colima stop
$ colima start 
1
2

配置好了之后就可以成功拉取镜像了

$ docker pull xxx
1

# 镜像

镜像是一个标准化的软件包,其中包含运行容器所需的所有文件、二进制文件、库和配置。

镜像有两个重要特性:

  • 不可变。镜像一旦创建,就无法修改。您只能创建新镜像或在其基础上进行更改。
  • 由多个层组成。每一层代表一组文件系统的更改,比如添加、删除或修改文件。

# 拉取镜像

我们可以通过 docker pull 命令来拉取镜像,比如拉取 nginx 镜像

$ docker pull nginx:lastet
1

拉取的镜像来自哪里呢?默认是 Docker Hub (opens new window)。也可以使用第三方的镜像仓库,比如阿里云、腾讯云等。

# Docker Hub

Docker Hub (opens new window) 是全球默认的镜像存储和分发平台。Docker Hub 提供各种 Docker 支持和认可的镜像。这些镜像提供完全托管的服务,可作为您创建自定义镜像的绝佳起点。其中包括:

# 镜像 Tag

Docker Hub (opens new window) 里的镜像有各种各样的 tag,比如 nginx 镜像 (opens new window) tag 如下:

1.29.6, mainline, 1, 1.29, latest, 1.29.6-trixie, mainline-trixie, 1-trixie, 1.29-trixie, trixie

1.29.6-perl, mainline-perl, 1-perl, 1.29-perl, perl, 1.29.6-trixie-perl, mainline-trixie-perl, 1-trixie-perl, 1.29-trixie-perl, trixie-perl

1.29.6-otel, mainline-otel, 1-otel, 1.29-otel, otel, 1.29.6-trixie-otel, mainline-trixie-otel, 1-trixie-otel, 1.29-trixie-otel, trixie-otel

1.29.6-alpine, mainline-alpine, 1-alpine, 1.29-alpine, alpine, 1.29.6-alpine3.23, mainline-alpine3.23, 1-alpine3.23, 1.29-alpine3.23, alpine3.23

1.29.6-alpine-perl, mainline-alpine-perl, 1-alpine-perl, 1.29-alpine-perl, alpine-perl, 1.29.6-alpine3.23-perl, mainline-alpine3.23-perl, 1-alpine3.23-perl, 1.29-alpine3.23-perl, alpine3.23-perl

1.29.6-alpine-slim, mainline-alpine-slim, 1-alpine-slim, 1.29-alpine-slim, alpine-slim, 1.29.6-alpine3.23-slim, mainline-alpine3.23-slim, 1-alpine3.23-slim, 1.29-alpine3.23-slim, alpine3.23-slim

1.29.6-alpine-otel, mainline-alpine-otel, 1-alpine-otel, 1.29-alpine-otel, alpine-otel, 1.29.6-alpine3.23-otel, mainline-alpine3.23-otel, 1-alpine3.23-otel, 1.29-alpine3.23-otel, alpine3.23-otel
1
2
3
4
5
6
7
8
9
10
11
12
13

这么多 tag,我们应该使用哪个呢?

其实镜像 tag 是由三个部分组成:

版本 + 操作系统 + 变体修饰
1

例如:

1.29.6-alpine3.23-otel
1

下面是 tag 各部分的含义

版本:

版本 含义
1.29.6 精确版本(最稳定、完全可控,生产推荐)
1.29 锁定 minor 版本,自动升级 patch(兼顾稳定和安全更新)
1 锁定大版本,自动升级 minor + patch(不够稳定)
latest 最新版本(完全不可控,不建议生产使用)
mainline 最新功能版本(更新快,可能不够稳定)
stable 稳定版本(生产首选)

操作系统:

操作系统 含义
trixie Debian 系(默认),稳定、兼容性好、但体积较大
alpine Alpine 系, 体积小、启动快

变体修饰:

变体修饰 含义
slim 精简版,体积压缩到极致
perl 带 Perl 模块
otel 集成 OpenTelemetry(用于监控/链路追踪)

生成环境建议使用精确版本,比如 nginx:1.29.6nginx:1.29.6-trixie

开发时可以使用 alpine 版本,加快构建速度,比如 nginx:1.29.6-alpinenginx:alpine

# 构建镜像

构建镜像一般要用到 Dockerfile (opens new window) 文件。它是一个基于文本的文件,向镜像构建者提供指令,包括要运行的命令、要复制的文件、启动命令等等。

下面是一个构建 Python 应用程序镜像的 Dockerfile 文件

FROM python:3.13
WORKDIR /usr/local/app

# Install the application dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Copy in the source code
COPY src ./src
EXPOSE 8080

# Setup an app user so the container doesn't run as the root user
RUN useradd app
USER app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Dockerfile 最常见的一些指令包括:

  • FROM <image> - 指定基础镜像。
  • WORKDIR <path> - 指定“工作目录”或镜像中的路径,后续的指令在此目录下执行。
  • COPY <host-path> <image-path> - 从主机复制文件到容器镜像。
  • RUN <command> - 告诉构建器运行指定的命令。
  • ENV <name> <value> - 设置一个容器将使用的环境变量。
  • EXPOSE <port-number> - 指定镜像要公开的端口。
  • USER <user-or-uid> - 为所有后续指令设置默认用户。
  • CMD ["<command>", "<arg1>"] - 设置镜像容器将运行的默认命令。

要了解 Dockerfile 的更多信息,请查看 Dockerfile reference (opens new window)

docker init 命令会分析你的项目并快速创建 Dockerfilecompose.yaml.dockerignore 文件,帮助你快速上手。

$ docker init
1

安装 Docker Desktop (opens new window) 才有 docker init 命令,也可以通过 brew install docker-init 安装

定义好 Dockerfile 文件之后,就可以通过 docker build 命令构建镜像了

$ docker build -t image-tag .
1

注意末尾的 . 号,表示在当前目录里查找 Dockerfile,并根据 Dockerfile 文件编译镜像。

image-tag 代表镜像标签,就是给镜像一个好记的名字,一般由四个部分组成:

[HOST[:PORT_NUMBER]/]PATH[:TAG]
1
  • HOST :注册表主机名(可选)。如果未指定主机名,则默认使用 Docker 的公共注册表 docker.io
  • PORT_NUMBER :如果提供了主机名,则为注册表端口号(可选)。
  • PATH :镜像路径,用斜杠分隔。对于 Docker Hub,格式为 [NAMESPACE/]REPOSITORY ,其中 NAMESPACE 可以是用户名称或组织名称。如果未指定 NAMESPACE,则使用 library ,这是 Docker 官方镜像的命名空间。
  • TAG :标签(可选),参考 [镜像 Tag](#镜像 Tag)。如果未指定标签,则默认使用 latest

# 发布镜像

镜像构建并打好标签后,就可以将其推送到镜像仓库了。

  1. 首先我们需要去 Docker Hub (opens new window) 上注册用户
  2. Docker Hub (opens new window) 里创建个人 Repository
  3. 使用 docker image build (opens new window) 构建镜像
$ docker build -t USER_NAME/REPOSITORY_NAME .
1

注意 USER_NAMEDocker Hub (opens new window) 上注册的用户名,REPOSITORY_NAME 是创建的仓库名称,这两个需要对应上。

  1. 使用 docker login (opens new window) 命令进行身份验证

  2. 最后使用 docker push (opens new window) 发布镜像

$ docker push USER_NAME/REPOSITORY_NAME
1

经测试,如果之前没有创建名为 REPOSITORY_NAME 的仓库,docker push 也可以成功,并且自动创建 REPOSITORY_NAME 仓库。

# 多阶段构建

在传统的构建过程中,所有构建指令都会按顺序在同一个构建容器中执行:下载依赖项、编译代码和打包应用程序。所有这些步骤最终都会整合到最终镜像中。

这种方法虽然可行,但会导致镜像体积庞大,增加不必要的重量,并提高安全风险。而多阶段构建则能有效解决这些问题。

多阶段构建是指在 Dockerfile 中引入多个阶段,每个阶段都有其特定用途。

您可以将其理解为能够在多个不同的环境中构建不同部分。通过将构建环境与最终运行时环境分离,您可以显著减小镜像大小和攻击面。这对于具有大量构建依赖项的应用程序尤其有利。

多阶段构建具有以下几个优点:

  • 将构建时依赖项与运行时依赖项分开。
  • 通过仅上传应用运行所需的数据来减小镜像的大小。

我们以构建 React 应用为例说明。构建 React 应用,我们需要一个 Node 环境来编译 JS/JSX 代码、SASS/LESS 样式表等,然后生成静态 HTML、JS 和 CSS 文件。如果我们没有服务器端渲染,生产环境就不需要 Node。我们就可以将静态资源放在 nginx 容器中部署。

# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
WORKDIR /app
COPY package* ./
RUN npm install
COPY public ./public
COPY src ./src
RUN npm run build

FROM nginx:alpine AS final
COPY --from=build /app/dist /usr/share/nginx/html
1
2
3
4
5
6
7
8
9
10
11

在这个例子中,Dockerfile 使用两个阶段:

  • build 阶段,编译 React 应用,此阶段依赖 node 环境

  • final 阶段,复制build 阶段的产物,在 nginx 容器中部署

docker build 命令最终生成的镜像仅包含最后一个阶段生成的镜像,但可以使用 --target 指定构建阶段。

 $ docker build -t image --target build .
1

# 常用镜像操作命令

# 搜索镜像

$ docker search nginx
1

# 查看本地镜像

$ docker image ls

# 简写
$ docker images
1
2
3
4

# 拉取镜像

$ docker image pull nginx
$ docker image pull node:alpine

# 简写
$ docker pull nginx
$ docker pull node:alpine
1
2
3
4
5
6

# 查看图像的图层

$ docker image history nginx:alpine
1

# 构建镜像

$ docker build -t my-app:latest .
1

# 添加另一个标签

$ docker image tag username/my-image username/another-image
1

# 镜像归档

$ docker image save myimage:latest | gzip > myimage_latest.tar.gz
1

# 发布镜像

$ docker image push USER_NAME/REPOSITORY_NAME

# 简写
$ docker push USER_NAME/REPOSITORY_NAME
1
2
3
4

# 清理无用镜像

$ docker image prune       # 删除 dangling 镜像
$ docker image prune -a    # 删除所有未使用镜像
1
2

# 删除镜像

$ docker image rm IMAGE_ID
1

# 容器

什么是容器?简单来说,容器是应用程序各个组件的独立进程。每个组件(前端 React 应用、Python API 引擎和数据库)都在各自独立的运行环境中运行,与计算机上的其它程序完全隔离。

容器具有:

  • 自包含性。每个容器都拥有运行所需的一切,无需依赖主机上任何预装的依赖项。
  • 隔离性。由于容器运行在隔离环境中,它们对主机和其它容器的影响极小,从而提高了应用程序的安全性。
  • 独立性。每个容器都独立管理。删除一个容器不会影响其它容器。

# 容器 VS 虚拟机

虚拟机就是一个完整的操作系统,拥有自己的内核、硬件驱动程序、程序和应用程序。仅仅为了隔离运行单个应用程序而启动虚拟机,会造成很大的系统开销。

而容器本质上是一个独立的进程,其中包含运行所需的所有文件。如果运行多个容器,它们共享同一个内核,从而可以在更少的资源上运行更多应用程序。

# 运行容器

然后使用 docker container run (opens new window)(简写 docker run) 运行容器

$ docker run -d --name CONTAINER_NAME IMAGE_NAME
1

比如运行 nginx

$ docker run -d --name nginx-container nginx
1

使用 docker container ls (opens new window) (简写 docker ps) 查看运行中的容器

CONTAINER ID   IMAGE                COMMAND                   CREATED         STATUS         PORTS                                         NAMES
e702ab5515c1   nginx:alpine         "/docker-entrypoint.…"   3 seconds ago   Up 3 seconds   80/tcp                                        nginx-container
1
2

使用 docker container stop (opens new window) 停止容器运行

$ docker container stop CONTAINER_ID
1

使用 docker container rm (opens new window) 删除容器

$ docker container rm CONTAINER_ID
1

# 端口映射

我们之前说过容器具有隔离性,这种隔离性对于安全性和依赖项管理非常有利,但也意味着我们无法直接访问容器。这个时候我们就需要端口映射。

端口映射是通过设置转发规则来突破一定的网络隔离。例如,我们可以指定将主机 8080 端口上的请求转发到容器的 80 端口。

# 语法
$ docker run -d -p HOST_PORT:CONTAINER_PORT nginx

# 例子
$ docker run -d -p 8080:80 nginx 
1
2
3
4
5

如果没有提供 HOST_PORT,Docker 自动选择端口。

 $ docker run -d -p 80 nginx
1

容器运行后,使用 docker ps 命令进行查询:

$ docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS                    NAMES
a527355c9c53   nginx         "/docker-entrypoint.…"   4 seconds ago    Up 3 seconds    0.0.0.0:54772->80/tcp    romantic_williamson
1
2
3

此时映射的是主机的 54772 端口。

# 桥接网络

默认情况下,容器运行时会自动连接到一个名为桥接网络的特殊网络。该桥接网络就像一个虚拟桥接器,允许同一主机上的容器相互通信,同时又将它们与外部网络和其他主机隔离。

对于大多数容器交互而言,这是一个便捷的起点。但是默认桥接网络有两个缺点:

  • 只能通过 IP 地址通信,而自定义网络,容器可以通过容器名称或别名通信。
  • 同一主机上的所有容器都能相互通信,这增加了风险。而自定义网络创建了一个作用域限定的网络,只有连接到该网络的容器才能通信,从而提供更好的隔离性。

通过在运行 docker run 命令时传递 --network 参数来创建自定义网络。而所有没有使用 --network 参数的容器都会连接到默认的桥接网络。

可以通过 docker network create (opens new window) 创建自定义网络。

# 1. 创建自定义网络
$ docker network create mynetwork

# 2. 连接到自定义网络
$ docker run -d --name db -e POSTGRES_PASSWORD=secret -p 5434:5432 --network mynetwork postgres
1
2
3
4
5

然后容器之间可以通过容器名称进行通信,比如 express 容器使用 db 访问 postgres 数据库

$ docker run -d --name my-express --network mynetwork your-express-image
1
const pool = new Pool({
  host: 'db', // 👈 容器名称
  port: 5432, // 👈 内部接口
  user: 'postgres',
  password: 'secret',
  database: 'postgres',
})
1
2
3
4
5
6
7

# 与容器共享本地文件

每个容器都拥有运行所需的一切,不需要依赖宿主机。这种隔离性带来的主要优势是:容器可以最大限度地减少与宿主机系统或其他容器的冲突。然而,这种隔离性也意味着默认情况下容器无法直接访问宿主机上的数据。

Docker 提供了两种主要的存储选项,用于在主机和容器之间持久化数据和共享文件:卷(volume)和绑定挂载(bind mount)。

  • 如果想要容器内生成或修改的数据在容器停止运行后仍然保留,则选择使用卷
  • 如果想要将主机系统上的特定文件或目录(例如配置文件或开发代码)直接共享给容器,则使用绑定挂载。

# Volume 卷

容器启动时,会使用镜像提供的文件和配置。每个容器都可以创建、修改和删除文件,而且这些操作不会影响其他容器。但是当容器删除时,这些文件更改也会被删除。

容器的这种短暂性固然很好,但当需要持久化数据时,却带来了挑战。例如,如果重启数据库容器,我们可能不希望数据库是空的。那么,如何持久化文件呢?需要使用 Volume。

Volume(卷)是一种存储机制,它能在容器生命周期之外持久化数据。可以把它想象成在容器内部和外部之间建立了一条快捷方式或符号链接。

使用 docker volume create (opens new window) 创建 volume

$ docker volume create postgres_data
1

使用 docker run -v VOLUME:容器目录 参数将 volume 挂载到容器的某个目录下,比如下面把 postgres_data 挂载到容器的 /var/lib/postgresql 目录

$ docker run --name=db -e POSTGRES_PASSWORD=secret -d -v postgres_data:/var/lib/postgresql postgres:18
1

当我重启容器时,postgres_data 的数据依然存在。

甚至当我删除该容器,启动另一个容器时,postgres_data 的数据也依然存在。

$ docker stop db
$ docker rm db
$ docker run --name=new-db -e POSTGRES_PASSWORD=secret -d -v postgres_data:/var/lib/postgresql postgres:18
1
2
3

需要注意的是 volume 数据并不是存在主机电脑了(可能因为我用的是 Mac 电脑,如果是 Linux 系统, volume 数据应该存在主机电脑上,有待验证),而存在 Docker Engine(Colima) 创建的 Linux 虚拟机里。即:

Mac 电脑
   ↓
Colima (Linux VM)
   ↓
Docker Engine
   ↓
Volume (postgres_data)
   ↓
/var/lib/docker/volumes/postgres_data/_data
1
2
3
4
5
6
7
8
9

使用下面的命令可以把 volume 数据导出到 Mabook 电脑,使用下面的命令

docker run --rm \
  -v postgres_data:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/postgres_data_backup.tar.gz -C /data .
1
2
3
4

说明:

  • --rm:执行完自动删除这个临时容器

  • -v postgres_data:/data:把你的 volume 挂载到临时容器里

  • -v $(pwd):/backup:把你 Mac 当前目录挂载进去

  • alpine:用一个轻量镜像来执行 tar 命令

# Volume 其它操作命令
$ docker volume ls                      # 查看所有 volume
$ docker volume inspect postgres_data   # 查看 volume 详情
$ docker volume prune							      # 删除无用的 volume
$ docker volume rm postgres_data        # 删除 volume
1
2
3
4

# Bind Mount 绑定挂载

绑定挂载像在主机和容器之间打开了一个共享通道。

绑定挂载非常适合开发环境,比如我们正在开发一个项目,当我们 build 时,构建生成的文件会保存在一个单独的子目录中。我们希望 Docker 容器能直接访问这些文件。此外,我们还希望重新构建时,容器能自动访问最新的构建文件。

我们可以使用 docker run -v 绑定挂载

 $ docker run -v /HOST/PATH:/CONTAINER/PATH -it nginx
1

或者使用 docker run ---mount 绑定挂载

docker run --mount type=bind,source=/HOST/PATH,target=/CONTAINER/PATH,readonly nginx
1

📢 Docker 建议使用 --mount 语法而不是 -v 。这样可以更好地控制挂载过程,并避免因目录缺失而导致的潜在问题。

比如我在 服务器部署 Storybook 文档 里,将 storybook 的构建文件挂载到 nginx 容器下。

$ docker run -d \
  -p 6006:80 \
  --name storybook \
  -v /remote-dir:/usr/share/nginx/html:ro \
  nginx
1
2
3
4
5

# 常用的容器操作命令

下面是关于容器的操作命令

# 查看容器

$ docker container ls            # 运行中的容器
$ docker container ls -a         # 所有容器(含已停止)

# 简写
$ docker ps                      # 运行中的容器
$ docker ps -a                   # 所有容器(含已停止)
1
2
3
4
5
6

# 创建并启动容器

$ docker container run nginx
# 简写
$ docker run nginx

# 常用参数
$ docker run -d nginx                # 后台运行
$ docker run -it alpine sh           # 交互式终端
$ docker run --name my-nginx nginx   # 指定容器名
$ docker run -p 8080:80 nginx        # 端口映射
$ docker run -v $(pwd):/app node     # 挂载目录
$ docker run -e foo=bar nginx        # 设置环境变量
$ docker run --env-file .env nginx   # 读取环境变量配置文件
# 限制资源的使用
$ docker run --memory="512m" --cpus="0.5" postgres
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 启停容器

$ docker container start CONTAINER_ID
$ docker container stop CONTAINER_ID
$ docker container restart CONTAINER_ID
1
2
3

# 删除容器

$ docker container rm CONTAINER_ID
$ docker container rm -f CONTAINER_ID   # 强制删除(运行中)
1
2

# 进入容器

$ docker container exec -it CONTAINER_ID sh
$ docker container exec -it CONTAINER_ID bash # bash

# 进入 pgsql 容器
$ docker container exec -it CONTAINER_ID psql -U postgres
1
2
3
4
5

# 查看容器日志

$ docker container logs CONTAINER_ID
$ docker container logs -f CONTAINER_ID   # 实时日志
1
2

# 多容器编排

容器管理的最佳实践是每个容器只做一件事,并且把它做好。如果一个应用比较复杂,涉及多项功能,可能需要运行多个容器。这个时候就需要 Docker Compose 实现多容器运行。

使用 Docker Compose,我们无需运行多个 docker run 命令。只需要一个名为 compose.yml 的 YAML 文件来定义整个多容器应用程序即可。

Docker 文档 (opens new window)里有个 compose.yml 例子:

services:
  app:
    image: node:24-alpine
    command: sh -c "npm install && npm run dev"
    ports:
      - 127.0.0.1:3000:3000
    working_dir: /app
    volumes:
      - ./:/app
    environment:
      MYSQL_HOST: mysql
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos

  mysql:
    image: mysql:8.0
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
  todo-mysql-data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

关于 compose.yml 文件的更多详情,请参考 Compose file reference (opens new window)

使用 docker compose up (opens new window) 创建和启动容器。

$ docker compose up -d --build
1

运行后,我们应该会看到类似如下的输出:

Creating network "app_default" with the default driver
Creating volume "app_todo-mysql-data" with default driver
Creating app_app_1   ... done
Creating app_mysql_1 ... done
1
2
3
4

它会自动创建 network、volume、镜像、容器,并且运行容器。

使用 docker compose down (opens new window) 停止并移除容器。

$ docker compose down
1

# compose.yml + Dockerfile

在实际的开发中,一般使用 compose.yml + Dockerfile 组合

Dockerfile 负责创建镜像,compose.yml 负责编排容器,比如下面是 React 项目经常用到的 compose.ymlDockerfile

Dockerfile

# =========================================
# Stage 1: Build the React.js Application
# =========================================
ARG NODE_VERSION=24.12.0-alpine
ARG NGINX_VERSION=alpine3.22

# Use a lightweight Node.js image for building (customizable via ARG)
FROM node:${NODE_VERSION} AS builder

# Set the working directory inside the container
WORKDIR /app

# Copy package-related files first to leverage Docker's caching mechanism
COPY package.json package-lock.json* ./

# Install project dependencies using npm ci (ensures a clean, reproducible install)
RUN --mount=type=cache,target=/root/.npm npm ci

# Copy the rest of the application source code into the container
COPY . .

# Build the React.js application (outputs to /app/dist)
RUN npm run build

# =========================================
# Stage 2: Prepare Nginx to Serve Static Files
# =========================================

FROM nginxinc/nginx-unprivileged:${NGINX_VERSION} AS runner

# Copy custom Nginx config
COPY nginx.conf /etc/nginx/nginx.conf

# Copy the static build output from the build stage to Nginx's default HTML serving directory
COPY --chown=nginx:nginx --from=builder /app/dist /usr/share/nginx/html

# Use a built-in non-root user for security best practices
USER nginx

# Expose port 8080 to allow HTTP traffic
# Note: The default NGINX container now listens on port 8080 instead of 80 
EXPOSE 8080

# Start Nginx directly with custom config
ENTRYPOINT ["nginx", "-c", "/etc/nginx/nginx.conf"]
CMD ["-g", "daemon off;"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

Dockerfile 采用多阶段构建,最终产生 dist + nginx 镜像

compose.yml

services:
  react-prod:
    build:
      context: .
    ports:
      - "8080:8080"
1
2
3
4
5
6

compose.yml 中的 build 参数 (opens new window) 告诉 Compose 如何从源代码构建应用程序,context 定义包含 Dockerfile 的目录路径

# 我的应用

# References