
最近把一台吃灰的群晖 DS920 翻出来干正经事——在上面部署了一套完整的 Python 数据处理与定时任务服务。最初我天真地以为在 NAS 上跑 Python 无非就是装个 Python 套件然后pip install结果第一轮就翻了车套件中心里的 Python 版本老得可怜依赖装一半直接失败代码倒是能跑起来但 DSM 一升级所有东西全废。后来老老实实切到容器化部署用 Docker 把 Python 项目完整跑起来从构建镜像、编排容器到配置自启动、挂载存储、接入群晖自带的反向代理整条链路算是彻底跑通了。这篇就是完整的部署记录给同样想在群晖 NAS 上部署 Python 项目的朋友一份能直接参考的实操流程。先声明一下我这里的硬件环境群晖 DS920系统是 DSM 7.2.1。DSM 7.2 之前的版本 Docker 套件还叫 Docker7.2 之后改名为 Container Manager功能基本一致我文中用 Container Manager 的名称来讲老版本用户自己对应一下即可。我的 Python 项目是一个 FastAPI 写的内部数据服务外加两个定时爬虫脚本整个项目代码量不大但涉及 Web 接口、SQLite 存储、日志落盘还算比较典型基本能把部署的核心问题都覆盖到。1. 环境准备从“装 Python”到“装容器运行时”很多人拿到群晖的第一反应是我能不能直接在上面装个 Python 然后跑脚本答案是能但不推荐。我第一轮就是这么干的后面踩了坑才回头走了容器这条路。这一章先把为什么选容器的逻辑讲清楚再说群晖上容器环境该怎么准备。1.1 为什么不推荐直接在群晖系统里安装 Python群晖的 DSM 本质上是一个封闭管理的 Linux 系统它有自己的套件体系、自己的权限模型、自己的文件系统布局很多东西是“托管”的不是让你随便改的。在这个基础上手动装 Python会面临几个很现实的问题第一套件中心里虽然有 Python 套件但更新节奏很慢版本经常停留在比较老的状态。我这次需要的 Python 3.11 的新特性套件中心里根本没有只能自己找第三方的源或者手动编译这本身就是一条非常折腾的路。第二一个 NAS 上通常不会只跑一个 Python 项目你很可能同时想跑爬虫、Web 服务、数据同步工具这些项目依赖的第三方库版本经常互相冲突手动管理一套系统级 Python 环境会非常痛苦。第三DSM 系统升级是一个常态操作升级后手动安装的 Python 环境经常需要重新配置有些依赖直接失效等于白部署。第四系统级部署的服务很容易被群晖自身的防火墙、权限体系限制住对外开放和端口管理都不方便。所以我的建议很明确不要在 DSM 系统层直接装 Python直接在 Container Manager 里用容器跑。容器带来的隔离性让每个项目有一套独立的依赖环境互不干扰镜像的可移植性让你换一台 NAS 或者恢复系统时只要重新拉取镜像就能还原版本可控性意味着你可以自由选择 Python 3.9、3.11 还是 3.12不用等套件中心更新清理也方便容器和镜像删掉就是删干净了不会在系统里留垃圾。1.2 群晖 Container Manager 必须知道的几个入口在套件中心搜索 Container Manager 并安装这个操作没有难度装完之后有几个界面入口是平时用得最频繁的我对照说明一下概览显示当前机器的资源使用情况以及正在运行的容器数量和健康状态平时瞄一眼就能知道 NAS 的状态。容器所有容器的管理入口可以启动、停止、重启、删除容器也可以进入容器的详情页看日志、看资源占用、改配置。映像本地已拉取和已构建的镜像列表构建镜像、拉取镜像、删除镜像都在这里操作。项目这是 Compose 项目入口对应命令行里的docker compose。如果你用 docker-compose.yml 来编排容器这里可以直接新建项目、填写 YAML 内容、一键构建和启动。我用的是这个方式后面详细讲。注册表用来搜索 Docker Hub 等镜像仓库里的镜像相当于图形化的docker search和docker pull。需要提醒的是Container Manager 的图形界面功能虽然完整但排查问题的效率远不如命令行。所以我个人的习惯是日常管理用图形界面构建、排查、看日志全都通过 SSH 进终端操作。群晖开启 SSH 的方法在“控制面板 → 终端机和 SNMP → 启用 SSH 功能”这个操作不用再多说但要注意开启后修改默认端口和强密码别把 22 端口裸奔在公网上。1.3 Python 镜像怎么选slim、alpine 还是标准版基础镜像的选择直接决定了最终镜像的体积和后续依赖安装的顺利程度。这里我踩过一个小坑最开始图体积小选了python:3.11-alpine结果项目里有一个需要编译 C 扩展的依赖alpine 用的 musl libc 跟主流 Linux 发行版的 glibc 不兼容编译报错一堆折腾半天最后还是换回了 slim 版本。我最终的选型是python:3.11-slim理由很简单slim 基于 Debian 精简版体积适中大概 150MB 左右而且保留了 glibc 和 apt绝大多数的 Python 包都有预编译的 wheel 可以直接安装省去了编译的烦恼。alpine 虽然能把镜像压到几十兆但很多依赖没有现成的 wheel遇到需要编译的场景非常痛苦。标准版python:3.11功能全但体积太大NAS 的存储空间本来就紧张没必要。拉取镜像的命令是docker pull python:3.11-slim如果拉取速度不理想可以在 Container Manager 的“注册表 → 设置”里配置镜像加速源。这个设置是全局生效的配置好之后拉取镜像的速度会有明显提升。我在实际部署中也配置了 pip 的国内软件源加速具体做法在下一章的 Dockerfile 里一起说明。2. 项目设计与 Dockerfile 编写把部署问题前置很多人写代码从来不考虑部署到了要上 NAS 才发现项目结构一团糟。这一章我把部署前必须想清楚的问题、Dockerfile 的写法、依赖管理的细节一次讲透。2.1 动手部署前必须想清楚的三件事在开始写 Dockerfile、构建镜像之前先花五分钟想清楚下面三个问题能帮你少走很多弯路。第一个问题你这个项目是常驻服务还是定时任务如果是常驻服务比如 Web API、机器人、消息队列消费者容器需要一直运行CMD 启动的是一个阻塞进程如果是定时任务比如每天跑一次爬虫、每周生成一次报表那更建议在容器里跑一次性任务或者用宿主机的 cron 定时执行docker run。我这次是两种类型都占了一个 FastAPI Web 服务需要常驻两个爬虫脚本用宿主机 crontab 定时调用容器执行这样职责清晰重启也不互相影响。第二个问题哪些数据必须持久化容器是一个临时环境里面的文件系统在容器重建后就会被清空。如果你有 SQLite 数据库、爬取的数据文件、生成的报表、日志文件这些都要通过存储卷挂载到宿主机目录否则容器一删数据全没。我这次把项目里的 data 目录和 logs 目录都挂载到了宿主机后面 3.1 小节会给出具体的目录规划。第三个问题配置信息放哪里数据库地址、API 密钥、访问密码这些敏感配置绝对不能写死在代码里。正确的做法是放到环境变量中通过 docker-compose 的 environment 字段注入。Dockerfile 里只保留代码逻辑这样代码仓库就算公开也不会泄露密钥。2.2 Dockerfile 的写法每一行都有讲究我最终的 Dockerfile 长这样先贴出来再逐行解释FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE1 \ PYTHONUNBUFFERED1 \ TZAsia/Shanghai WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . RUN useradd -m -s /bin/bash appuser \ mkdir -p /app/data /app/logs \ chown -R appuser:appuser /app USER appuser EXPOSE 8000 CMD [python, main.py]这里有几个关键点需要展开讲。首先是PYTHONDONTWRITEBYTECODE和PYTHONUNBUFFERED这两个环境变量前者禁止 Python 生成__pycache__字节码文件避免在只读层写入无意义的缓存后者让 Python 输出不经过缓冲直接打印到标准输出这样docker logs才能实时看到日志。这两个变量是 Python 容器里的标配没有的话排查问题会非常难受。其次是安装依赖的指令顺序。我把COPY requirements.txt .和RUN pip install放在复制整个项目代码之前这是利用 Docker 的层缓存机制。Docker 构建镜像时每一行指令对应一层如果这一层没有变化就会直接复用缓存。只要 requirements.txt 没有改动后面再改代码、再重新构建都不会重新安装依赖构建速度能快非常多。如果先把所有代码复制进去再装依赖只要代码目录里任何文件有变化依赖的缓存层就全部失效每次构建都要重新 pip install浪费时间还容易触发网络问题。pip install里的-i https://pypi.tuna.tsinghua.edu.cn/simple指定了清华的 PyPI 镜像源这在国内网络环境下几乎是必备的没有这一行装几十个依赖可能要等很久而且经常超时失败。--no-cache-dir参数则是禁用 pip 的本地缓存避免这些缓存文件被打进镜像里白白增大体积。然后是创建非 root 用户的操作。容器默认以 root 用户运行这在权限上是不安全的。我创建了一个名为appuser的普通用户把/app目录的所有权都交给它再通过USER appuser切换过去。这样即使容器被攻破权限也只是普通用户级别对宿主机的影响会小很多。顺便说一句这个做法还会影响挂载目录的权限后面第 5 章的常见问题里我会讲我踩过的权限坑。EXPOSE 8000只是声明容器内服务监听的端口并不会自动映射到宿主机真正的端口映射在 docker-compose 里完成。CMD [python, main.py]是容器的启动命令这里务必使用 JSON 数组格式不要用CMD python main.py这种 shell 格式因为 JSON 格式会直接作为 exec 调用信号处理更规范方便容器优雅停止。2.3 requirements.txt 的锁定与加速Python 项目的依赖管理最基本的做法就是 requirements.txt。我建议在本地开发环境里用pip freeze requirements.txt把当前环境的所有依赖版本固定下来这样在任何地方部署都能还原出一模一样的环境。如果手动写 requirements.txt一定要给关键依赖锁定版本号比如fastapi0.110.0不要只写fastapi否则不同时间构建的镜像可能依赖不同版本出了 bug 很难排查。另外有很多人会犯一个错误把pip freeze的结果直接全量导出结果包含了一堆只有本机才有的无关包。正确做法是只保留项目真正用到的依赖如果项目代码里 import 了某个库才把它写进 requirements.txt。我通常在本地新建一个干净的虚拟环境只安装项目必需的依赖再在这个环境里导 requirements.txt这样导出的清单非常干净。如果你有多个 Python 项目要部署依赖之间有版本冲突可以更进一步考虑用虚拟环境隔离但这些在容器内部已经天然解决了——每个镜像一套环境根本不需要在容器里再用 venv。3. 部署实操从代码上传到容器跑起来这一章是全文的核心实操部分。我按照自己实际部署的顺序来写先规划目录结构再上传代码然后写 docker-compose.yml再执行构建和启动最后配置反向代理。照着做就能把项目跑起来。3.1 存储目录规划与代码上传群晖的文件系统一般挂在/volume1下我习惯把所有 Docker 相关的数据统一放在/volume1/docker目录每个项目一个子目录。这次的 Python 项目目录结构如下/volume1/docker/ └── python-app/ ├── app/ # 项目代码包含 Dockerfile、main.py、requirements.txt ├── data/ # SQLite 数据库、数据文件 └── logs/ # 运行日志为什么要这么分app 目录放代码data 和 logs 目录通过存储卷挂载到容器里这样代码更新时只要重新部署 app 目录对应的镜像数据目录和日志目录完全不受影响。反过来如果有一天你删掉整个容器重新建只要 data 目录还在数据就不会丢。代码上传有三条路最简单的是用群晖的 File Station 图形界面上传直接把项目文件夹拖进去就行如果你想用命令行可以用 scp 命令上传scp -r ./python-app usernas-ip:/volume1/docker/如果代码已经存在 Git 仓库里更推荐直接 SSH 进群晖执行git clone这样以后更新代码只需要git pull非常方便。我这次用的是 git clone原因很简单后续改代码时不用重复上传整个目录也方便回滚到历史版本。3.2 docker-compose.yml部署的灵魂文件我现在部署任何容器服务第一选择都是 docker-compose而不是一个一个docker run。docker-compose 最核心的价值是把容器的全部配置写成一个 YAML 文件可版本管理、可重复执行、可一键启动和停止比手敲命令强太多了。我的docker-compose.yml长这样version: 3.8 services: python-app: build: . image: python-app:latest container_name: python-app restart: unless-stopped ports: - 8000:8000 volumes: - ./data:/app/data - ./logs:/app/logs environment: - TZAsia/Shanghai - DB_PATH/app/data/app.db - LOG_LEVELINFO logging: driver: json-file options: max-size: 10m max-file: 3逐个参数说明这些都是我用下来觉得最重要的配置。build: .表示从当前目录的 Dockerfile 构建镜像image: python-app:latest则指定构建完成后给镜像打上的名字和标签。container_name给容器取一个固定的名字方便后续用命令操作。restart: unless-stopped是重启策略这个参数必须展开讲。Docker 有四种重启策略no表示任何情况下都不自动重启on-failure表示只有容器异常退出时才重启unless-stopped表示除了手动停止容器之外其他情况都会自动重启always则表示只要 Docker 服务重启容器就跟着启动即使你手动 stop 过。对 NAS 这种 7x24 小时运行的设备我推荐unless-stopped它的语义最符合直觉你自己不想停的容器无论是断电重启、Docker 服务重启还是崩溃都会自动恢复你明确手动停止的容器它不会违背你的意愿强行启动。ports做端口映射格式是宿主机端口:容器端口。我自己的 NAS 上 8000 端口没有被占用所以直接映射到8000:8000。如果你要用群晖自带的反向代理或者 Nginx Proxy Manager 做域名访问甚至可以只映射到局域网端口然后通过反向代理转发。volumes是数据持久化的关键。我把宿主机的./data和./logs目录分别挂载到容器的/app/data和/app/logs这样容器里写的数据库和日志实际都落在宿主机上容器重建也不会丢。environment里设置应用需要的环境变量。除了时区TZAsia/Shanghai我还把数据库路径DB_PATH和日志级别LOG_LEVEL通过环境变量注入这样代码里只需要读环境变量就行不用改代码就能调整配置。最后是logging配置。容器默认的日志驱动是无限制收集运行时间一长json-file 日志文件会越来越大磁盘很快就满了。我用max-size: 10m把单个日志文件上限设为 10MBmax-file: 3表示最多保留 3 个历史日志文件超出就滚动覆盖。这个配置强烈建议每个人都加上否则哪天日志文件把 NAS 磁盘撑爆了才知道后悔。3.3 构建、启动与验证在项目目录下执行cd /volume1/docker/python-app docker compose up -d --build--build参数表示每次都重新构建镜像然后以守护模式启动容器。第一次执行会先构建镜像再创建并启动容器。构建过程中如果 Dockerfile 里有什么错误终端里会直接打印出来方便定位。构建成功后执行docker ps就能看到容器已经在运行了。验证服务是否正常我习惯按顺序做三件事第一步看容器状态docker ps确认 STATUS 是 Up第二步看日志docker logs -f python-app确认应用启动没有报错第三步在 NAS 上直接访问http://127.0.0.1:8000或者从局域网内的其他设备访问http://NAS的IP:8000看接口是否能正常返回。如果你不想进 SSH 用命令行用 Container Manager 图形界面的“项目”功能也可以完成上面的操作新增项目输入项目名称指定 docker-compose.yml 所在的路径点击构建图形界面就会自动完成构建和启动。但说实话我还是建议至少会用命令行因为后面排查问题、查看实时日志命令行效率比图形界面高太多了。3.4 外部访问与群晖反向代理配置容器跑起来之后默认只能通过IP:端口的方式访问端口号暴露在外面不美观而且如果以后迁移到不同的端口还要改一堆地方。更优雅的做法是配置反向代理用域名加标准端口来访问。群晖系统自带了反向代理功能在“控制面板 → 登录门户 → 高级 → 反向代理”里新增一条规则。我这里用一个例子说明如果打算通过python.home.example.com访问服务就填来源协议 HTTPS、来源主机名python.home.example.com、来源端口 443目的地协议 HTTP、目的地主机名localhost、目的地端口 8000。保存之后再通过群晖的证书功能给这个域名申请一张证书访问就变成 HTTPS 加密的了。这个做法的好处很明显外部访问只暴露 443 端口内部服务的真实端口不用暴露到外网多个服务可以共用 443 端口根据域名区分证书申请和续期都集成在群晖系统里省去了手动管理证书的工作量。如果你已经用了 Nginx Proxy Manager 这类容器化反向代理原理完全一样只是管理界面不同。4. 部署后的运行管理与运维细节容器跑起来只是第一步真正考验部署质量的是后续的日常运维。这一章讲清楚开机自启、日志管理、升级清理这几个关键问题。4.1 开机自启与重启策略NAS 断电后的自动恢复NAS 用户的日常是家里突然停电来电后 NAS 自动开机然后我希望上面的服务也自动恢复运行不需要我手动去点启动。restart: unless-stopped就是解决这个问题的。我在 3.2 小节简单讲过这四种策略这里补充一下实际测试的感受。我自己在配置好之后专门模拟过一次断电重启NAS 开机后打开 Container Manager容器已经自动处于运行状态应用日志显示它是在系统启动后自动拉起来的整个过程不需要任何人工干预。这也符合 NAS 作为家用服务器的一个基本要求——无人值守。还有一个小技巧如果你有多个容器之间有依赖关系比如你的 Python 应用依赖数据库容器先启动可以在 docker-compose 里用depends_on字段声明依赖顺序。不过在单机部署的场景下restart: unless-stopped已经足够覆盖绝大多数情况depends_on更多是锦上添花。4.2 日志查看与日志轮转查看容器日志最直接的方式是命令行docker logs -f python-app-f参数表示跟随输出相当于tail -f适合实时观察服务运行状态。日志会打印到标准输出和标准错误这是因为 Dockerfile 里设置了PYTHONUNBUFFERED1Python 的 print 和日志输出不会缓冲能实时看到。在 Container Manager 的容器详情页里也有一个“日志”标签页可以查看但实时性和搜索能力都不如命令行。如果日志量特别大我会直接查看挂载到宿主机的日志文件因为我事先把/app/logs挂载到了宿主机应用自己写的日志文件本身就落在宿主机上用tail -f /volume1/docker/python-app/logs/app.log查看更直观。日志轮转这块除了我在 3.2 小节 docker-compose.yml 里配置的logging驱动限制应用层也应该配合。比如用 Python 的 logging 模块时配置RotatingFileHandler按文件大小切割日志或者配置TimedRotatingFileHandler按时间切分。容器日志驱动和应用日志切割是两个层面的事情前者管容器标准输出的总量后者管应用自身写文件的大小两个都配上才最稳妥。4.3 镜像升级清理与存储空间回收代码改了之后怎么升级部署流程很简单cd /volume1/docker/python-app git pull docker compose up -d --build因为 Dockerfile 里COPY requirements.txt和RUN pip install在 COPY 代码之前如果依赖没有变化这一部分会命中缓存构建速度非常快。构建完成后docker compose up -d会检测到镜像变化自动用新镜像重建容器整个过程服务只会中断几秒钟。但是每次重新构建都会产生新的镜像旧的镜像不会被自动删除时间长了会堆积大量悬空镜像白白占用磁盘空间。我用两条命令维护docker image prune -f docker system df第一条清理所有悬空镜像也就是没有标签、没有被任何容器引用的镜像第二条查看磁盘占用情况看看镜像、容器、卷、构建缓存分别占了多少空间。NAS 的存储空间比较宝贵我建议每次升级部署后都顺手执行一次清理。如果某个容器要彻底删除在项目目录下执行docker compose down会停止并删除容器但默认不会删除数据卷所以数据是安全的。这点确实让我省心之前第一次用 docker compose 时重建了很多次容器数据都还在就是因为在 compose 文件里把数据目录挂载到了宿主机这个习惯一定要从第一天就坚持。5. 常见问题与排查心得实测踩坑全记录最后这一部分我整理了自己在群晖上部署 Python 项目的常见问题、排查思路和效率提升建议。每一个问题都是真实踩过坑之后总结出来的希望能帮你省掉一些排查时间。5.1 常见坑点速查表问题现象原因分析处理方式容器启动后立即退出启动命令报错、依赖没装全、端口被占用docker logs查看容器日志根据报错信息定位日志时间比本地时间差 8 小时容器默认时区是 UTC设置环境变量TZAsia/Shanghaislim 镜像需安装 tzdata宿主机端口被占用其他容器或套件已经占用了端口docker ps查看端口占用改掉冲突的映射或停掉占用方容器重建后数据丢失数据目录没有挂载到宿主机把 data 目录通过 volumes 挂载出来容器内写文件提示 Permission denied容器用户权限与挂载目录权限不匹配在 Dockerfile 里创建 appuser 并chown挂载目录对应路径pip 安装依赖超时默认源访问速度不理想使用国内 PyPI 镜像源如清华源镜像拉取非常慢默认的 Docker Hub 源速度不稳定在 Container Manager 注册表设置里配置镜像加速源浏览器访问不通端口映射、防火墙、路由器转发、监听地址四层问题按段排查容器内 curl、宿主机 curl、局域网访问、公网访问5.2 我踩过的三个典型问题第一个问题为什么我的 Python 环境里tzdata都不带。我最初部署的时候在 compose 文件里设置了TZAsia/Shanghai以为这样时区就对了。结果容器起来后应用日志的时间戳依然是 UTC 时间跟宿主机差了 8 个小时定时任务全都在错误的时间点触发。排查后才发现python:3.11-slim这个精简镜像里没有安装 tzdata 时区数据库设置TZ环境变量是无效的。解决方法是 Dockerfile 里加上安装 tzdata 的步骤RUN apt-get update apt-get install -y --no-install-recommends tzdata \ rm -rf /var/lib/apt/lists/*--no-install-recommends参数是防止 apt 安装多余的建议包rm -rf /var/lib/apt/lists/*删除 apt 缓存都是为了让镜像保持精简。这个问题提醒我slim 镜像虽然省空间但很多基础工具和数据文件是不带的需要在 Dockerfile 里自己补。第二个问题容器内写宿主机目录权限不够。我的爬虫脚本跑起来之后往/app/data下写文件时直接报 Permission denied。原因很简单我在 Dockerfile 里创建了appuser用户并切到了这个用户但宿主机的挂载目录/volume1/docker/python-app/data的所有者是 root 或者其他用户appuser 没有写权限。解决思路有两个。一个是在 Dockerfile 里把挂载目标目录的所有权直接交给 appuser我最终用的就是这种方式RUN mkdir -p /app/data /app/logs chown -R appuser:appuser /app这样容器里的 appuser 对挂载点有完整的读写权限。另一个方式是用环境变量指定用户 ID 和组 ID比如在 compose 文件里设置PUID1026、PGID100Dockerfile 里通过useradd -u $PUID -g $PGID创建用户让容器用户的 UID/GID 与宿主机目录属主的 UID/GID 对齐。两种方式都能解决问题看个人习惯。第三个问题明明容器在跑为什么浏览器就是不通。这个问题的排查是最典型的四层网络排查。第一层在容器内部执行curl http://127.0.0.1:8000看服务本身是否正常第二层在宿主机上执行curl http://127.0.0.1:8000看端口映射是否生效第三层从局域网的另一台设备访问http://NAS的IP:8000看群晖的防火墙是否放行第四层如果要公网访问看路由器是否做了端口转发、NAS 的防火墙是否允许外部访问。我当时卡在第二层docker ps显示端口映射正常但宿主机上 curl 不通。查了半天发现容器的启动命令监听的是0.0.0.0没问题真正的问题出在容器内的 Python 服务默认只监听了 IPv6 的::导致 IPv4 访问被拒。这类问题靠经验很难猜中最好的办法就是逐层排查每层输出结果都记录下来效率反而最高。5.3 部署效率提升的几个小建议这套流程跑通之后我又做了几件事来提升日常维护的效率都是亲测有用的。第一定时任务不要写死在容器里。我之前把爬虫脚本的循环逻辑写进了容器的主进程后来发现想改执行频率还得改代码重新构建镜像非常笨。最后改成常驻的 Web 服务保留在容器里定时爬虫脚本通过宿主机 cron 定时执行docker compose run --rm python-app python crawler.py。这样每次执行都是干净的一次性容器跑完自动删除日志输出还能被 crontab 捕获改动执行时间只需要改宿主机 cron 配置不用动镜像。第二把部署脚本化。我在宿主机上放了一个deploy.sh内容就三行git pull、docker compose up -d --build、docker image prune -f。以后升级部署只需要执行这个脚本不用每次手动敲三条命令。这个脚本我建议放到项目根目录之外的地方避免它本身被 git 管理又或者放到 git 仓库里的 scripts 目录管理看个人习惯。第三敏感信息不要写进 compose 文件。数据库密码、API 密钥这些用 compose 文件里的env_file字段引用一个独立的.env文件或者直接在群晖的“环境变量”设置里填入。.env文件不要提交到 git 仓库这样即使代码仓库泄露敏感信息也不会跟着漏出去。按这套流程折腾下来我自己的感受是群晖上跑 Python 项目真正难的不是写代码而是把环境、依赖、数据、权限、网络、自启动这些事情合理地串起来。容器化部署把一大半问题都收拢到了一个 Dockerfile 加一个 docker-compose.yml 文件里只要把这两份文件写好剩下的就是执行命令的问题。如果你手头正好有一台在吃灰的群晖完全可以按这个路径一步步试一遍第一次跑通之后后面再部署其他的 Python 服务基本就是复制改改的事。