TrueNAS docker环境下 drawio应用环境变量不生效问题的排查与修复记录

1. 问题现象

在自部署 draw.io(项目地址)中关联 Google Drive 时,Google OAuth 返回错误:

1
2
3
4
5
6
7
8
错误 400:redirect_uri_mismatch

您无法登录此应用,因为它不符合 Google 的 OAuth 2.0 政策的规定。
如果您是此应用的开发者,请在 Google Cloud Console 中注册重定向 URI。

请求详情:
redirect_uri=https://draw.example.org.com
flowName=GeneralOAuthFlow

部署环境:

1
2
3
4
5
TrueNAS SCALE
draw.io Docker 镜像:jgraph/drawio:30.3.6
外部访问域名:https://draw.example.org.com/
反向代理:Traefik
容器内部服务端口:8080

2. Google OAuth 应有配置

draw.io 外部访问地址:

1
https://draw.example.org.com/

Google Cloud Console 中 OAuth Client 类型应为:

1
Web Application

对应配置:

1
2
Authorized JavaScript origins:
https://draw.example.org.com
1
2
Authorized redirect URIs:
https://draw.example.org.com/google

draw.io 容器环境变量:

1
2
3
4
5
DRAWIO_SERVER_URL=https://draw.example.org.com/

DRAWIO_GOOGLE_CLIENT_ID=1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com
DRAWIO_GOOGLE_CLIENT_SECRET=<your-client-secret>
DRAWIO_GOOGLE_APP_ID=1026463614587

说明:

1
DRAWIO_GOOGLE_APP_ID = 就是 CLIENT_ID 中横杠前面的数字

例如:

1
2
CLIENT_ID = 1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com
APP_ID    = 1026463614587

3. 最初怀疑方向

最初看到 Google 报错:

1
redirect_uri=https://draw.example.org.com/google

因此首先检查以下内容:

  1. Google Cloud Console 中是否配置了精确的 redirect URI;
  2. OAuth Client ID 是否和 draw.io 环境变量中的 Client ID 一致;
  3. Traefik 是否正确反代到 draw.io 容器;
  4. draw.io 是否配置了正确的外部访问地址;
  5. 容器是否重启 / 重建使环境变量生效。

以上都检查后,错误仍然存在。


4. 关键发现一:线上 PreConfig.js 仍是默认内容

访问:

1
https://draw.example.org.com/js/PreConfig.js

发现返回的仍是 draw.io 默认配置:

1
2
3
4
window.DRAWIO_PUBLIC_BUILD = true;
window.EXPORT_URL = 'REPLACE_WITH_YOUR_IMAGE_SERVER';
window.DRAWIO_BASE_URL = null;
window.DRAWIO_CONFIG = null;

而正常情况下,启用 DRAWIO_SERVER_URL 和 Google OAuth 后,应该出现类似:

1
2
3
4
window.DRAWIO_SERVER_URL = 'https://draw.example.org.com/';
window.DRAWIO_BASE_URL = 'https://draw.example.org.com';
window.DRAWIO_GOOGLE_APP_ID = '1026463614587';
window.DRAWIO_GOOGLE_CLIENT_ID = '1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com';

这说明:

1
Docker 环境变量虽然配置了,但没有被 draw.io entrypoint 成功写入运行时配置文件。

5. 关键发现二:容器内 Google client 文件仍是占位值

进入容器检查:

1
2
grep -n "DRAWIO_SERVER_URL\|DRAWIO_BASE_URL\|DRAWIO_GOOGLE" \
  $CATALINA_HOME/webapps/draw/js/PreConfig.js

结果只有默认内容:

1
8:window.DRAWIO_BASE_URL = null; // Replace with path to base of deployment...

检查 Google client 文件:

1
cat $CATALINA_HOME/webapps/draw/WEB-INF/google_client_id

结果是:

1
Replace_with_your_own_google_client_id

这进一步证明:

1
DRAWIO_GOOGLE_CLIENT_ID 没有被写入 draw.io 服务端配置文件。

因此,Google OAuth 实际使用的不是预期 Client ID,而是镜像内的默认占位/旧配置,最终导致 redirect_uri_mismatch


6. 检查容器环境变量

执行:

1
env | grep -E "DRAWIO_SERVER_URL|DRAWIO_BASE_URL|DRAWIO_GOOGLE"

输出显示环境变量确实存在:

1
2
3
4
DRAWIO_GOOGLE_CLIENT_ID=1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com
DRAWIO_GOOGLE_CLIENT_SECRET=...
DRAWIO_GOOGLE_APP_ID=1026463614587
DRAWIO_SERVER_URL=https://draw.example.org.com/

因此排除:

1
环境变量没有传入容器

问题转为:

1
环境变量已传入容器,但 draw.io entrypoint 没有成功写入配置文件。

7. 检查 entrypoint 和日志

检查容器实际 entrypoint:

1
2
docker inspect <container_id> \
  --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}'

结果:

1
Entrypoint=["/docker-entrypoint.sh"] Cmd=["catalina.sh","run"]

说明 entrypoint 确实执行。

继续检查日志:

1
docker logs <container_id> | grep -E "Init PreConfig|Skipping runtime configuration|No write access|DRAWIO_GOOGLE|DRAWIO_SERVER_URL"

发现大量权限错误

1
2
3
4
/docker-entrypoint.sh: line 16: /usr/local/tomcat/webapps/draw/js/PreConfig.js: Permission denied
/docker-entrypoint.sh: line 92: /usr/local/tomcat/webapps/draw/WEB-INF/google_client_id: Permission denied
/docker-entrypoint.sh: line 93: /usr/local/tomcat/webapps/draw/WEB-INF/google_client_secret: Permission denied
/docker-entrypoint.sh: line 136: /usr/local/tomcat/webapps/draw/js/PostConfig.js: Permission denied

这说明:

1
entrypoint 执行了,但没有权限写 draw.io 的运行配置文件。

8. 检查容器用户和文件权限

容器内执行:

1
id

结果:

1
uid=1000(ubuntu) gid=999(tomcat) groups=999(tomcat),568

检查文件权限:

1
2
3
4
5
ls -ld /usr/local/tomcat
ls -ld /usr/local/tomcat/webapps
ls -ld /usr/local/tomcat/webapps/draw
ls -l /usr/local/tomcat/webapps/draw/js/PreConfig.js
ls -l /usr/local/tomcat/webapps/draw/WEB-INF/google_client_id

结果:

1
2
3
4
5
drwxr-xr-x 1 tomcat tomcat ... /usr/local/tomcat
drwxr-xr-x 1 tomcat tomcat ... /usr/local/tomcat/webapps
drwxr-xr-x 1 tomcat tomcat ... /usr/local/tomcat/webapps/draw
-rw-r--r-- 1 tomcat tomcat ... /usr/local/tomcat/webapps/draw/js/PreConfig.js
-rw-r--r-- 1 tomcat tomcat ... /usr/local/tomcat/webapps/draw/WEB-INF/google_client_id

权限含义:

1
2
目录:755
文件:644

也就是:

1
2
3
owner tomcat 可写
group tomcat 不可写
others 不可写

当前进程用户是:

1
ubuntu,uid=1000

虽然属于 tomcat 组,但 group 没有写权限,因此无法写入文件。


9. 查询镜像内 tomcat 用户

执行:

1
getent passwd tomcat

结果:

1
tomcat:x:1001:999::/usr/local/tomcat:/bin/bash

说明:

1
2
tomcat 用户 UID = 1001
tomcat 组 GID = 999

而 draw.io 的文件 owner 正是:

1
tomcat:tomcat

因此正确运行用户应为:

1
1001:999

10. 关键发现三:TrueNAS rendered compose 中 user 配置错误

通过 inspect 找到 TrueNAS 生成的 compose 文件路径:

1
docker inspect <container_id> --format '{{json .Config.Labels}}' | jq

关键字段:

1
2
3
4
5
6
{
  "com.docker.compose.project": "ix-drawio",
  "com.docker.compose.project.config_files": "/mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml",
  "com.docker.compose.project.working_dir": "/mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered",
  "com.docker.compose.service": "drawio"
}

查看 compose 文件:

1
2
3
4
cat /mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml

#输出内容
{"networks": {"ix-traefik_default": {"external": true}}, "services": {"drawio": {"cap_drop": ["ALL"], "deploy": {"resources": {"limits": {"cpus": "1", "memory": "512M"}}}, "environment": {"DRAWIO_CONFIG": "{\"defaultFonts\":[\"Helvetica\",\"Verdana\"]}", "DRAWIO_GOOGLE_APP_ID": "1026463614587", "DRAWIO_GOOGLE_CLIENT_ID": "1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com", "DRAWIO_GOOGLE_CLIENT_SECRET": "GOCSPX-。。。", "DRAWIO_SERVER_URL": "https://draw.example.org.com/", "GID": "999", "GROUP_ID": "999", "NVIDIA_VISIBLE_DEVICES": "void", "PGID": "999", "PUID": "1001", "TZ": "Etc/UTC", "UID": "1001", "UMASK": "002", "UMASK_SET": "002", "USER_ID": "1001"}, "expose": ["8080/tcp"], "group_add": [568], "healthcheck": {"interval": "30s", "retries": 5, "start_interval": "2s", "start_period": "15s", "test": ["CMD", "curl", "--request", "GET", "--silent", "--output", "/dev/null", "--show-error", "--fail", "http://127.0.0.1:8080/"], "timeout": "5s"}, "image": "jgraph/drawio:30.3.6", "labels": {" traefik.http.routers.drawio.entrypoints": "websecure", "traefik.enable": "true", "traefik.http.routers.drawio.rule": "Host(`draw.example.org.com`)", "traefik.http.routers.drawio.tls": "true", "traefik.http.services.drawio.loadbalancer.server.port": "8080"}, "networks": {"ix-traefik_default": {}}, "platform": "linux/amd64", "privileged": false, "restart": "unless-stopped", "security_opt": ["no-new-privileges=true"], "stdin_open": false, "tty": false, "user": "1000:999"}}, "x-action-required": false, "x-notes": "# Draw.io\n\n## Security\n\n**Read the following security precautions to ensure that you wish to continue using this application.**\n\n---\n\n### Container: [drawio]\n\n#### Joined networks\n\n- ix-traefik_default\n\n#### Running user/group(s)\n\n- User: 1000\n- Group: 999\n- Supplementary Groups: apps\n\n---\n\n## Bug Reports and Feature Requests\n\nIf you find a bug in this app or have an idea for a new feature, please file an issue at\nhttps://github.com/truenas/apps\n", "x-portals": []}

发现虽然环境变量里有:

1
2
3
4
5
"PUID": "1001",
"UID": "1001",
"USER_ID": "1001",
"GID": "999",
"PGID": "999"

但真正的 Docker 运行用户是:

1
"user": "1000:999"

关键点:

1
2
jgraph/drawio 镜像不依赖 PUID/PGID/UID/USER_ID 环境变量来切换运行用户。
真正决定进程用户的是 compose 里的 user 字段。

因此容器实际仍然以:

1
1000:999

运行,导致权限不足


11. 最终根因

最终根因:

1
2
3
4
5
6
7
TrueNAS 渲染出的 docker-compose.yaml 中,drawio 服务的 user 被硬编码设置为 1000:999。

但 jgraph/drawio 镜像内 Tomcat/draw.io 文件属于 tomcat:tomcat,即 1001:999,并且权限是 755/644,只有 owner tomcat 可写。

因此容器以 1000:999 运行时,虽然属于 tomcat 组,但 group 没有写权限,导致 /docker-entrypoint.sh 无法写入 PreConfig.js、PostConfig.js、google_client_id、google_client_secret 等文件。

最终 DRAWIO_GOOGLE_* 环境变量没有真正注入 draw.io 运行时配置,Google OAuth 使用默认/旧配置,触发 redirect_uri_mismatch。

修复步骤

问题原因调查清楚了,如何修复该问题呢?

既然我们已经知道了根本原因,修复起来 也不是特别复杂:

  1. 只要修改 TrueNAS 应用对应的配置文件/mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml 中的 "user": "1000:999" –> "user": "1001:999"
  2. 然后重建 这个docker 应用就可以了,既有的配置不用改。

详细操作步骤如下——

TODO 1:确认 Google OAuth 配置

在 Google Cloud Console 中确认 OAuth Client 类型为:

1
Web Application

确认配置:

1
2
Authorized JavaScript origins:
https://draw.example.org.com
1
2
Authorized redirect URIs:
https://draw.example.org.com/google

确认 Client ID:

1
1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com

TODO 2:确认 draw.io 容器环境变量

容器环境变量应包含:

1
2
3
4
DRAWIO_SERVER_URL=https://draw.example.org.com/
DRAWIO_GOOGLE_CLIENT_ID=1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com
DRAWIO_GOOGLE_CLIENT_SECRET=<your-client-secret>
DRAWIO_GOOGLE_APP_ID=1026463614587

检查命令:

1
2
3
docker exec -it <container_id> sh -lc '
env | grep -E "DRAWIO_SERVER_URL|DRAWIO_BASE_URL|DRAWIO_GOOGLE"
'

TODO 3:找到 TrueNAS 生成的 compose 文件路径

执行:

1
docker inspect <container_id> --format '{{json .Config.Labels}}' | jq

关注字段:

1
2
3
4
com.docker.compose.project
com.docker.compose.project.config_files
com.docker.compose.project.working_dir
com.docker.compose.service

本次路径为:

1
/mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml

项目名:

1
ix-drawio

服务名:

1
drawio

TODO 4:备份 rendered compose 文件

1
2
cp /mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml \
   /mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml.bak

TODO 5:修改 compose 中真正的运行用户

由于文件内容实际是 JSON,可以使用 Python 修改。

将:

1
"user": "1000:999"

改为:

1
"user": "1001:999"

执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
python3 - <<'PY'
import json
from pathlib import Path

path = Path("/mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml")

data = json.loads(path.read_text())
data["services"]["drawio"]["user"] = "1001:999"

path.write_text(json.dumps(data, ensure_ascii=False, indent=2))
print("updated user =", data["services"]["drawio"]["user"])
PY

确认:

1
grep -n '"user"' /mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml

预期:

1
"user": "1001:999"

TODO 6:用 TrueNAS rendered compose 强制重建 drawio 服务

执行:

1
2
3
4
docker compose \
  -p ix-drawio \
  -f /mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml \
  up -d --force-recreate drawio

说明:

不要在 /root 下直接执行:

1
docker compose up -d --force-recreate drawio

否则会报:

1
no configuration file provided: not found

因为 TrueNAS 的 compose 文件不在当前目录,而在 rendered 配置路径下。


TODO 7:验证新容器运行用户

查新容器:

1
docker ps | grep -i draw

检查用户:

1
docker exec -it <new_container_id> sh -lc 'id'

预期:

1
uid=1001(tomcat) gid=999(tomcat) groups=999(tomcat),568

如果仍然是:

1
uid=1000(ubuntu) gid=999(tomcat)

说明 compose 修改没有生效,或者被 TrueNAS 重新渲染覆盖。


TODO 8:验证 entrypoint 已成功写入配置

检查日志:

1
docker logs <new_container_id> | grep -E "Permission denied|Init PreConfig|DRAWIO_SERVER_URL|DRAWIO_GOOGLE"

预期:

1
2
3
4
Init PreConfig.js
window.DRAWIO_SERVER_URL = 'https://draw.example.org.com/';
window.DRAWIO_GOOGLE_APP_ID = '1026463614587';
window.DRAWIO_GOOGLE_CLIENT_ID = '1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com';

并且不应再出现:

1
Permission denied

TODO 9:验证 PreConfig.js

执行:

1
2
3
4
docker exec -it <new_container_id> sh -lc '
grep -n "DRAWIO_SERVER_URL\|DRAWIO_BASE_URL\|DRAWIO_GOOGLE" \
  $CATALINA_HOME/webapps/draw/js/PreConfig.js
'

预期输出:

1
2
3
4
window.DRAWIO_SERVER_URL = 'https://draw.example.org.com/';
window.DRAWIO_BASE_URL = 'https://draw.example.org.com';
window.DRAWIO_GOOGLE_APP_ID = '1026463614587';
window.DRAWIO_GOOGLE_CLIENT_ID = '1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com';

TODO 10:验证服务端 Google Client ID 文件

执行:

1
2
3
4
docker exec -it <new_container_id> sh -lc '
cat $CATALINA_HOME/webapps/draw/WEB-INF/google_client_id
echo
'

预期:

1
1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com

不要打印 secret 内容,只检查长度:

1
2
3
docker exec -it <new_container_id> sh -lc '
wc -c $CATALINA_HOME/webapps/draw/WEB-INF/google_client_secret
'

TODO 11:浏览器侧重新测试

建议使用无痕窗口访问:

1
https://draw.example.org.com/

或者强制刷新:

1
Ctrl + F5

然后重新执行 Google Drive 关联。

如果仍然报错,检查 Google 错误页中的:

1
2
client_id
redirect_uri

应为:

1
2
client_id=1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com
redirect_uri=https://draw.example.org.com/google

TODO 12:安全处理 Client Secret

由于 OAuth Client Secret 曾经在聊天或日志中出现过,建议修复完成后重新生成。

Google Cloud Console:

1
2
3
4
APIs & Services
→ Credentials
→ 对应 OAuth Client
→ Reset secret / 重新生成 secret

然后更新 TrueNAS / compose 中:

1
DRAWIO_GOOGLE_CLIENT_SECRET=<new-client-secret>

再重建 drawio 服务。


后续维护注意事项

1. TrueNAS rendered compose 可能被覆盖

本次修改的是:

1
/mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml

这是 TrueNAS 渲染后的 compose 文件。

后续如果发生以下操作,可能会被覆盖:

1
2
3
4
更新应用
重新保存应用配置
升级 TrueNAS App
重新部署应用

如果 draw.io Google Drive 功能以后再次失效,优先检查:

1
grep -n '"user"' /mnt/.ix-apps/app_configs/drawio/versions/1.4.27/templates/rendered/docker-compose.yaml

确认是否仍为:

1
"user": "1001:999"

如果被恢复为:

1
"user": "1000:999"

需要重新改回:

1
"user": "1001:999"

并重建服务。


2. 最佳长期方案

更长期、更稳定的方案是使用自定义 App / 自维护 compose,明确写入:

1
2
3
4
5
6
7
8
9
services:
  drawio:
    image: jgraph/drawio:30.3.6
    user: "1001:999"
    environment:
      DRAWIO_SERVER_URL: "https://draw.example.org.com/"
      DRAWIO_GOOGLE_CLIENT_ID: "1026463614587-js2p9c0nc7p6evm76vo6di4fa8.apps.googleusercontent.com"
      DRAWIO_GOOGLE_CLIENT_SECRET: "<your-client-secret>"
      DRAWIO_GOOGLE_APP_ID: "1026463614587"

这样避免 TrueNAS App 模板重新渲染时把 user 改回错误值。


最终结论

本次问题表面是 Google OAuth:

1
400 redirect_uri_mismatch

但实际根因不是 Google Cloud Console 配错,也不是 Traefik 反代问题,而是:

1
TrueNAS 渲染出的 drawio 容器 user 为 1000:999,导致 jgraph/drawio 的 docker-entrypoint.sh 没有权限写入 Tomcat/draw.io 运行配置文件。

修复方式:

1
将 compose 中 drawio 服务的 user 从硬编码的 1000:999 改为 1001:999,使容器以镜像内 tomcat 用户运行。

修复后使用正确的环境变量值:

1
2
3
4
DRAWIO_SERVER_URL
DRAWIO_GOOGLE_APP_ID
DRAWIO_GOOGLE_CLIENT_ID
DRAWIO_GOOGLE_CLIENT_SECRET

就可以成功写入tomcat 配置文件:

1
2
3
/usr/local/tomcat/webapps/draw/js/PreConfig.js
/usr/local/tomcat/webapps/draw/WEB-INF/google_client_id
/usr/local/tomcat/webapps/draw/WEB-INF/google_client_secret

最后验证下:

这回drawio 应用与Google Drive 的关联就正常了。

参考文章:

  1. 项目代码:https://github.com/jgraph/docker-drawio

  2. 项目启动程序:https://github.com/jgraph/docker-drawio/blob/dev/main/docker-entrypoint.sh

    1
    2
    3
    4
    5
    6
    7
    8
    
    if ! touch $CATALINA_HOME/webapps/draw/js/PreConfig.js 2>/dev/null; then
        echo "WARNING: No write access to $CATALINA_HOME (running as UID $(id -u), GID $(id -g))."
        echo "         Skipping runtime configuration: DRAWIO_* environment variables, SSL and the"
        echo "         context path will NOT be applied. To run as an arbitrary user, give it GID 0,"
        echo "         e.g. 'docker run --user 1234:0', compose 'group_add: [\"0\"]' or kubernetes"
        echo "         'runAsGroup: 0' / 'supplementalGroups: [0]'. See README: Running as non-root."
        exec "$@"
    fi
    
Licensed under CC BY-NC-SA 4.0