服务编排

整体考虑编排结构如下:

├── app
   ├── compose.yml
   ├── serviceA
   ├── serviceB

这里的compose.yml作为核心,将serviceA和serviceB的docker-compose.yml使用include加入;基础的compose.yml如下:

 
include:
# 注意后面添加的服务需要在这里添加进来
 
networks:
  app_net:
    ipam:
      config:
        - subnet: "172.20.10.0/24"

Router - Traefik

相比Nginx更加简单,给出docker-compose文件:

services:
  traefik:
    image: traefik:v3.4
    container_name: traefik
    security_opt:
      - no-new-privileges:true
    networks:
      - app_net
    command:
      - "--api.insecure=false"
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=app_net"
      - "--providers.file.directory=/etc/traefik/dynamic"
      - "--entryPoints.http.address=:80"
      - "--entryPoints.https.address=:443"
      - "--entryPoints.https.http.tls=true"
      - "--entryPoints.http.http.redirections.entryPoint.to=https"
      - "--entryPoints.http.http.redirections.entryPoint.scheme=https"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./certs:/certs:ro"
      - "./dynamic:/etc/traefik/dynamic:ro"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.evalexp.top`)"
      - "traefik.http.routers.dashboard.entrypoints=https"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.tls=true"
      - "traefik.http.routers.dashboard.middlewares=dashboard-auth"
      - "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$05$$HaY5fUzjZxKwK6AqKVZyjOMLOIw.BLGtFuHHecSBRocWu6AxeLBDu"

这里需要说明的是,证书需要放置到当前目录下的certs目录中,文件的动态配置放置到dynamic文件夹下,并且添加一个tls.yaml的配置,如下:

tls:
  certificates:
    - certFile: /certs/SAN_evalexp.top.cer
      keyFile: /certs/SAN_evalexp.top.key

这样就添加了TLS证书;需要注意的是,在我们配置路由的过程中,无需指定证书,traefik会根据SNI自动匹配合适的证书。

当前结构如下:

traefik
├── certs
│   ├── SAN_evalexp.top.cer
│   └── SAN_evalexp.top.key
├── docker-compose.yml
└── dynamic
    ├── ipfs.yml
    └── tls.yaml

Blog

直接通过label让Traefik自动反向代理:

services:
  blog:
    image: registry.cn-shanghai.aliyuncs.com/evalexp-private/blog:latest
    restart: always
    hostname: blog.evalexp.top
    container_name: blog
    networks:
      app_net: 
    logging:
      options:
        max-size: "5m"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.blog.rule=Host(`blog.evalexp.top`) || Host(`evalexp.top`)"
      - "traefik.http.routers.blog.service=blog"
      - "traefik.http.services.blog.loadbalancer.server.port=80"
      - "traefik.http.routers.blog.entrypoints=https"
      - "traefik.http.routers.blog.tls=true"
 

不用额外的配置了。

OCIS

Nextcloud实在是太重了,依赖PostgreSQL以及Redis,加上本上还是PHP的,搭建Nextcloud平台好用固好用,但是始终感觉对机器性能是一个不小的考验。

OCIS是Nextcloud的一个替代品,实际就是ownCloud的下一代网盘,它可以仅以单一的二进制启动;无需关系数据库配置、无需缓存数据库配置、微服务模式、Golang高效语言,等等拥有十分多的优点;当然缺点也有,即生态和文档都不齐全。

不过相较于它的优点我觉得还是十分有必要切换成OCIS,并且切换为OCIS后我就可以使用traefik作为网关(不用再考虑php-fpm的volume映射问题),这使得我可以从Nginx的配置中抽离出来,对整体的服务编排而言十分友好。

参考例子:Discover oCIS with Docker | ownCloud

配置生成

OCIS部署首先需要先生成一个配置文件,在service/app/ocis下执行命令:

sudo touch ocis.yaml && sudo chown 1000:1000 ocis.yaml && docker run --rm -it -v $(pwd)/ocis.yaml:/etc/ocis/ocis.yaml owncloud/ocis:latest init --force-overwrite

输出应该如下:

evalexp@VM-8-6-debian:~/service/app/ocis$ sudo touch ocis.yaml && sudo chown 1000:1000 ocis.yaml && docker run --rm -it -v $(pwd)/ocis.yaml:/etc/ocis/ocis.yaml owncloud/ocis:latest init --force-overwrite
Do you want to configure Infinite Scale with certificate checking disabled?
 This is not recommended for public instances! [yes | no = default] yes
 
=========================================
 generated OCIS Config
=========================================
 configpath : /etc/ocis/ocis.yaml
 user       : admin
 password   : ???
 
 
=========================================
An older config file has been backuped to
 /etc/ocis/ocis.yaml.2025-07-31-15-15-16.backup

注意这里选择输入yes,不要检查证书;本身就是在Nginx后面的;然后里面也会有输出对应的密码,当然在配置文件中也有。

插件安装

配置文件生成后不需要修改,接下来安装核心插件GitHub - mschlachter/ocis-app-tokens: This plugin for ownCloud Infinite Scale enables a UI to create and manage app tokens, which enable third-party apps to connect to Infinite Scale.

执行命令:

mkdir plugins
curl -Lo ocis-app-tokens.zip https://github.com/mschlachter/ocis-app-tokens/releases/download/v0.0.4/ocis-app-tokens-v0.0.4.zip
unzip ocis-app-tokens.zip -d plugins/ocis-app-tokens && rm ocis-app-tokens.zip 
sudo chown -R 1000:1000 plugins

此时的目录结构应该是这样:

.
├── docker-compose.yml
├── ocis.yaml
└── plugins
    └── ocis-app-tokens
        ├── index.js
        ├── js
   └── chunks
       └── App-DwLmhITC.mjs
        └── manifest.json
 
5 directories, 5 files

docker部署

Compose如下:

services:
  ocis:
    image: owncloud/ocis:latest
    restart: unless-stopped
    container_name: ocis
    networks:
      app_net:
    logging:
      options:
        max-size: "5m"
    volumes:
      - /datapool/encryption/ocis_data:/var/lib/ocis
      - ./ocis.yaml:/etc/ocis/ocis.yaml
      - ./plugins:/web/plugins
    environment:
      OCIS_INSECURE: "true"
      OCIS_URL: "https://ocis.evalexp.top"
      OCIS_LOG_LEVEL: error
      OCIS_ADD_RUN_SERVICES: "auth-app"
      PROXY_ENABLE_APP_AUTH: "true"
      WEB_ASSET_APPS_PATH: "/web/plugins"
      PROXY_TLS: "false"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.ocis.rule=Host(`ocis.evalexp.top`)"
      - "traefik.http.routers.ocis.service=ocis"
      - "traefik.http.services.ocis.loadbalancer.server.port=9200"
      - "traefik.http.routers.ocis.entrypoints=https"
      - "traefik.http.routers.ocis.tls=true"

注意这里的环境变量都是必设的,其中OCIS_ADD_RUN_SERVICESPROXY_ENABLE_APP_AUTH是为了能够使用Auth-App进行WebDav认证,这是为了TrueNAS主动备份的,必须设置。

注意这里的数据文件夹放到了ZFS池中;数据文件的配置优先于配置文件的配置,所以如果密码错误,先看看数据文件是不是空的。

同步相关

另外,请注意使用TrueNAS同步时,WebDav地址比较复杂,必须复制粘贴,尽量不要使用/dav/files/admin/进行同步,这个Endpoint的速度比使用UUID索引的速度慢。

这个链接看起来像这样:https://ocis.evalexp.top/dav/spaces/dfd54c02-dd89-4fab-8018-a175d7252950$c812b03e-5034-49b2-866c-77358187351a

这个Endpoint也是ownCloud的默认Endpoint

IPFS Reverse Proxy

对于Traefik而言,如果不能使用Docker的Label进行自动发现,譬如,希望反向代理IPFS,这个时候我们需要添加配置,推荐使用Dynamic File Provider;以下为IPFS示例:

http:
  routers:
    ipfs:
      rule: "Host(`tools.evalexp.top`) && PathPrefix(`/ipfs`)"
      entryPoints:
        - https
      service: ipfs
      middlewares:
        - rewrite-ipfs
      tls: {}
 
  middlewares:
    rewrite-ipfs:
      replacePathRegex:
        regex: "^/ipfs/(.*)"
        replacement: "/ipfs/${1}"
 
  services:
    ipfs:
      loadBalancer:
        servers:
          - url: "https://ipfs.io"
        passHostHeader: false

这里的中间件可以直接删除,无用。

反代非本地服务

事实上上面的IPFS Reverse Proxy本身就是一个非本地服务的反向代理,并且添加了一个中间件。

放在内网环境中,如果需要代理一个https服务,但是这个TLS证书是自签名的怎么办?如果使用上面的模板,直接改一下url,会发现出现500状态码,这核心原因是由于Traefik无法验证PVE的证书导致的。

而在内网环境中,我们的证书通常由内网的CA颁发,域内的CA的根证书默认不会被Traefik信任,如果为了解决这个问题让Traefik去信任这个证书其实比较麻烦,而且是侵入了系统层面的配置,不是很推荐。

好在Traefik在http下提供了一个serversTransports子项,可以用于添加transport的相关配置,而PVE就可以通过配置transport跳过证书验证环节来使得Traefik直接信任其自签名证书:

http:
  routers:
    pve:
      rule: "Host(`pve.evalexp.top`) || Host(`pve`) || Host(`pve.home.net`)"
      entryPoints:
        - https
      service: pve
      tls: {}
 
  services:
    pve:
      loadBalancer:
        servers:
          - url: "https://192.168.31.3:8006"
        passHostHeader: false
        serversTransport: pve-transport
        
  serversTransports:
    pve-transport:
      insecureSkipVerify: true

Headscale

整体上来说把仓库中的配置文件exmaple拿下来移除TLS相关的,把对应的服务器地址改一下,对应的监听地址改一下即可。

Compose如下:

services:
  headscale:
    image: headscale/headscale
    restart: unless-stopped
    container_name: headscale
    volumes:
      - ./config.yaml:/etc/headscale/config.yaml
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtim:ro
      - /datapool/encryption/headscale_data:/var/lib/headscale
    ports:
      - "3478:3478"
      - "3478:3478/udp"
    command: serve
    networks:
      - app_net
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.headscale.rule=Host(`headscale.evalexp.top`)"
      - "traefik.http.routers.headscale.service=headscale"
      - "traefik.http.services.headscale.loadbalancer.server.port=8080"
      - "traefik.http.routers.headscale.entrypoints=https"
      - "traefik.http.routers.headscale.tls=true"
      # CORS Middleware Configuration
      - "traefik.http.middlewares.headscale-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS"
      - "traefik.http.middlewares.headscale-cors.headers.accessControlAllowHeaders=Authorization,Content-Type"
      - "traefik.http.middlewares.headscale-cors.headers.accessControlAllowOriginList=https://headscale.evalexp.top"
      - "traefik.http.middlewares.headscale-cors.headers.accessControlMaxAge=100"
      # Attach Middleware to Router
      - "traefik.http.routers.headscale.middlewares=headscale-cors"
 
  headscale-admin:
    restart: always
    container_name: headscale-admin
    image: goodieshq/headscale-admin:latest
    networks:
      - app_net
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.headscale-admin.rule=Host(`headscale.evalexp.top`) && PathPrefix(`/admin`)"
      - "traefik.http.routers.headscale-admin.service=headscale-admin"
      - "traefik.http.services.headscale-admin.loadbalancer.server.port=80"
      - "traefik.http.routers.headscale-admin.entrypoints=https"
      - "traefik.http.routers.headscale-admin.tls=true"

同时加一个cli:

#!/bin/bash
 
docker exec -it headscale headscale "$@"

注意这里还创建了一个面板;由于面板的规则更严苛会优先匹配。

用户创建

evalexp@VM-8-6-debian:~/service/app/headscale$ ./headscale-cli.sh users create evalexp
User created

API密钥创建

evalexp@VM-8-6-debian:~/service/app/headscale$ ./headscale-cli.sh apikeys create
kJejtJR.xxx-jwrz
evalexp@VM-8-6-debian:~/service/app/headscale$ ./headscale-cli.sh apikeys list
ID | Prefix  | Expiration          | Created            
1  | kJejtJR | 2025-11-25 03:50:01 | 2025-08-27 03:50:01
 

面板搭建

将上面的api密钥填到管理面板里即可使用:

800

面板可以快速帮我们创建预认证密钥等;建议在使用后删除该API密钥,提升安全性。

接入子网

接下来考虑家中的NAS接入子网,compose如下:

services:
  tailscale:
    image: ghcr.io/tailscale/tailscale:latest
    container_name: headscale
    volumes:
      - ./data:/var/lib/tailscale
    restart: always
    network_mode: host
    environment:
      - TS_HOSTNAME=subnet-router
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_AUTHKEY=???
      - TS_ROUTES=192.168.31.0/24
      - TS_EXTRA_ARGS=--login-server=https://headscale.evalexp.top

注意这里其实和官方的tailscale接入的区别只需要添加TS_EXTRA_ARGS,指定到我们的headscale即可。

接入后我们批准这个子网,通过命令方式的话:

evalexp@VM-8-6-debian:~/service/app/headscale$ ./headscale-cli.sh nodes list-routes
ID | Hostname      | Approved | Available       | Serving (Primary)
4  | subnet-router |          | 192.168.31.0/24 |    

我们可以看到接入设备ID 4,名字为subnet-router提供了192.168.31.0/24的子网接入:

evalexp@VM-8-6-debian:~/service/app/headscale$ ./headscale-cli.sh nodes approve-routes -i 4 --routes 192.168.31.0/24
Node updated
evalexp@VM-8-6-debian:~/service/app/headscale$ ./headscale-cli.sh nodes list-routes
ID | Hostname      | Approved        | Available       | Serving (Primary)
4  | subnet-router | 192.168.31.0/24 | 192.168.31.0/24 | 192.168.31.0/24  

这个时候我们就可以正常进入这个子网了。

当然用面板更快:

1200

打开即可批准此子网接入。