Docker 容器化前端应用

1. 为什么前端也要 Docker

后端 Docker 是刚需(环境隔离、JVM 版本锁定)。前端构建产物是纯静态文件,貌似一个 Nginx 就够了——但容器化仍有价值:

Java 对照:前端的 Docker 流程 ≈ Java 的 FROM maven AS build + FROM jdk AS run 多阶段构建——编译环境和运行环境分开。


2. 标准 Dockerfile:多阶段构建

in4vue 的典型 Dockerfile:

# ========== 阶段 1: 构建 ==========
FROM node:22-alpine AS build

# pnpm (比 npm 快很多,in4vue 用的就是它)
RUN corepack enable && corepack prepare pnpm@10 --activate

WORKDIR /app

# 先复制 lock 文件,利用 Docker 层缓存
COPY pnpm-lock.yaml package.json ./
RUN pnpm install --frozen-lockfile

# 再复制源码(业务代码变动不影响依赖层缓存)
COPY . .

# 构建生产版本
RUN pnpm build

# ========== 阶段 2: 运行 ==========
FROM nginx:alpine AS runtime

# 复制 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 从构建阶段拷出 dist
COPY --from=build /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

为什么多阶段

层缓存COPY package.jsonCOPY . 分开——package.json 没变时 pnpm install 层直接命中缓存,改业务代码秒级构建。


3. Nginx 配置

nginx.conf(和 Dockerfile 同目录,会被 COPY 到容器里):

server {
    listen 80;
    server_name _;

    root /usr/share/nginx/html;
    index index.html;

    # SPA 路由回退:任何找不到的路径都返回 index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 静态资源长缓存(Vite 带 hash)
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # index.html 不缓存(新版本发布要立刻生效)
    location = /index.html {
        expires -1;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }

    # gzip 压缩
    gzip on;
    gzip_types text/css application/javascript application/json;
    gzip_min_length 1024;
    gzip_comp_level 6;

    # 基础安全头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

关键三点


4. 构建和运行

# 构建镜像
docker build -t in4vue:latest .

# 起一个容器
docker run -d --name in4vue -p 8080:80 in4vue:latest

# 看日志
docker logs -f in4vue

# 停并删
docker stop in4vue && docker rm in4vue

访问 http://localhost:8080 就是 in4vue 生产页面。


5. .dockerignore:别把 node_modules 拷进容器

.gitignore 类似:

# .dockerignore
node_modules
dist
.git
.env.local
.env.*.local
coverage
*.log
.vscode
.idea
README.md

为什么重要:没这个文件,COPY . . 会把本地的 node_modules(几百 MB)拷进构建上下文。构建速度慢 + 镜像可能"使用了错架构下装的依赖"。


6. 环境变量:构建时 vs 运行时

6.1 构建时(Vite 的常规用法)

VITE_ 前缀的变量在构建时就打进 JS。Docker 构建时通过 ARG 注入:

FROM node:22-alpine AS build
RUN corepack enable && corepack prepare pnpm@10 --activate
WORKDIR /app

ARG VITE_API_BASE_URL
ARG VITE_SENTRY_DSN

# 传给构建进程
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN

COPY pnpm-lock.yaml package.json ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

构建时:

docker build \
  --build-arg VITE_API_BASE_URL=https://api.example.com \
  --build-arg VITE_SENTRY_DSN=https://xxx@sentry.io/yyy \
  -t in4vue:prod .

缺点:不同环境(dev/staging/prod)要构建多个镜像

6.2 运行时(一个镜像多环境)

生产项目常见痛点:"这个镜像要部 test / staging / prod 三个环境,API 地址各不同"。

方案:构建时占位符,启动时替换。

index.html 里:

<script>
  window.__APP_CONFIG__ = {
    apiBase: '__API_BASE__',
    sentryDsn: '__SENTRY_DSN__',
  }
</script>

容器启动脚本 docker-entrypoint.sh

#!/bin/sh
set -e

ROOT=/usr/share/nginx/html

# 把环境变量替换进 index.html
sed -i "s|__API_BASE__|${API_BASE:-/api}|g" $ROOT/index.html
sed -i "s|__SENTRY_DSN__|${SENTRY_DSN:-}|g" $ROOT/index.html

# 启动 nginx
exec "$@"

Dockerfile 里:

COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

运行时:

docker run -d \
  -e API_BASE=https://api-prod.example.com \
  -e SENTRY_DSN=https://xxx \
  -p 8080:80 \
  in4vue:latest

前端读

// src/utils/config.ts
export const appConfig = (window as any).__APP_CONFIG__

Java 对照:类似 Spring Boot 的 @Value + 环境变量 —— 一个 jar 到处跑,配置运行时注入。


7. 镜像体积优化

默认 nginx:alpine + dist 大概 30-40 MB。能再压:

7.1 用更小的 Nginx

FROM nginxinc/nginx-unprivileged:alpine AS runtime

nginx-unprivileged 是以非 root 用户运行的版本(安全 + 适合 K8s Pod)。

7.2 Node 构建镜像瘦身

FROM node:22-alpine AS build
# 装完依赖后清 pnpm 缓存
RUN pnpm install --frozen-lockfile && pnpm store prune

(其实最终镜像里没这些,但构建快一点 / CI 缓存小)

7.3 不用 Nginx,用更小的静态服务器

FROM joseluisq/static-web-server:2-alpine
COPY --from=build /app/dist /public

static-web-server 用 Rust 写的,镜像 20 MB 不到。功能够用(支持 SPA fallback、gzip、HTTP/2)。

实际选择


8. docker-compose:前后端一起起

开发时本地起一套完整环境:

docker-compose.yml

version: '3.8'

services:
  frontend:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - '5173:80'
    environment:
      API_BASE: http://backend:8080
    depends_on:
      - backend
    networks:
      - app-net

  backend:
    image: openjdk:21-slim
    volumes:
      - ./backend/target/app.jar:/app.jar
    ports:
      - '8080:8080'
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/in4vue
      SPRING_DATASOURCE_USERNAME: app
      SPRING_DATASOURCE_PASSWORD: changeme
    command: java -jar /app.jar
    depends_on:
      - db
    networks:
      - app-net

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: in4vue
      MYSQL_USER: app
      MYSQL_PASSWORD: changeme
    volumes:
      - db-data:/var/lib/mysql
    ports:
      - '3306:3306'
    networks:
      - app-net

volumes:
  db-data:

networks:
  app-net:
docker-compose up -d     # 后台起全套
docker-compose logs -f   # 跟踪日志
docker-compose down      # 停
docker-compose down -v   # 停并删除卷(清数据库数据)

好处

Java 对照:类似 Spring 的 spring-boot:run + 手动起 MySQL,用 compose 统一编排。


9. 多架构镜像(ARM + x86)

你的 Mac M1 / M2 是 ARM64,服务器一般是 AMD64。默认 docker build 只出当前架构的镜像——传到服务器跑不起来。

解法 1:--platform

docker buildx build --platform linux/amd64 -t in4vue:prod .

解法 2:多架构

docker buildx build --platform linux/amd64,linux/arm64 -t myrepo/in4vue:latest --push .

一次构建,两种架构,推到镜像仓库。


10. 镜像仓库

10.1 Docker Hub(公共)

docker tag in4vue:latest yourname/in4vue:latest
docker push yourname/in4vue:latest

# 别人拉
docker pull yourname/in4vue:latest

公共免费。私有镜像有限额,企业项目用不起。

10.2 阿里云 ACR / 腾讯云 TCR

国内团队常用。推送前先登录:

docker login registry.cn-hangzhou.aliyuncs.com
docker tag in4vue:latest registry.cn-hangzhou.aliyuncs.com/ns/in4vue:v1
docker push registry.cn-hangzhou.aliyuncs.com/ns/in4vue:v1

10.3 GitHub Container Registry

echo $GH_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
docker tag in4vue ghcr.io/username/in4vue:latest
docker push ghcr.io/username/in4vue:latest

配合 GitHub Actions 自动推送(下一篇 CI/CD 会讲)。


11. 健康检查

K8s / Docker Swarm 里健康检查决定"要不要把流量打给这个容器":

HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
    CMD wget -q -O /dev/null http://localhost/ || exit 1

Nginx 没有专门的健康接口?直接请求首页,200 就算健康。

专业点:加一个 /health 静态文件:

RUN echo 'ok' > /usr/share/nginx/html/health
HEALTHCHECK CMD wget -q -O - http://localhost/health | grep -q ok

12. 常见坑点

现象 原因 解法
镜像巨大(超过 500MB) 没用多阶段 / 没 .dockerignore 用多阶段 + 加 dockerignore
代码改了但镜像没更新 构建层缓存命中 docker build --no-cache 或正确拆分 COPY 顺序
Windows 拷贝的脚本在容器里跑不起来 CRLF 换行 git config --global core.autocrlf input + 重拉
容器里文件权限错 Docker 里默认 root 用 nginx-unprivileged 或显式 USER
docker-compose 里前端连不上后端 localhost 用 service 名(http://backend:8080
生产环境变量没生效 ARG vs ENV 搞混 ARG 只构建时,ENV 运行时;两者都要声明
容器启动就退出 Nginx 没前台运行 CMD ["nginx", "-g", "daemon off;"]

13. 决策表

需求 方案
单纯部署一套前端 Dockerfile + Nginx
前后端一起本地起 docker-compose
一个镜像部多环境 运行时替换 __API_BASE__
镜像极致瘦身 static-web-servercaddy:alpine
K8s 部署 nginx-unprivileged + HEALTHCHECK
Mac 开发服务器生产 docker buildx --platform linux/amd64

14. in4vue 要不要 Docker

当前 MVP:用 Cloudflare Pages,不需要 Docker。拉代码 + pnpm build + wrangler pages deploy 就完事。

什么时候用

学习上:写一个 Dockerfile 放仓库里,哪怕暂时不用——未来某天要用时不慌


15. 完整的 in4vue Dockerfile 示例

贴一个能直接扔进项目根目录的 Dockerfile:

# syntax=docker/dockerfile:1.7

# ========== 构建 ==========
FROM node:22-alpine AS build

ARG VITE_API_BASE_URL=/api
ARG VITE_SENTRY_DSN=""
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN

RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app

COPY pnpm-lock.yaml package.json ./
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
    pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

# ========== 运行 ==========
FROM nginx:alpine AS runtime

RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=5s CMD wget -q -O /dev/null http://localhost/ || exit 1

CMD ["nginx", "-g", "daemon off;"]

配合 nginx.conf(第 3 节的版本)、.dockerignore(第 5 节的版本),即可:

docker build -t in4vue:latest .
docker run -d -p 8080:80 in4vue:latest

小练习

  1. 按本篇第 15 节写 Dockerfile + nginx.conf + .dockerignore
  2. docker build 看最终镜像大小(用 docker images
  3. 跑起来访问 http://localhost:8080,测 SPA 路由刷新是否 200
  4. docker-compose.yml 加一个假后端(用 httpd:alpine 起一个)
  5. docker buildx --platform linux/amd64 构建一次,上传到 Docker Hub

延伸阅读