Docker 容器化前端应用
1. 为什么前端也要 Docker
后端 Docker 是刚需(环境隔离、JVM 版本锁定)。前端构建产物是纯静态文件,貌似一个 Nginx 就够了——但容器化仍有价值:
- 一致性:开发/CI/生产用同一个镜像,跑
docker run就能重现问题 - 编排:和后端/数据库一起
docker-compose up起全套 - 部署标准化:K8s / Docker Swarm / 云厂商的容器服务直接用
- 版本快速回滚:
docker run image:v1秒切
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;"]
为什么多阶段:
- 阶段 1 的镜像有 Node.js、pnpm、源码、node_modules(几百 MB 到 1GB)
- 阶段 2 只要 Nginx + 打包后的
dist/(总共 30MB 左右) - 最终镜像只保留阶段 2,
node_modules这些都不留
层缓存:COPY package.json 和 COPY . 分开——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;
}
关键三点:
try_files—— SPA 路由必备,不加刷新 404/assets/长缓存 +index.html不缓存 —— 版本更新无缝- 基础安全头 —— 降低 XSS / Clickjacking 风险
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)。
实际选择:
- 公司标准栈有 Nginx 运维经验 →
nginx:alpine - 追求极致小 →
static-web-server - K8s / 云原生 →
nginx-unprivileged
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 # 停并删除卷(清数据库数据)
好处:
- 新人入职 clone 项目 +
docker-compose up就有完整环境 - 前后端一起迭代时不互相依赖"对方的本地环境配好了没"
- 接入数据库 / Redis / MQ 都是加一个 service
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-server 或 caddy:alpine |
| K8s 部署 | nginx-unprivileged + HEALTHCHECK |
| Mac 开发服务器生产 | docker buildx --platform linux/amd64 |
14. in4vue 要不要 Docker
当前 MVP:用 Cloudflare Pages,不需要 Docker。拉代码 + pnpm build + wrangler pages deploy 就完事。
什么时候用:
- 后续接入 Spring Boot 后端 + 要本地起全套 → 加
docker-compose.yml - 企业内部署(没有 Cloudflare/Vercel) → 前端做 Docker 镜像 + K8s 部署
学习上:写一个 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
小练习
- 按本篇第 15 节写
Dockerfile+nginx.conf+.dockerignore docker build看最终镜像大小(用docker images)- 跑起来访问
http://localhost:8080,测 SPA 路由刷新是否 200 - 写
docker-compose.yml加一个假后端(用httpd:alpine起一个) - 用
docker buildx --platform linux/amd64构建一次,上传到 Docker Hub
延伸阅读
- Docker 官方文档
- Nginx Docker 镜像
- Dockerfile 最佳实践
- static-web-server(Rust 版静态服务器)
- Caddy(另一个轻量选择,自动 HTTPS)
- Docker Compose 文档