使用 Docker Compose 和 Nginx 部署前端项目

本文档介绍如何使用 Docker Compose 和 Nginx 部署前端项目(如 React、Vue 等静态网站),通过交互式一键启动脚本 (docker-init-up.sh) 实现自动化部署。方案通过 .env 文件动态配置基础镜像 (BASE_IMAGE)、主机端口 (HOST_PORT)、域名 (SERVER_NAME)、API 地址 (API_URL)、源目录 (DOCKER_DIR)、目标目录 (TARGET_DIR)、容器时区 (TZ)、容器名 (CONTAINER) 和 Docker 网络名 (NETWORKS_NAME),并为所有变量提供默认值。脚本会在 DOCKER_DIR 不为空时,将其内容拷贝到项目根目录下的 TARGET_DIR,否则直接使用 TARGET_DIR.env 文件检查位于脚本第一步,Nginx 固定使用端口 80。脚本支持交互式修改 .envnginx.confdocker-compose.yml,使用 NETWORKS_NAME 指定的外部网络(external: true,默认 common-network),避免网络冲突警告。docker-compose.ymlvolumes 配置优先使用 DOCKER_DIR,否则使用 $PWD/${TARGET_DIR:-dist},直接使用 BASE_IMAGE(默认 nginx:alpine)运行容器。API_URL 默认值为空,nginx.conflocation /api/ 块使用 #API_URL_PLACEHOLDER# 避免语法错误。脚本结束时显示容器最近 200 行日志并实时跟踪输出。参考文章:https://yan3.club/archives/cd012ac0-4e91-49a1-9149-d5e6ac95198a[](https://yan3.club/archives/cd012ac0-4e91-49a1-9149-d5e6ac95198a)

项目结构

my-frontend-project/
├── dist/                   # 前端构建后的静态文件目录(默认 TARGET_DIR)
├── nginx.conf              # Nginx 配置文件
├── docker-compose.yml      # Docker Compose 配置文件
├── .env                    # 环境变量文件
├── docker-init-up.sh       # 一键启动脚本

环境变量配置

在项目根目录下创建 .env 文件:

# 基础镜像(默认:nginx:alpine)
BASE_IMAGE=nginx:alpine

# 主机端口号(默认:8080)
HOST_PORT=8080

# Nginx 服务域名(默认:localhost)
SERVER_NAME=localhost

# 后端 API 地址(默认:空)
API_URL=

# 源目录(默认:空,表示不拷贝,直接使用 $PWD/${TARGET_DIR:-dist})
DOCKER_DIR=

# 目标目录,项目根目录下的静态文件目录(默认:dist)
TARGET_DIR=dist

# 容器时区(默认:Asia/Shanghai)
TZ=Asia/Shanghai

# 容器名(默认:frontend)
CONTAINER=frontend

# Docker 网络名(默认:common-network)
NETWORKS_NAME=common-network

说明

  • BASE_IMAGE:默认 nginx:alpine,可设置为 nginx:latest 等。
  • HOST_PORT:默认 8080,映射到容器端口 80
  • SERVER_NAME:默认 localhost
  • API_URL:默认空,空时禁用 API 代理。
  • DOCKER_DIR:默认空,指向静态文件源目录(如 /path/to/static)。
  • TARGET_DIR:默认 dist,项目根目录下的目标目录。
  • TZ:默认 Asia/Shanghai,设置容器时区。
  • CONTAINER:默认 frontend,指定容器名称。
  • NETWORKS_NAME:默认 common-network,指定外部 Docker 网络。
  • 确保 DOCKER_DIR$PWD/${TARGET_DIR:-dist} 存在(通过 npm run build 生成)。
  • 确保 NETWORKS_NAME 指定的网络已存在(可通过 docker network create common-network 创建)。
  • 建议将 .env 加入 .gitignore

修改 .env 和配置文件

脚本通过交互式输入支持修改 .env 和重新生成 nginx.confdocker-compose.yml

  1. 依次提示更新所有环境变量,用户输入新值或按回车保留默认/现有值。
  2. 询问是否重新生成 nginx.confdocker-compose.yml(选择 yn)。

示例交互:

./docker-init-up.sh
Enter BASE_IMAGE (default: nginx:alpine): [Enter]
Enter HOST_PORT (default: 8080): 9090
Enter SERVER_NAME (default: localhost): example.com
Enter API_URL (default: empty): http://api.example.com
Enter DOCKER_DIR (default: empty): /path/to/static
Enter TARGET_DIR (default: dist): static
Enter TZ (default: Asia/Shanghai): UTC
Enter CONTAINER (default: frontend): my-frontend
Enter NETWORKS_NAME (default: common-network): my-network
Regenerate nginx.conf? (y/n): y
Regenerate docker-compose.yml? (y/n): y
  • .env 修改:新值更新到 .env,回车保留现有值。
  • 配置文件修改:选择 y 重新生成,n 保留现有文件。

Nginx 配置文件

nginx.conf 支持 SPA 路由,固定端口 80API_URL 为空时使用占位符:

示例(API_URL 为空):

worker_processes 1;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;
    server_name ${SERVER_NAME:-localhost};

    # 静态文件目录
    root /usr/share/nginx/html;
    index index.html;

    # 处理 SPA 路由
    location / {
      try_files $uri $uri/ /index.html;
    }

    # 开启 gzip 压缩
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # 代理后端 API 请求(可选,禁用)
    # location /api/ {
    #   proxy_pass #API_URL_PLACEHOLDER#;
    #   proxy_set_header Host $host;
    #   proxy_set_header X-Real-IP $remote_addr;
    # }
  }
}

说明

  • listen 80:固定端口。
  • ${SERVER_NAME:-localhost}:默认 localhost
  • try_files:支持 SPA 路由。
  • API_URL
    • 空:注释 location /api/,使用 #API_URL_PLACEHOLDER#
    • 非空:未注释 location /api/proxy_pass ${API_URL}

Docker Compose 配置文件

docker-compose.yml 使用 BASE_IMAGE,配置时区、容器名、外部网络:

version: '3.8'

services:
  web:
    image: ${BASE_IMAGE:-nginx:alpine}
    container_name: ${CONTAINER:-frontend}
    ports:
      - "${HOST_PORT:-8080}:80"
    environment:
      - SERVER_NAME=${SERVER_NAME:-localhost}
      - API_URL=${API_URL:-}
      - TZ=${TZ:-Asia/Shanghai}
    volumes:
      - ${DOCKER_DIR:-$PWD/${TARGET_DIR:-dist}}:/usr/share/nginx/html
      - $PWD/nginx.conf:/etc/nginx/nginx.conf
    restart: unless-stopped
    networks:
      - custom_network

networks:
  custom_network:
    name: ${NETWORKS_NAME:-common-network}
    external: true

说明

  • image${BASE_IMAGE:-nginx:alpine}
  • container_name${CONTAINER:-frontend}
  • ports${HOST_PORT:-8080}:80
  • environment:传递 SERVER_NAMEAPI_URLTZ
  • volumes:挂载静态文件和 nginx.conf
  • networks:使用 ${NETWORKS_NAME:-common-network},标记为 external: true

一键启动脚本

docker-init-up.sh 支持交互式配置,使用外部网络:

#!/bin/bash

# 检查 .env 文件是否存在,若不存在则创建默认配置
if [ ! -f ".env" ]; then
  echo "Creating .env file..."
  cat <<EOL > .env
BASE_IMAGE=nginx:alpine
HOST_PORT=8080
SERVER_NAME=localhost
API_URL=
DOCKER_DIR=
TARGET_DIR=dist
TZ=Asia/Shanghai
CONTAINER=frontend
NETWORKS_NAME=common-network
EOL
fi

# 加载现有 .env 文件
if [ -f ".env" ]; then
  export $(cat .env | grep -v '^#' | xargs)
fi

# 设置默认值
BASE_IMAGE="${BASE_IMAGE:-nginx:alpine}"
HOST_PORT="${HOST_PORT:-8080}"
SERVER_NAME="${SERVER_NAME:-localhost}"
API_URL="${API_URL:-}"
DOCKER_DIR="${DOCKER_DIR:-}"
TARGET_DIR="${TARGET_DIR:-dist}"
TZ="${TZ:-Asia/Shanghai}"
CONTAINER="${CONTAINER:-frontend}"
NETWORKS_NAME="${NETWORKS_NAME:-common-network}"

# 函数:更新 .env 文件
update_env() {
  local key=$1
  local value=$2
  if grep -q "^${key}=" .env; then
    sed -i "s|^${key}=.*|${key}=${value}|" .env
  else
    echo "${key}=${value}" >> .env
  fi
}

# 函数:询问是否重新生成文件
prompt_regenerate() {
  local file=$1
  local prompt=$2
  echo "$prompt (y/n):"
  read response
  if [ "$response" = "y" ] || [ "$response" = "Y" ]; then
    return 0
  else
    return 1
  fi
}

# 交互式输入
echo "Enter BASE_IMAGE (default: ${BASE_IMAGE}):"
read input_base_image
[ -n "$input_base_image" ] && BASE_IMAGE="$input_base_image"
[ -n "$input_base_image" ] && update_env "BASE_IMAGE" "${BASE_IMAGE}"

echo "Enter HOST_PORT (default: ${HOST_PORT}):"
read input_host_port
[ -n "$input_host_port" ] && HOST_PORT="$input_host_port"
[ -n "$input_host_port" ] && update_env "HOST_PORT" "${HOST_PORT}"

echo "Enter SERVER_NAME (default: ${SERVER_NAME}):"
read input_server_name
[ -n "$input_server_name" ] && SERVER_NAME="$input_server_name"
[ -n "$input_server_name" ] && update_env "SERVER_NAME" "${SERVER_NAME}"

echo "Enter API_URL (default: ${API_URL:-empty}):"
read input_api_url
API_URL="$input_api_url"
update_env "API_URL" "${API_URL}"

echo "Enter DOCKER_DIR (default: ${DOCKER_DIR:-empty}):"
read input_docker_dir
[ -n "$input_docker_dir" ] && DOCKER_DIR="$input_docker_dir"
[ -n "$input_docker_dir" ] && update_env "DOCKER_DIR" "${DOCKER_DIR}"

echo "Enter TARGET_DIR (default: ${TARGET_DIR}):"
read input_target_dir
[ -n "$input_target_dir" ] && TARGET_DIR="$input_target_dir"
[ -n "$input_target_dir" ] && update_env "TARGET_DIR" "${TARGET_DIR}"

echo "Enter TZ (default: ${TZ}):"
read input_tz
[ -n "$input_tz" ] && TZ="$input_tz"
[ -n "$input_tz" ] && update_env "TZ" "${TZ}"

echo "Enter CONTAINER (default: ${CONTAINER}):"
read input_container
[ -n "$input_container" ] && CONTAINER="$input_container"
[ -n "$input_container" ] && update_env "CONTAINER" "${CONTAINER}"

echo "Enter NETWORKS_NAME (default: ${NETWORKS_NAME}):"
read input_networks_name
[ -n "$input_networks_name" ] && NETWORKS_NAME="$input_networks_name"
[ -n "$input_networks_name" ] && update_env "NETWORKS_NAME" "${NETWORKS_NAME}"

# 拷贝 DOCKER_DIR 到 TARGET_DIR
if [ -n "${DOCKER_DIR}" ]; then
  if [ ! -d "${DOCKER_DIR}" ]; then
    echo "Error: Source directory ${DOCKER_DIR} not found."
    exit 1
  fi
  echo "Copying ${DOCKER_DIR} to ${TARGET_DIR}..."
  rm -rf "${TARGET_DIR}"
  cp -r "${DOCKER_DIR}" "${TARGET_DIR}"
fi

# 检查挂载目录
VOLUME_DIR="${DOCKER_DIR:-$PWD/${TARGET_DIR:-dist}}"
if [ ! -d "${VOLUME_DIR}" ]; then
  echo "Error: Volume directory ${VOLUME_DIR} not found."
  exit 1
fi

# 生成 nginx.conf
if [ ! -f "nginx.conf" ] || prompt_regenerate "nginx.conf" "Regenerate nginx.conf?"; then
  echo "Creating/Updating nginx.conf..."
  if [ -n "${API_URL}" ]; then
    cat <<EOL > nginx.conf
worker_processes 1;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;
    server_name ${SERVER_NAME};

    # 静态文件目录
    root /usr/share/nginx/html;
    index index.html;

    # 处理 SPA 路由
    location / {
      try_files \$uri \$uri/ /index.html;
    }

    # 开启 gzip 压缩
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # 代理后端 API 请求
    location /api/ {
      proxy_pass ${API_URL};
      proxy_set_header Host \$host;
      proxy_set_header X-Real-IP \$remote_addr;
    }
  }
}
EOL
  else
    cat <<EOL > nginx.conf
worker_processes 1;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;
    server_name ${SERVER_NAME};

    # 静态文件目录
    root /usr/share/nginx/html;
    index index.html;

    # 处理 SPA 路由
    location / {
      try_files \$uri \$uri/ /index.html;
    }

    # 开启 gzip 压缩
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # 代理后端 API 请求(禁用)
    # location /api/ {
    #   proxy_pass #API_URL_PLACEHOLDER#;
    #   proxy_set_header Host \$host;
    #   proxy_set_header X-Real-IP \$remote_addr;
    # }
  }
}
EOL
  fi
fi

# 生成 docker-compose.yml
if [ ! -f "docker-compose.yml" ] || prompt_regenerate "docker-compose.yml" "Regenerate docker-compose.yml?"; then
  echo "Creating/Updating docker-compose.yml..."
  cat <<EOL > docker-compose.yml
version: '3.8'

services:
  web:
    image: ${BASE_IMAGE}
    container_name: ${CONTAINER}
    ports:
      - "${HOST_PORT}:80"
    environment:
      - SERVER_NAME=${SERVER_NAME}
      - API_URL=${API_URL}
      - TZ=${TZ}
    volumes:
      - ${DOCKER_DIR:-$PWD/${TARGET_DIR}}:/usr/share/nginx/html
      - $PWD/nginx.conf:/etc/nginx/nginx.conf
    restart: unless-stopped
    networks:
      - custom_network

networks:
  custom_network:
    name: ${NETWORKS_NAME}
    external: true
EOL
fi

# 启动 Docker Compose
echo "Starting Docker Compose..."
docker-compose up -d

# 显示容器日志
echo "Displaying logs for container ${CONTAINER}..."
docker logs -f -n 200 "${CONTAINER}"

echo "Frontend deployed successfully! Access at http://localhost:${HOST_PORT}"

说明

  • 文章适配
    • 原文章为 Java 应用部署(openjdk:8u102-jrespringboot.jar),现适配为前端 Nginx 部署(nginx:alpine)。
    • 保留交互式脚本逻辑,与文章的 docker-init-up.sh 一致(交互式修改 .env,生成配置文件)。
  • 网络配置
    • 默认 NETWORKS_NAME 改为 common-network
    • docker-compose.yml 使用 external: true,需手动创建网络(docker network create common-network)。
  • 交互式脚本
    • 支持交互式输入所有变量,更新 .env
    • 提示是否重新生成 nginx.confdocker-compose.yml
  • 功能保留
    • API_URL 空时,nginx.conf 使用 #API_URL_PLACEHOLDER#
    • volumes:静态文件和 nginx.conf 挂载。
    • TZCONTAINERNETWORKS_NAME 配置。
    • 日志显示 docker logs -f -n 200 "${CONTAINER}"

部署步骤

  1. 创建网络(若不存在):
    docker network create common-network
    
  2. 构建前端:运行 npm run build,生成 DOCKER_DIR$PWD/${TARGET_DIR}
  3. 权限
    chmod +x docker-init-up.sh
    
  4. 运行脚本
    ./docker-init-up.sh
    
  5. 交互输入
    Enter BASE_IMAGE (default: nginx:alpine): nginx:latest
    Enter HOST_PORT (default: 8080): 9090
    Enter SERVER_NAME (default: localhost): example.com
    Enter API_URL (default: empty): http://api.example.com
    Enter DOCKER_DIR (default: empty): /path/to/static
    Enter TARGET_DIR (default: dist): static
    Enter TZ (default: Asia/Shanghai): UTC
    Enter CONTAINER (default: frontend): my-frontend
    Enter NETWORKS_NAME (default: common-network): my-network
    Regenerate nginx.conf? (y/n): y
    Regenerate docker-compose.yml? (y/n): y
    
  6. 查看日志:脚本显示容器 my-frontend 的最近 200 行日志,并实时跟踪。
  7. 访问http://localhost:9090

验证和日志

  • 实时日志:脚本运行 docker logs -f -n 200 "${CONTAINER}"
  • 容器状态
    docker ps
    
  • 网络状态
    docker network ls
    

停止和清理

  • 停止
    docker-compose down
    
  • 清理(网络需手动移除):
    docker system prune
    docker network rm ${NETWORKS_NAME}
    

示例:启用 API 代理

  • 输入
    BASE_IMAGE=nginx:latest
    HOST_PORT=9090
    SERVER_NAME=example.com
    API_URL=http://api.example.com
    DOCKER_DIR=/path/to/static
    TARGET_DIR=static
    TZ=UTC
    CONTAINER=my-frontend
    NETWORKS_NAME=my-network
    Regenerate: y
    
  • 前置:运行 docker network create my-network
  • 结果:容器 my-frontendmy-network 运行,UTC 时区,API 代理启用,显示 200 行日志。

示例:无 API 代理

  • 输入
    默认值,API_URL=空,重新生成 nginx.conf
    
  • 前置:运行 docker network create common-network
  • 结果:容器 frontendcommon-network,挂载 dist,无 API 代理,显示日志。

优化建议

  • 网络检查:脚本可检查网络是否存在,提示用户创建。
  • 健康检查:添加 healthcheck
  • 变量验证:检查 TZNETWORKS_NAME
  • 非交互模式:添加 --no-interact
  • 备份:重新生成前备份文件。

注意事项

  • 运行前确保 NETWORKS_NAME 网络存在(docker network create ${NETWORKS_NAME})。
  • 确保 DOCKER_DIR$PWD/${TARGET_DIR} 存在。
  • TARGET_DIR 必须为项目根目录相对路径。
  • 生产环境建议 HTTPS。
  • 验证 BASE_IMAGEAPI_URL
  • 外部网络需手动清理(docker network rm)。