Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
bda06ca4
Commit
bda06ca4
authored
Aug 04, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
merge: fix/compose-logging-driver → main(podman 日志驱动 + rootless 部署回退 + 停机写实)
parents
0cf8409e
5950b2fa
Pipeline
#3534
failed in 0 seconds
Changes
4
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
105 additions
and
1 deletions
+105
-1
apps/pac-service/tests/compose-logging.spec.ts
+47
-0
deploy/README.md
+10
-1
deploy/deploy-prod.sh
+26
-0
docker-compose.prod.yml
+22
-0
No files found.
apps/pac-service/tests/compose-logging.spec.ts
0 → 100644
View file @
bda06ca4
import
{
readFileSync
}
from
'node:fs'
;
import
{
join
}
from
'node:path'
;
import
*
as
yaml
from
'js-yaml'
;
/**
* compose 日志驱动契约。
*
* 【为什么显式声明 json-file】docker 默认就是 json-file(写不写行为一样),但
* **podman rootless 默认是 journald**。2026-08-04 新服务器实测:
* - `docker logs <c>` 只回一行兼容层提示
* - `podman logs <c>` 返回 0 行
* - 日志进了用户 journal,而普通用户不在 systemd-journal 组 → 读不到
* 等于线上出问题看不到日志。显式声明让两种运行时行为一致。
*
* 【顺带治既有隐患】此前无任何 rotate —— 当前生产 /var/lib/docker/containers 已 1.4 GB
* 且只增不减。2026-08-01 测试服正是被写满后 Postgres 崩溃重启(pg_wal 写不下)。
*/
const
compose
=
yaml
.
load
(
readFileSync
(
join
(
__dirname
,
'../../../docker-compose.prod.yml'
),
'utf-8'
),
)
as
{
services
:
Record
<
string
,
{
logging
?:
{
driver
?:
string
;
options
?:
Record
<
string
,
string
>
};
profiles
?:
string
[]
}
>
};
describe
(
'docker-compose.prod.yml 日志驱动'
,
()
=>
{
const
names
=
Object
.
keys
(
compose
.
services
);
test
(
'⭐ 每个服务都声明了 logging —— 漏一个,那个容器在 podman 上就看不到日志'
,
()
=>
{
const
missing
=
names
.
filter
((
n
)
=>
!
compose
.
services
[
n
]
!
.
logging
);
expect
(
missing
).
toEqual
([]);
});
test
(
'驱动是 json-file(不是 podman 默认的 journald)'
,
()
=>
{
for
(
const
n
of
names
)
{
expect
(
compose
.
services
[
n
]
!
.
logging
!
.
driver
).
toBe
(
'json-file'
);
}
});
test
(
'⭐ 带 rotate 上限 —— 否则日志只增不减,最终吃满磁盘'
,
()
=>
{
for
(
const
n
of
names
)
{
const
opts
=
compose
.
services
[
n
]
!
.
logging
!
.
options
??
{};
expect
(
opts
[
'max-size'
]).
toBeTruthy
();
expect
(
opts
[
'max-file'
]).
toBeTruthy
();
}
});
test
(
'pac-asr 仍是 profile 可选服务(默认不启,迁移时无需额外处理)'
,
()
=>
{
expect
(
compose
.
services
[
'pac-asr'
]
!
.
profiles
).
toContain
(
'asr'
);
});
});
deploy/README.md
View file @
bda06ca4
...
...
@@ -21,7 +21,16 @@ ssh <server> && cd /opt/pac
bash deploy/deploy-prod.sh
```
`git pull`
→ build → force-recreate → 验证(镜像 ID 一致 / 迁移无 pending / health 200 / web 200),任一不过即失败。只动代码,
**不碰数据**
。期间 service 秒级重启。
`git pull`
→ build → force-recreate → 验证(镜像 ID 一致 / 迁移无 pending / health 200 / web 200),任一不过即失败。只动代码,
**不碰数据**
。
⚠️
**期间有约 16 秒停机**
(2026-08-04 实测三次:15.97 / 16.86 / ~16s,0.5s 间隔探
`/health`
)。
`force-recreate`
**不是**
滚动更新 —— 它就是停旧容器、起新容器,中间必然有真空期。
build 阶段不影响服务(老容器一直跑),停机只发生在重建那一刻,所以
**部署总耗时长短与停机无关**
。
影响:那 16 秒内 API 报错、工作台加载失败,刷新即恢复;登录态不掉(JWT 在浏览器),无数据风险
(迁移由 pac-migrate 单独跑完才起 service,写操作要么已完成、要么失败可重试)。建议挑非高峰时段。
要真正零停机得引入网关层蓝绿切换(两套容器 + 上游切换),不在本脚本范围内。
> 改了 `NEXT_PUBLIC_*`(前端 build-time 变量)必须走本脚本重 build;只改后端 `.env` 用 `docker compose -f docker-compose.prod.yml restart pac-service` 即可。
...
...
deploy/deploy-prod.sh
View file @
bda06ca4
...
...
@@ -21,6 +21,7 @@
set
-euo
pipefail
log
()
{
printf
'\n\033[1;36m== %s ==\033[0m\n'
"
$*
"
;
}
warn
()
{
printf
'\033[1;33mWARN: %s\033[0m\n'
"
$*
"
>
&2
;
}
die
()
{
printf
'\033[1;31mFAIL: %s\033[0m\n'
"
$*
"
>
&2
;
exit
1
;
}
main
()
{
...
...
@@ -82,8 +83,33 @@ main() {
log
"build 镜像(显式,不和 up 混)"
"
${
COMPOSE
[@]
}
"
build
"
${
SERVICES
[@]
}
"
# ⭐ 主路径仍是直接 force-recreate —— **它在 docker 上是零停机的**。
# 2026-08-04 三组实测(0.5s 间隔探 /health,部署全程采样):
# docker + 直接 force-recreate → 停机 0.00s(535 样本全 200)
# docker + 先 stop 再 recreate → 停机 15.97s ❌
# podman + 先 stop 再 recreate → 停机 5.44s
# compose 的 force-recreate 是逐服务滚动(新容器起来再切旧的),而"先全 stop"会造出
# 真空期 —— 把零停机做成了十几秒停机。所以**不能**为了兼容 rootless 就无条件加 stop。
#
# rootless podman(运维刻意选的形态:非 root 账号 + netavark/pasta,攻击面更小)下,
# force-recreate 要删**运行中**的容器,而 rootless 杀不掉它的网络进程:
# rootless netns: kill network process: permission denied
# → 仅在此时回退:清掉失败留下的 `<hash>_<proj>-<svc>-1` 半成品容器,stop 后重建。
# 代价是那次部署有几秒停机,但只发生在 rootless 环境,docker 侧行为完全不变。
# ⚠️ 永远不退回裸 `up -d`:那正是本脚本要绕开的 compose diff 缺陷(见文件头)。
log
"force-recreate(不信 compose 的重建判定)"
if
!
"
${
COMPOSE
[@]
}
"
up
-d
--force-recreate
"
${
SERVICES
[@]
}
"
;
then
warn
"force-recreate 失败(疑似 rootless 删不掉运行中容器的网络进程),回退:清残留 → stop → 重建"
local
proj_r svc_r leftover
proj_r
=
$(
basename
"
$PWD
"
)
for
svc_r
in
"
${
SERVICES
[@]
}
"
;
do
# 失败会留下带 hash 前缀的半成品容器(如 b69b5bec_pac-pac-service-1),不清则重建报名字冲突
leftover
=
$(
docker ps
-a
--format
'{{.Names}}'
2>/dev/null |
grep
-E
"_
${
proj_r
}
-
${
svc_r
}
-1$"
||
true
)
[[
-n
"
$leftover
"
]]
&&
docker rm
-f
$leftover
>
/dev/null 2>&1
&&
log
" 清理残留容器
$leftover
"
done
"
${
COMPOSE
[@]
}
"
stop
"
${
SERVICES
[@]
}
"
||
true
"
${
COMPOSE
[@]
}
"
up
-d
--force-recreate
"
${
SERVICES
[@]
}
"
fi
# ── 部署后硬验证:任一不过 = 部署失败 ─────────────────────────────
log
"验证 1/3:容器跑的镜像 == 刚构建的镜像"
...
...
docker-compose.prod.yml
View file @
bda06ca4
...
...
@@ -17,8 +17,24 @@
#
# 内部网络:容器之间用服务名访问(postgres:5432 / redis:6379),容器内 port 用 image 默认
# ─── 日志驱动(所有服务共用)────────────────────────────────────────────
# 【为什么显式声明 json-file】docker 的默认就是 json-file,写不写行为一样;但 **podman
# rootless 默认是 journald** —— 2026-08-04 新服务器实测:`docker logs` 只回一行兼容层提示,
# `podman logs` 返回 0 行,而容器日志进了用户 journal、普通用户不在 systemd-journal 组读不到,
# 等于**线上出问题看不到日志**。显式声明让两种运行时行为一致。
#
# 【顺带治一个既有隐患】此前没有任何 rotate —— 当前生产 /var/lib/docker/containers 已 1.4 GB,
# 且只会一直涨。50m × 5 = 单容器上限 250MB,够排查近期问题,又不会把盘吃满
# (2026-08-01 测试服就是被写满后 Postgres 崩溃重启的)。
x-logging
:
&default-logging
driver
:
json-file
options
:
max-size
:
"
50m"
max-file
:
"
5"
services
:
postgres
:
logging
:
*default-logging
image
:
postgres:16-alpine
restart
:
always
env_file
:
./apps/pac-service/.env
# 读 POSTGRES_USER/PASSWORD/DB
...
...
@@ -39,6 +55,7 @@ services:
retries
:
5
redis
:
logging
:
*default-logging
image
:
redis:7-alpine
restart
:
always
command
:
[
"
redis-server"
,
"
--appendonly"
,
"
yes"
]
...
...
@@ -51,6 +68,7 @@ services:
# Runs before pac-service so the schema is current before any traffic
# hits the API. Safe to re-run — `migrate deploy` is idempotent.
pac-migrate
:
logging
:
*default-logging
build
:
context
:
.
dockerfile
:
apps/pac-service/Dockerfile
...
...
@@ -67,6 +85,7 @@ services:
command
:
[
"
npx"
,
"
prisma"
,
"
migrate"
,
"
deploy"
]
pac-service
:
logging
:
*default-logging
build
:
context
:
.
dockerfile
:
apps/pac-service/Dockerfile
...
...
@@ -97,6 +116,7 @@ services:
# 启用 → docker compose --profile asr -f docker-compose.prod.yml up -d
# 不启用时 pac-service 的 PAC_ASR_URL 指向的服务不在,transcribe 端点会失败(不影响其它功能/启动)。
pac-asr
:
logging
:
*default-logging
profiles
:
[
"
asr"
]
build
:
context
:
.
...
...
@@ -113,6 +133,7 @@ services:
mem_limit
:
2g
pac-web
:
logging
:
*default-logging
build
:
context
:
.
dockerfile
:
apps/pac-web/Dockerfile
...
...
@@ -136,6 +157,7 @@ services:
-
"
127.0.0.1:3100:3100"
pac-docs
:
logging
:
*default-logging
build
:
context
:
.
dockerfile
:
apps/pac-docs/Dockerfile
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment