Skip to content

Commit 38437b1

Browse files
committed
feat: implement runtime configuration injection for VITE_API_BASE
- Add docker-entrypoint.sh to inject VITE_API_BASE at container startup - Update Dockerfile to use entrypoint script for runtime configuration - Modify api.ts to support runtime config with fallback mechanism - Add placeholder in index.html for configuration injection - Update docker-compose.yml to pass VITE_API_BASE environment variable - Support multiple injection strategies (placeholder replacement, script update, script insertion) - Ensure configuration injection failures don't prevent container startup
1 parent cb25986 commit 38437b1

9 files changed

Lines changed: 214 additions & 34 deletions

File tree

.github/workflows/develop-deploy.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ jobs:
5555
context: ./web
5656
file: ./web/Dockerfile
5757
push: true
58-
build-args: |
59-
VITE_API_BASE=${{ secrets.VITE_API_BASE || 'http://localhost:3000' }}
6058
tags: |
6159
${{ secrets.DOCKERHUB_USERNAME }}/rote-frontend:latest-dev
6260
${{ secrets.DOCKERHUB_USERNAME }}/rote-frontend:develop

.github/workflows/release-deploy.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ jobs:
5656
context: ./web
5757
file: ./web/Dockerfile
5858
push: true
59-
build-args: |
60-
VITE_API_BASE=${{ secrets.VITE_API_BASE || 'http://localhost:3000' }}
6159
tags: |
6260
${{ secrets.DOCKERHUB_USERNAME }}/rote-frontend:latest
6361
${{ secrets.DOCKERHUB_USERNAME }}/rote-frontend:${{ github.event.release.tag_name || github.ref_name || 'main' }}

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,30 @@
88

99
## Deploy
1010

11-
To Be Continue...
11+
### 快速开始
1212

13+
#### 方式一:使用 Docker Hub 镜像(推荐)
14+
15+
```bash
16+
# 使用最新版本(默认配置文件)
17+
docker-compose up -d
18+
19+
# 使用特定版本
20+
IMAGE_TAG=v1.0.0 docker-compose up -d
21+
```
22+
23+
#### 方式二:本地构建
24+
25+
```bash
26+
# 从源码构建并启动
27+
# VITE_API_BASE 在构建时注入到前端代码中(可选,默认 http://localhost:3000)
28+
VITE_API_BASE=http://localhost:3000 docker-compose -f docker-compose.build.yml up -d --build
29+
```
30+
31+
### 详细说明
32+
33+
更多部署选项和配置说明,请查看 [Docker Compose 使用指南](doc/userguide/DOCKER-COMPOSE-GUIDE.md)
1334

1435
## Technology stack
36+
1537
![Frame 1](https://github.com/Rabithua/Rote/assets/34543831/fc00f797-82bc-47fe-8c75-36ea0b1f6f76)

docker-compose.build.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
services:
2+
rote-backend:
3+
build:
4+
context: ./server
5+
environment:
6+
# 基础配置 - 必需
7+
- POSTGRESQL_URL=postgresql://prisma:${POSTGRES_PASSWORD:-rote_password_123}@postgres:5432/rote
8+
ports:
9+
- "3000:3000"
10+
depends_on:
11+
- postgres
12+
restart: unless-stopped
13+
14+
rote-frontend:
15+
build:
16+
context: ./web
17+
args:
18+
# 构建时传递 VITE_API_BASE,会在构建时注入到前端代码中
19+
# 如果未设置,将使用默认值 http://localhost:3000
20+
- VITE_API_BASE=${VITE_API_BASE:-http://localhost:3000}
21+
ports:
22+
- "3001:3001"
23+
depends_on:
24+
- rote-backend
25+
restart: unless-stopped
26+
postgres:
27+
image: postgres:17
28+
container_name: rote-postgres
29+
restart: unless-stopped
30+
ports:
31+
- "5432:5432"
32+
environment:
33+
POSTGRES_USER: prisma
34+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rote_password_123}
35+
POSTGRES_DB: rote
36+
volumes:
37+
- rote-postgres-data:/var/lib/postgresql/data
38+
39+
volumes:
40+
rote-postgres-data:

docker-compose.yml

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,57 @@
1+
# Docker Compose configuration using pre-built images from Docker Hub
2+
# Usage: docker-compose up -d
3+
#
4+
# To use a specific version, set IMAGE_TAG environment variable:
5+
# IMAGE_TAG=v1.0.0 docker-compose up -d
6+
#
7+
# Available tags:
8+
# - latest: Latest stable release
9+
# - main: Latest from main branch
10+
# - develop: Latest from develop branch
11+
# - v1.0.0: Specific version (if released)
12+
113
services:
214
rote-backend:
3-
build:
4-
context: ./server
15+
image: rabithua/rote-backend:${IMAGE_TAG:-latest}
16+
container_name: rote-backend
517
environment:
618
# 基础配置 - 必需
719
- POSTGRESQL_URL=postgresql://prisma:${POSTGRES_PASSWORD:-rote_password_123}@postgres:5432/rote
820
ports:
921
- "3000:3000"
1022
depends_on:
11-
- postgres
23+
postgres:
24+
condition: service_healthy
25+
healthcheck:
26+
test:
27+
[
28+
"CMD-SHELL",
29+
'node -e "require(''http'').get(''http://localhost:3000/v2/api/health'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)}).on(''error'', () => process.exit(1))" || exit 1',
30+
]
31+
interval: 10s
32+
timeout: 5s
33+
retries: 5
34+
start_period: 30s
1235
restart: unless-stopped
1336

1437
rote-frontend:
15-
build:
16-
context: ./web
17-
args:
18-
# 构建时传递 VITE_API_BASE,如果未设置则使用默认值
19-
- VITE_API_BASE=${VITE_API_BASE:-http://localhost:3000}
38+
image: rabithua/rote-frontend:${IMAGE_TAG:-latest}
39+
container_name: rote-frontend
2040
ports:
2141
- "3001:3001"
2242
depends_on:
23-
- rote-backend
43+
rote-backend:
44+
condition: service_healthy
45+
environment:
46+
- VITE_API_BASE=${VITE_API_BASE:-http://localhost:3000}
47+
healthcheck:
48+
test: ["CMD-SHELL", "curl -f http://localhost:3001 || exit 1"]
49+
interval: 10s
50+
timeout: 5s
51+
retries: 5
52+
start_period: 10s
2453
restart: unless-stopped
54+
2555
postgres:
2656
image: postgres:17
2757
container_name: rote-postgres
@@ -34,6 +64,12 @@ services:
3464
POSTGRES_DB: rote
3565
volumes:
3666
- rote-postgres-data:/var/lib/postgresql/data
67+
healthcheck:
68+
test: ["CMD-SHELL", "pg_isready -U prisma -d rote"]
69+
interval: 10s
70+
timeout: 5s
71+
retries: 5
72+
start_period: 10s
3773

3874
volumes:
3975
rote-postgres-data:

web/Dockerfile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ RUN rm -rf node_modules && bun install --force
2323
COPY . .
2424

2525
# 构建前端静态文件
26-
# 使用 ARG 接收构建参数,然后设置为环境变量供构建时使用
26+
# VITE_API_BASE 在构建时通过 ARG 传入并注入到代码中
2727
ARG VITE_API_BASE=http://localhost:3000
2828
ENV VITE_API_BASE=${VITE_API_BASE}
2929
RUN bun run build
@@ -47,6 +47,11 @@ COPY --from=builder /app/vite.config.ts ./
4747
# 安装所有依赖(vite preview 需要 vite,它在 devDependencies 中)
4848
RUN bun install
4949

50-
# 直接用 Vite 预览模式启动(适合开发/预览环境)
50+
# 从构建上下文复制启动脚本并设置执行权限
51+
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
52+
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
53+
54+
# 使用启动脚本作为入口点,在启动时注入环境变量
5155
EXPOSE 3001
56+
ENTRYPOINT ["docker-entrypoint.sh"]
5257
CMD ["bun", "run", "preview"]

web/docker-entrypoint.sh

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/bin/sh
2+
# Docker 容器启动脚本
3+
# 在容器启动时注入环境变量到 HTML 文件中
4+
5+
# 默认值
6+
VITE_API_BASE="${VITE_API_BASE:-http://localhost:3000}"
7+
8+
# 替换 HTML 中的占位符(配置注入失败不应阻止容器启动)
9+
if [ -f /app/dist/index.html ]; then
10+
# 转义 sed 替换字符串中的特殊字符(& 和 \)
11+
# 使用 | 作为分隔符避免与 URL 中的 / 冲突
12+
# 先转义 \,再转义 &
13+
ESCAPED_API_BASE=$(echo "${VITE_API_BASE}" | sed 's/\\/\\\\/g; s/&/\\&/g' 2>/dev/null || echo "${VITE_API_BASE}")
14+
15+
# 转义 JSON 字符串中的特殊字符(用于 JavaScript 代码)
16+
JSON_ESCAPED_API_BASE=$(echo "${VITE_API_BASE}" | sed 's/\\/\\\\/g; s/"/\\"/g' 2>/dev/null || echo "${VITE_API_BASE}")
17+
18+
# 首先尝试替换占位符(如果存在)
19+
if grep -q "__VITE_API_BASE_PLACEHOLDER__" /app/dist/index.html 2>/dev/null; then
20+
# 使用 sed 替换占位符(Alpine/busybox 兼容)
21+
if sed -i "s|__VITE_API_BASE_PLACEHOLDER__|${ESCAPED_API_BASE}|g" /app/dist/index.html 2>/dev/null; then
22+
echo "✓ 已注入 VITE_API_BASE: ${VITE_API_BASE}"
23+
elif sed "s|__VITE_API_BASE_PLACEHOLDER__|${ESCAPED_API_BASE}|g" /app/dist/index.html > /tmp/index.html.tmp 2>/dev/null && \
24+
mv /tmp/index.html.tmp /app/dist/index.html 2>/dev/null; then
25+
echo "✓ 已注入 VITE_API_BASE: ${VITE_API_BASE}"
26+
else
27+
echo "⚠ 警告: 占位符替换失败,尝试插入配置脚本"
28+
# 如果替换失败,尝试在 </head> 前插入配置脚本
29+
CONFIG_SCRIPT="<script>window.__ROTE_CONFIG__=window.__ROTE_CONFIG__||{};window.__ROTE_CONFIG__.VITE_API_BASE=\"${JSON_ESCAPED_API_BASE}\";</script>"
30+
if sed -i "s|</head>|${CONFIG_SCRIPT}</head>|" /app/dist/index.html 2>/dev/null || \
31+
sed "s|</head>|${CONFIG_SCRIPT}</head>|" /app/dist/index.html > /tmp/index.html.tmp 2>/dev/null && \
32+
mv /tmp/index.html.tmp /app/dist/index.html 2>/dev/null; then
33+
echo "✓ 已插入配置脚本,VITE_API_BASE: ${VITE_API_BASE}"
34+
else
35+
echo "⚠ 警告: 配置注入失败,将使用构建时配置或默认值"
36+
fi
37+
fi
38+
else
39+
# 如果占位符不存在,检查是否已有配置脚本
40+
if grep -q "__ROTE_CONFIG__" /app/dist/index.html 2>/dev/null; then
41+
# 如果已有配置脚本,尝试更新它
42+
if sed -i "s|\"VITE_API_BASE\":\"[^\"]*\"|\"VITE_API_BASE\":\"${JSON_ESCAPED_API_BASE}\"|g" /app/dist/index.html 2>/dev/null || \
43+
sed "s|\"VITE_API_BASE\":\"[^\"]*\"|\"VITE_API_BASE\":\"${JSON_ESCAPED_API_BASE}\"|g" /app/dist/index.html > /tmp/index.html.tmp 2>/dev/null && \
44+
mv /tmp/index.html.tmp /app/dist/index.html 2>/dev/null; then
45+
echo "✓ 已更新配置脚本,VITE_API_BASE: ${VITE_API_BASE}"
46+
else
47+
echo "⚠ 警告: 配置脚本更新失败,将使用构建时配置或默认值"
48+
fi
49+
else
50+
# 如果既没有占位符也没有配置脚本,在 </head> 前插入配置脚本
51+
CONFIG_SCRIPT="<script>window.__ROTE_CONFIG__=window.__ROTE_CONFIG__||{};window.__ROTE_CONFIG__.VITE_API_BASE=\"${JSON_ESCAPED_API_BASE}\";</script>"
52+
if sed -i "s|</head>|${CONFIG_SCRIPT}</head>|" /app/dist/index.html 2>/dev/null || \
53+
sed "s|</head>|${CONFIG_SCRIPT}</head>|" /app/dist/index.html > /tmp/index.html.tmp 2>/dev/null && \
54+
mv /tmp/index.html.tmp /app/dist/index.html 2>/dev/null; then
55+
echo "✓ 已插入配置脚本,VITE_API_BASE: ${VITE_API_BASE}"
56+
else
57+
echo "⚠ 警告: 配置注入失败,将使用构建时配置或默认值"
58+
fi
59+
fi
60+
fi
61+
else
62+
echo "⚠ 警告: /app/dist/index.html 不存在,跳过配置注入"
63+
fi
64+
65+
# 执行原始命令(使用 set -e 确保命令失败时容器退出)
66+
set -e
67+
exec "$@"
68+

web/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
<script>
3434
iosPWASplash('logo.png', '#ffffff');
3535
</script>
36+
<!-- 运行时配置注入:容器启动脚本会替换此处的占位符 -->
37+
<script>
38+
window.__ROTE_CONFIG__ = window.__ROTE_CONFIG__ || {
39+
VITE_API_BASE: '__VITE_API_BASE_PLACEHOLDER__',
40+
};
41+
</script>
3642
<!-- Umami -->
3743
<script
3844
defer

web/src/utils/api.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,41 @@ const API_PATH = '/v2/api';
66

77
/**
88
* 获取 API 基础 URL
9-
* 处理构建时可能为 undefined 的情况,确保始终返回有效的 URL
9+
* 优先级:运行时配置 > 构建时配置 > 默认值
10+
* 支持通过 window.__ROTE_CONFIG__ 在运行时注入配置
1011
*/
1112
const getApiPoint = (): string => {
12-
// Vite 使用 import.meta.env 访问环境变量(以 VITE_ 开头的变量会自动暴露)
13-
const apiBase = import.meta.env.VITE_API_BASE;
1413
const defaultValue = 'http://localhost:3000';
1514

16-
// 如果 VITE_API_BASE 未设置或无效,使用默认值
17-
if (!apiBase) {
18-
if (import.meta.env.DEV) {
19-
// eslint-disable-next-line no-console
20-
console.warn('[api.ts] VITE_API_BASE is not set, using default:', defaultValue);
15+
// 优先读取运行时配置(从 window 对象,由容器启动脚本注入)
16+
const runtimeConfig = (window as any).__ROTE_CONFIG__;
17+
if (runtimeConfig?.VITE_API_BASE) {
18+
const runtimeApiBase = String(runtimeConfig.VITE_API_BASE).trim();
19+
if (
20+
runtimeApiBase &&
21+
runtimeApiBase !== 'undefined' &&
22+
runtimeApiBase !== 'null' &&
23+
runtimeApiBase !== ''
24+
) {
25+
return runtimeApiBase;
2126
}
22-
return defaultValue;
2327
}
2428

25-
// 确保 apiBase 是字符串类型并验证有效性
26-
const apiBaseStr = String(apiBase).trim();
27-
28-
if (apiBaseStr === 'undefined' || apiBaseStr === 'null' || apiBaseStr === '') {
29-
if (import.meta.env.DEV) {
30-
// eslint-disable-next-line no-console
31-
console.warn('[api.ts] VITE_API_BASE is invalid, using default:', defaultValue);
29+
// 其次读取构建时配置(Vite 环境变量)
30+
const apiBase = import.meta.env.VITE_API_BASE;
31+
if (apiBase) {
32+
const apiBaseStr = String(apiBase).trim();
33+
if (apiBaseStr !== 'undefined' && apiBaseStr !== 'null' && apiBaseStr !== '') {
34+
return apiBaseStr;
3235
}
33-
return defaultValue;
3436
}
3537

36-
return apiBaseStr;
38+
// 如果都未设置或无效,使用默认值
39+
if (import.meta.env.DEV) {
40+
// eslint-disable-next-line no-console
41+
console.warn('[api.ts] VITE_API_BASE is not set, using default:', defaultValue);
42+
}
43+
return defaultValue;
3744
};
3845

3946
export const API_POINT = getApiPoint();

0 commit comments

Comments
 (0)