本节摘要:自托管协作应用模板覆盖团队最常自建的三件套:Nextcloud 私有网盘、Gitea 轻量代码托管、Portainer 容器管理面板。Nextcloud 给出与 PostgreSQL、Redis 组合的完整配置,重点讲信任域;Gitea 给出 SQLite 与 MySQL 两种形态,重点讲 SSH 端口映射;Portainer 讲 Docker socket 挂载的能力与风险边界。
Nextcloud 是自托管网盘加协作套件,单容器就能跑,但生产习惯是给它配上 PostgreSQL 和 Redis:PostgreSQL 存元数据,Redis 做缓存、锁和文件锁,性能与并发能力完全不同。
services: nextcloud: image: nextcloud:29 container_name: nextcloud restart: always ports: - "8080:80" environment: NEXTCLOUD_TRUSTED_DOMAINS: cloud.example.com localhost NEXTCLOUD_ADMIN_USER: admin NEXTCLOUD_ADMIN_PASSWORD: admin_password_2024 volumes: - nextcloud_html:/var/www/html - ./apps:/var/www/html/custom_apps depends_on: db: condition: service_healthy redis: condition: service_healthy db: image: postgres:14 restart: always environment: POSTGRES_USER: nextcloud POSTGRES_PASSWORD: nextcloud_password POSTGRES_DB: nextcloud volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U nextcloud -d nextcloud"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7 restart: always command: redis-server --requirepass redis_password volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "-a", "redis_password", "ping"] interval: 10s timeout: 5s retries: 5 volumes: nextcloud_html: db_data: redis_data:
关键行逐个说:
nextcloud_html:/var/www/html:Nextcloud 的应用代码与用户数据都在这棵目录树里。/var/www/html 挂卷是必须的,否则每次重建容器都要重新初始化;用户上传的文件存在 data 子目录下,版本升级前建议把整棵目录和数据库一起备份。NEXTCLOUD_TRUSTED_DOMAINS:信任域白名单,多个域名用空格分隔。这是 Nextcloud 第一坑:访问域名不在白名单里,页面直接报 403,提示"访问被拒绝"。换域名、换端口、加反代之后都要回来改这个变量并重建容器。NEXTCLOUD_ADMIN_USER 与 NEXTCLOUD_ADMIN_PASSWORD:首次初始化时创建管理员账号,只对空数据卷生效,和数据库的初始化规则一致。./apps:/var/www/html/custom_apps:自定义应用目录绑定挂载,离线安装插件时把应用包放进去即可。condition: service_healthy 等两边就绪,避免首启时报数据库连接失败然后进入半初始化状态。后续管理命令:进容器跑 occ 是 Nextcloud 的日常操作,比如 docker compose exec -u www-data nextcloud php occ maintenance:mode --on 开维护模式,或 php occ app:install 装应用。注意官方镜像里要加 -u www-data 以正确的属主执行,否则文件属主会乱。
离线装应用。内网环境访问不了 Nextcloud 应用商店时,插件要手工安装:从能联网的机器下载应用包,解压放进 custom_apps 挂载目录,再执行 php occ app:enable 应用名。装完后验证一下版本兼容:应用商店的版本号与 Nextcloud 主版本有对应关系,装错版本会在后台报兼容性警告,界面功能时好时坏。升级 Nextcloud 前先确认所有已装应用有对应新版本,这是升级失败的头号原因。
变体:反向代理托管:Nextcloud 走反代时,除了信任域还要配 overwrite 参数,比如 NEXTCLOUD_OVERWRITEHOST 与 NEXTCLOUD_OVERWRITEPROTOCOL,否则重定向会把用户带到容器内部端口。这个组合与 3.1 节的反代模板正好衔接。
⚠️ 升级前先备份加只读挂载:Nextcloud 大版本升级要求停服、备份数据与数据库、再换镜像 tag。升级失败最常见的原因是旧版本插件不兼容。谨慎的做法是升级前把 nextcloud_html 卷改成只读挂载并加维护模式,跑 php occ upgrade 确认无报错再恢复。
Gitea 是 Go 写的 Git 服务,内存占用小,单容器起步。最简单的形态用内置 SQLite:
services: gitea: image: gitea/gitea:1.22 container_name: gitea restart: always ports: - "3000:3000" - "2222:22" environment: GITEA__server__DOMAIN: git.example.com GITEA__server__ROOT_URL: http://git.example.com:3000 GITEA__server__SSH_PORT: 2222 GITEA__server__SSH_LISTEN_PORT: 22 volumes: - gitea_data:/data volumes: gitea_data:
GITEA__server__DOMAIN 与 GITEA__server__ROOT_URL:界面显示与克隆地址的域名。ROOT_URL 不配对的后果很隐蔽:仓库克隆按钮给出的地址是错的,用户复制下来 git clone 报错,还以为是权限问题。GITEA__server__SSH_PORT: 2222:告诉 Gitea"对用户公告的 SSH 端口是 2222",克隆地址里会带这个端口;SSH_LISTEN_PORT: 22 是容器内 sshd 实际监听的端口。两者配合,宿主机 2222 转容器 22,用户看到 2222,SSH 链接完整闭合。gitea_data:/data:仓库、数据库、配置全在这一个目录里,备份拷这个卷即可。SQLite 文件也在里面,单机规模完全够用。配 MySQL 的形态:数据量大或多实例共享时换 MySQL,环境变量加数据库连接段,服务里加 db 依赖:
services: gitea: image: gitea/gitea:1.22 environment: GITEA__database__DB_TYPE: mysql GITEA__database__HOST: db:3306 GITEA__database__NAME: gitea GITEA__database__USER: gitea GITEA__database__PASSWD: gitea_password depends_on: db: condition: service_healthy db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root_password MYSQL_DATABASE: gitea MYSQL_USER: gitea MYSQL_PASSWORD: gitea_password volumes: - db_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
切换数据库类型的时机:首次初始化时选型,之后换库要迁移工具,成本不小。个人和小团队直接 SQLite,预计用户上百人或要跑 CI 再上 MySQL,别一开始就过度设计。
⚠️ Gitea 镜像的 UID 是 1000:绑定挂载 /data 时宿主机目录属主要对齐 1000。另外 SSH 端口改动后,老用户的 known_hosts 会报警告,这是换端口的正常现象,不是攻击。
Portainer 给 Docker 提供图形界面:容器启停、镜像管理、日志查看、卷与网络管理都在浏览器里点。部署本身极其简单:
services: portainer: image: portainer/portainer-ce:2.20.0 container_name: portainer restart: always ports: - "9000:9000" - "9443:9443" volumes: - /var/run/docker.sock:/var/run/docker.sock - portainer_data:/data volumes: portainer_data:
/var/run/docker.sock 挂载是能力的来源也是风险的来源:Portainer 通过这个 socket 与 Docker daemon 通信,等于拿到了宿主机的 root 权限——容器里的进程能用 Docker API 拉起任意特权容器,进而控制宿主机。portainer_data:/data:管理数据,包括环境配置、用户账号、访问控制规则。安全边界要划清楚:第一,Portainer 界面不要直接暴露公网,挂在反代后面加认证,或者至少用 9443 的 HTTPS;第二,给团队成员分配受限用户而不是共享管理员账号;第三,Portainer 管理的机器范围要可控,能少管一台就少管一台。socket 挂载这件事本身没有替代方案,能做的是限制谁能碰到这个面板。
变体:Agent 模式管远程主机:多台机器时,每台机器跑一个 portainer agent 容器,主面板通过 agent 管理远端。agent 容器也要挂 socket,每台机器暴露一个 9001 端口给面板连。单机场景用不上,但架构上要知道有这个选项。
三件套单跑没问题,但都映射端口会让宿主机端口表越来越乱。成熟的做法是让它们共享一个反向代理和一张网络:反代按域名分流,nextcloud 走 cloud 域名、gitea 走 git 域名、portainer 走管理域名,内部服务都不再映射端口。
services: caddy: image: caddy:2.8 ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data networks: - family nextcloud: image: nextcloud:29 volumes: - nextcloud_html:/var/www/html networks: - family gitea: image: gitea/gitea:1.22 volumes: - gitea_data:/data networks: - family networks: family: driver: bridge
Caddyfile 里三行一个站点,自动申请证书:
cloud.example.com { reverse_proxy nextcloud:80 } git.example.com { reverse_proxy gitea:3000 } portainer.example.com { reverse_proxy portainer:9000 }
共享网络的收益是服务名互通:Nextcloud 后台要挂外部存储时,可以直接填 gitea:3000 这类服务名地址。端口只开 80 和 443 两个口子,管理面大幅收窄。内网没有公网域名时,Caddy 的自动证书走不通,改用 internal 模式只服务内网,或自签证书后在客户端安装信任——把这一条写进部署文档,省得同事问"为什么浏览器一直报不安全"。

Nextcloud 的缓存锁配置。加了 Redis 容器不代表 Nextcloud 自动用它,要在 config.php 里声明 memcache 服务。环境变量写法:
environment: NEXTCLOUD_MEMCACHE_LOCAL: '\\OC\\Memcache\\Redis' NEXTCLOUD_REDIS_HOST: redis NEXTCLOUD_REDIS_PASSWORD: redis_password
NEXTCLOUD_MEMCACHE_LOCAL 指向 Redis 的缓存实现,文件锁、分布式锁才真正落到 Redis 上。不配的话,多客户端并发编辑同一文件会出现锁冲突,表现为"文件被锁"的报错。数据目录的属主是 www-data,即 UID 33,绑定挂载 data 目录时先 chown 33:33,否则上传和缩略图生成都写不进。
Gitea 的备份与 Webhook。Gitea 自带备份命令,进容器一条命令打全包:
docker compose exec gitea gitea dump --config /data/gitea/conf/app.ini --file /tmp/gitea-dump.zip
gitea dump 把仓库、数据库、配置、LFS 数据打包成一个 zip,拷出容器就是完整备份;恢复时解包后按文档放回对应目录。日常迁移、换机器都靠它。Webhook 在仓库设置里配:推送、发 PR、打标签时向指定 URL 发事件,配合 CI 服务实现"推代码自动构建"。内网场景注意 Webhook 的 URL 要填 CI 服务在 compose 网络里的服务名,填公网地址反而绕一圈。
Portainer 的访问控制。管理界面里给非管理员建受限用户:他们只能操作被授权的容器和环境,看不到 Docker 的全局配置。多团队共用一台机器时,按项目建"环境组"并指派成员,避免所有人都是 root 级管理员。另外把 9000 的 HTTP 端口只绑定到回环地址,外部一律走 9443 的 HTTPS,浏览器会提示证书自签,这是正常现象,正式环境换成反代托管域名证书后提示消失。
域名与证书的落地顺序。自托管家族能不能顺畅用,一半取决于 DNS 和证书:先在 DNS 里把 cloud、git、管理三个子域名解析到机器,再起反代,最后填信任域与 ROOT_URL。Caddy 的自动证书要求域名能公网解析;纯内网环境没有公网域名,用自签证书加客户端信任,或者内网 DNS 解析加 HTTP 模式。顺序错了会有一堆"证书申请失败""重定向到奇怪地址"的连锁报错,先 DNS 后服务能省掉大半。
数据卷规划。三件套里 Nextcloud 的卷增长最快,用户文件、缩略图、版本历史都在里面;Gitea 的仓库卷看团队规模;Portainer 的卷最小。给 Nextcloud 的卷预留 1.5 倍预估空间的余量,并监控磁盘使用率——网盘类应用磁盘写满的后果比一般应用严重得多,用户上传直接失败,日志里全是写不进去的错误。
后台任务用 cron 模式。Nextcloud 的定时任务(文件扫描、过期清理)有三种模式:AJAX 模式默认开启但只在有人访问页面时才触发;cron 模式要宿主机或容器定时敲 php cron.php。人少的站 AJAX 凑合能用,人一多文件扫描会滞后,表现为"上传了但客户端半天看不到"。推荐在 compose 里加一个 command: cron -f 的旁路容器,镜像还是 nextcloud:29,挂同一个数据卷,每分钟执行一次后台任务,与 Web 容器互不干扰。
Portainer 自己的备份。管理面板的数据在 portainer_data 卷里,导出备份用界面里的 Backups 功能下载一个 zip;恢复时把 zip 放到卷里再起容器。面板数据丢了不致命——环境可以重新注册、容器照跑——但访问控制规则和用户账号要重建,趁早备份省事。备份文件里含环境地址等敏感信息,存放位置按密钥对待。
Gitea 与容器镜像的联动。Gitea 只托管代码,容器镜像可以交给配套的 Gitea Container Registry(同一镜像内集成)。仓库设置里开 Registry 功能后,docker login 用 Gitea 账号登录,镜像 tag 推到 git.example.com/项目名/镜像名,与代码同源同账号,省掉单独维护镜像仓库的精力。内网部署 CI 时,构建产物推这里,部署机从这里拉,一条链路通到底。
| 组件 | 镜像 | 界面端口 | 依赖 | 数据卷 | 核心配置 |
|---|---|---|---|---|---|
| Nextcloud | nextcloud:29 | 8080 | PostgreSQL Redis | /var/www/html | NEXTCLOUD_TRUSTED_DOMAINS |
| Gitea | gitea/gitea:1.22 | 3000 | 可选 MySQL | /data | ROOT_URL SSH 端口 |
| Portainer | portainer/portainer-ce:2.20.0 | 9000 9443 | Docker socket | /data | 管理员密码 |
💡 三个服务共用一套反代的收益,比想象中大:80 和 443 两个端口收口后,新增第四个服务只需加一条反代规则加一个网络挂载,对外暴露面、证书管理、访问日志全部集中到一处。自托管家族做大的关键不是把每个服务单独管好,而是让它们共用同一套入口和同一张网络。
到这里,七张配方卡全部备齐:从 Web 到数据库,从队列到监控,从存储到协作,覆盖了自托管场景的大半边天。下一章我们把这些模板推上生产——安全加固、资源限制、备份恢复,让"能跑"变成"能扛"。