用 Sablier + Nginx 实现容器按需启停,闲置自动回收内存

用 Sablier + Nginx 实现容器按需启停,闲置自动回收内存
Photo by Rubaitul Azad / Unsplash

方案概述

Sablier 是一个开源的容器按需启动工具,配合 nginx 的 auth_request 认证代理机制,可以实现"请求到达时自动拉起容器,空闲后自动关闭"的完整闭环。核心原理如下:

  1. 为每个需要按需启动的目标容器添加 sablier.enable=truesablier.group=<组名> 标签,sablier 通过挂载的 /var/run/docker.sock 实时感知并管理这些容器。
  2. nginx 在访问目标服务前,先向 sablier 的 /api/strategies/blocking 接口发起一个内部认证子请求(auth_request)。
  3. sablier 收到请求后,检查对应组名的容器是否已运行:
    • 若未运行,则立即通过 Docker API 启动该组内所有容器,并在容器就绪后返回 200 OK,此时 nginx 将原始请求放行,转发至目标服务。
    • 若已在运行,则直接返回 200 OK 放行。
  4. 请求结束后,容器会一直保持运行,直到超过 session_duration(会话时长)无新请求,sablier 自动将其停止,实现资源回收。

sablier 容器部署

docker-compose 配置

services:
  sablier:
    image: sablierapp/sablier:latest
    container_name: sablier
    restart: always
    network_mode: bridge
    environment:
      - TZ=Asia/Shanghai
    command:
      - start
      - --provider.name=docker
    ports:
      - "10000:10000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

参数说明

  • --provider.name=docker:指定容器提供方为 Docker,sablier 通过宿主机 Docker API 管理容器。
  • ports:暴露 10000 端口供 nginx 调用 sablier API。
  • volumes:将宿主机 /var/run/docker.sock 挂载进容器,这是 sablier 与 Docker 通信的关键。

说明:sablier 使用默认配置即可,无需额外挂载 sablier.yaml 配置文件。

目标容器添加标签

为需要被 sablier 管理的容器(以 emby 为例)添加以下 labels:

labels:
  - sablier.enable=true
  - sablier.group=emby

emby 容器完整 docker-compose.yaml 示例

services:
  emby:
    image: emby/embyserver:latest
    container_name: emby
    hostname: emby
#    user: 1000:10
    network_mode: bridge
    privileged: true
    volumes:
      - ./config:/config

      - /volume3:/volume3
    devices:
      - /dev/dri:/dev/dri  #需要硬解的配置
    ports:
      - 8096:8096
#      - 8920:8920 #optional
      - 7359:7359/udp #optional
      - 1900:1900/udp #optional
    restart: 'unless-stopped'
    environment:
      - TZ=Asia/Shanghai
      - UID=0
      - GID=0
    labels:
      - sablier.enable=true
      - sablier.group=emby

端口 8096 对应 nginx 中 proxy_pass 的目标端口。

nginx 完整配置

前提条件

nginx 必须编译 ngx_http_auth_request_module 模块(通常默认已包含,可通过 nginx -V 2>&1 | grep http_auth_request_module 验证)。

完整 server 块示例

server {
    listen 80;
    server_name emby.example.com;

    #  ===== 在 server 下添加 ====
    include /volume1/Workspace/nginx/sablier_auth.conf;

    location / {
        # ===== 在 location 下添加 ====
        set $sablier_names emby; # 此处应为容器的 container_name,而非 sablier.group 标签值
        set $sablier_session 30m;
        auth_request /_sablier_auth;

        proxy_pass http://127.0.0.1:8096;  # 转发到 emby 实际服务端口
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

注意$sablier_names 需要设置的是目标容器的 container_name(容器名),而不是 sablier.group 标签值。请确保目标容器在 docker-compose 中显式配置了 container_name(如 emby 示例中的 container_name: emby),sablier 才能正确匹配并管理该容器。

sablier_auth.conf 内容

#  ===== 在 server 下 添加 ====
#  include /volume1/Workspace/nginx/sablier_auth.conf;
#  ===== 在 location 下添加 ====
#        set $sablier_names emby;
#        set $sablier_session 30m;
#        auth_request /_sablier_auth;
#

location = /_sablier_auth {
    internal;
    proxy_pass http://127.0.0.1:10000/api/strategies/blocking?names=$sablier_names&session_duration=$sablier_session;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_connect_timeout 10s;
    proxy_read_timeout 300s;
}

auth_request 相关配置项详解

  • auth_request /_sablier_auth;:指定认证子请求的 URI,该 location 必须为 internal,仅用于内部转发。
  • auth_request_set:可选,用于将子请求的响应变量赋值给 nginx 变量,便于后续逻辑判断。
  • proxy_pass:将子请求转发至 sablier 的 blocking 策略接口,names 对应目标容器组名,session_duration 为会话时长。
  • proxy_pass_request_body off:认证子请求无需携带原始请求 body,减少传输开销。
  • proxy_set_header Content-Length "":因禁用了 body,必须将 Content-Length 置空,避免请求头冲突。
  • proxy_connect_timeout:连接 sablier 的超时时间,建议 10s。
  • proxy_read_timeout:等待 sablier 返回的读取超时,应大于容器的最大启动时间,建议 300s(5 分钟)。

认证响应机制

  • 当 sablier 返回 200 时,nginx 放行原始请求;
  • 当返回 401403 时,nginx 直接向客户端返回对应错误码;
  • 若容器启动超时或 sablier 不可达,会根据上述超时设置返回 502 Bad Gateway

多场景应用扩展

场景一:多服务分组

不同业务可配置为不同分组,共享同一套 sablier 与 nginx 认证机制。例如同时管理 emby 与 qbittorrent:

# 容器 emby
labels:
  - sablier.enable=true
  - sablier.group=emby

# 容器 qbittorrent
labels:
  - sablier.enable=true
  - sablier.group=download

nginx 中对应两个 location(或两个 server),分别设置 $sablier_names 即可。

场景二:不同会话时长

按服务重要性设置不同会话时长,例如对 nas 管理面板设置 1h,对临时 demo 设置 5m:

# nas 管理面板
location /nas/ {
    set $sablier_names nas;
    set $sablier_session 1h;
    auth_request /_sablier_auth;
    proxy_pass http://127.0.0.1:5000;
}

# 临时 demo
location /demo/ {
    set $sablier_names demo;
    set $sablier_session 5m;
    auth_request /_sablier_auth;
    proxy_pass http://127.0.0.1:8080;
}

场景三:dynamic 等待页模式

若不想让用户在容器启动期间白屏等待,可改用 dynamic 策略,通过等待页自动刷新感知启动进度。将 proxy_pass 指向 dynamic 接口:

location / {
    proxy_pass http://127.0.0.1:10000/api/strategies/dynamic?names=$sablier_names&session_duration=$sablier_session&theme=hacker-terminal;
}

sablier 会返回一个等待页面,容器启动后自动跳转到目标服务,体验更佳。

常见问题与排障

1. docker.sock 权限问题

若 sablier 容器日志出现 permission denied while trying to connect to the Docker daemon socket,说明当前用户(或容器内映射的用户)无权访问 /var/run/docker.sock

解决方法

# 将当前用户加入 docker 组
sudo usermod -aG docker $USER
newgrp docker
# 验证
docker ps

注意:不要随意 chmod 666,存在安全风险。若已修改,可复原:

sudo chown root:docker /var/run/docker.sock
sudo chmod 660 /var/run/docker.sock

2. 请求超时

  • 现象:访问目标服务长时间无响应,最终报 504。proxy_read_timeoutproxy_connect_timeout 设置过短,容器启动超过超时时间。可将 proxy_read_timeout 调大(如 300s)。
  • 排查:手动执行 sablier API 看响应耗时:curl -v "http://127.0.0.1:10000/api/strategies/blocking?names=emby&session_duration=30m"

3. 容器不自动停止

  • 现象:容器空闲后一直运行。先检查 session_duration 是否设置过短,且请求是否持续产生。
  • 排查:查看 sablier 日志,确认容器是否注册成功;若容器标签或组名配置错误,sablier 无法追踪,自然不会停止。

4. 认证失败排查

  • 现象:访问返回 401/403。
  • 排查:确认 sablier 服务正常,且 nginx 能连通 10000 端口;检查 sablier 容器日志,查看请求是否被拒绝;确认 names 参数是否与容器标签组名一致。