使用 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
。脚本支持交互式修改 .env
、nginx.conf
和 docker-compose.yml
,使用 NETWORKS_NAME
指定的外部网络(external: true
,默认 common-network
),避免网络冲突警告。docker-compose.yml
的 volumes
配置优先使用 DOCKER_DIR
,否则使用 $PWD/${TARGET_DIR:-dist}
,直接使用 BASE_IMAGE
(默认 nginx:alpine
)运行容器。API_URL
默认值为空,nginx.conf
中 location /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.conf
、docker-compose.yml
:
- 依次提示更新所有环境变量,用户输入新值或按回车保留默认/现有值。
- 询问是否重新生成
nginx.conf
和docker-compose.yml
(选择y
或n
)。
示例交互:
./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 路由,固定端口 80
,API_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_NAME
、API_URL
、TZ
。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-jre
,springboot.jar
),现适配为前端 Nginx 部署(nginx:alpine
)。 - 保留交互式脚本逻辑,与文章的
docker-init-up.sh
一致(交互式修改.env
,生成配置文件)。
- 原文章为 Java 应用部署(
- 网络配置:
- 默认
NETWORKS_NAME
改为common-network
。 docker-compose.yml
使用external: true
,需手动创建网络(docker network create common-network
)。
- 默认
- 交互式脚本:
- 支持交互式输入所有变量,更新
.env
。 - 提示是否重新生成
nginx.conf
和docker-compose.yml
。
- 支持交互式输入所有变量,更新
- 功能保留:
API_URL
空时,nginx.conf
使用#API_URL_PLACEHOLDER#
。volumes
:静态文件和nginx.conf
挂载。TZ
、CONTAINER
、NETWORKS_NAME
配置。- 日志显示
docker logs -f -n 200 "${CONTAINER}"
。
部署步骤
- 创建网络(若不存在):
docker network create common-network
- 构建前端:运行
npm run build
,生成DOCKER_DIR
或$PWD/${TARGET_DIR}
。 - 权限:
chmod +x docker-init-up.sh
- 运行脚本:
./docker-init-up.sh
- 交互输入:
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
- 查看日志:脚本显示容器
my-frontend
的最近 200 行日志,并实时跟踪。 - 访问:
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-frontend
在my-network
运行,UTC
时区,API 代理启用,显示 200 行日志。
示例:无 API 代理
- 输入:
默认值,API_URL=空,重新生成 nginx.conf
- 前置:运行
docker network create common-network
。 - 结果:容器
frontend
在common-network
,挂载dist
,无 API 代理,显示日志。
优化建议
- 网络检查:脚本可检查网络是否存在,提示用户创建。
- 健康检查:添加
healthcheck
。 - 变量验证:检查
TZ
、NETWORKS_NAME
。 - 非交互模式:添加
--no-interact
。 - 备份:重新生成前备份文件。
注意事项
- 运行前确保
NETWORKS_NAME
网络存在(docker network create ${NETWORKS_NAME}
)。 - 确保
DOCKER_DIR
或$PWD/${TARGET_DIR}
存在。 TARGET_DIR
必须为项目根目录相对路径。- 生产环境建议 HTTPS。
- 验证
BASE_IMAGE
和API_URL
。 - 外部网络需手动清理(
docker network rm
)。