NAS 上的 docker-compose 代码库

这里是我公众号 「辣丝刀客」已发表 61 篇 NAS 教程里出现过的 docker-compose(yml)全文,共 79 份。
按用途分了 10 类;每份独立成块,右上角一键复制、粘进 docker-compose.yml 就能跑;卡片抬头点一下就回原文。
👇 整包拿走:79 份 compose + 对应 .env(zip) · 纯文本版

整项目 · 整包可复现 2 个

AI爬虫收费站-Cloudflare-Worker:一个能跑的完整工程,文件逐个列在下面,也可以直接下 zip 整包带走。
项目:AI爬虫收费站-Cloudflare-Worker / README.md整包53行
# AI 爬虫收费站 · Cloudflare Worker + D1

给自己的网站套一层 Worker:AI 爬虫来访问就记一笔(可选:直接返 402),
普通访客照常拿到源站页面。附带一个后台统计页。

## 文件

```
wrangler.toml         Worker 配置 + D1 绑定
schema.sql            建表
src/index.js          全部逻辑(识别 / 记账 / 402 / 反代 / 后台)
test/worker.test.mjs  本地自测:假 D1 + 假源站,14 项
```

## 跑起来

```bash
npm install -D wrangler
npx wrangler login
npx wrangler d1 create crawlpay_db
# 把输出的 database_id 填进 wrangler.toml
npx wrangler d1 execute crawlpay_db --remote --file schema.sql
npx wrangler deploy
```

部署后地址形如 `https://ai-crawl-toll.<你的子域>.workers.dev`。

## 三个必改项

1. `src/index.js` 顶部的 `ORIGIN`:改成你真实源站,**别填本 Worker 的域名**,
   否则请求递归引用自己,报 Error 1019。
2. `src/index.js` 顶部的 `KEY`:后台口令,访问 `/dashboard?key=口令` 才给看。
3. `CHARGE`:`false` 只统计,`true` 给 AI 爬虫返 402。

## 本地自测

```bash
node test/worker.test.mjs
```

不需要 Cloudflare 账号,不联网(源站是打桩的)。
覆盖:记账、普通浏览器不记账、垃圾扫描路径不记账、后台口令、402 分支。

## 免费额度(个人站够用,细则以 Cloudflare 官网为准)

- Worker:10 万请求/天
- D1:500 万行读/天,10 万行写/天

## 说清楚两件事

- UA 可以伪造,这套识别只是「记账」,不是「认证」。
- 402 只是礼貌拒绝,收不到钱。真要收单得走 Cloudflare 官方的 Pay-Per-Crawl。
项目:AI爬虫收费站-Cloudflare-Worker / schema.sql整包20行
-- AI 爬虫收费站 · D1 建表
-- 用法:
--   npx wrangler d1 execute \
--     crawlpay_db --remote \
--     --file schema.sql

CREATE TABLE IF NOT EXISTS crawls (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  ts INTEGER NOT NULL,      -- 毫秒时间戳
  bot TEXT NOT NULL,        -- 爬虫关键字
  path TEXT NOT NULL,       -- 被爬路径
  -- paid 预留给以后接真收款
  paid INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_crawls_ts
  ON crawls (ts);
CREATE INDEX IF NOT EXISTS idx_crawls_bot
  ON crawls (bot);
项目:AI爬虫收费站-Cloudflare-Worker / src/index.js整包227行
// AI 爬虫收费站:Cloudflare Worker + D1
// CHARGE=false 只记账;true 给爬虫返 402
const CHARGE = false;

// 你的真实源站,别填本 Worker 的域名
const ORIGIN = "https://你的源站.example";

// 后台口令:/dashboard?key=这个值
const KEY = "改成你自己的口令";

// 认得的 AI 爬虫 UA 关键字(小写)
const BOTS = [
  "gptbot", "oai-searchbot",
  "chatgpt-user", "claudebot",
  "claude-web", "anthropic-ai",
  "perplexitybot", "googleother",
  "applebot-extended",
  "meta-externalagent",
  "amazonbot", "bytespider",
  "ccbot", "cohere-ai",
  "diffbot", "petalbot",
];

// 黑客扫描常打的路径,命中就不记账
const JUNK = [
  ".env", "/.git", "wp-", "phpinfo",
  "/admin", "/login", "/backup",
];

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const path = url.pathname;
    const hd = request.headers;
    const raw = hd.get("user-agent");
    const ua = (raw || "").toLowerCase();

    // 1 后台,得带对口令
    if (path === "/dashboard") {
      const q = url.searchParams;
      if (q.get("key") !== KEY) {
        return notFound();
      }
      return dash(env);
    }

    // 2 垃圾扫描:透传,不记账
    if (isJunk(path)) {
      return proxy(request);
    }

    // 3 AI 爬虫:先记一笔
    const bot = botOf(ua);
    if (bot) {
      const sql = "INSERT INTO crawls" +
        " (ts, bot, path)" +
        " VALUES (?, ?, ?)";
      await env.DB.prepare(sql)
        .bind(Date.now(), bot, path)
        .run();
      if (CHARGE) return pay402();
    }

    // 4 其他一律透传到源站
    return proxy(request);
  },
};

function botOf(ua) {
  for (const b of BOTS) {
    if (ua.includes(b)) return b;
  }
  return "";
}

function isJunk(path) {
  const p = path.toLowerCase();
  return JUNK.some((x) => p.includes(x));
}

function notFound() {
  const opt = { status: 404 };
  return new Response("404", opt);
}

// 402:告诉爬虫「要付钱」
function pay402() {
  const body = JSON.stringify({
    error: "402 Payment Required",
    price: "0.001 USDC on Base",
  });
  return new Response(body, {
    status: 402,
    headers: {
      "content-type": "application/json",
      "cache-control": "no-store",
      "x-payment": "0.001 USDC on Base",
    },
  });
}

// 反代到源站:剔掉 hop-by-hop 头
async function proxy(request) {
  const url = new URL(request.url);
  const target = new URL(
    url.pathname + url.search, ORIGIN);
  const hd = new Headers(request.headers);
  const drop = [
    "host", "connection",
    "keep-alive", "transfer-encoding",
  ];
  drop.forEach((x) => hd.delete(x));
  const init = {
    method: request.method,
    headers: hd,
    redirect: "follow",
  };
  if (request.method !== "GET" &&
      request.method !== "HEAD") {
    init.body = request.body;
  }
  const res = await fetch(target, init);
  const out = new Response(res.body, res);
  out.headers.set(
    "access-control-allow-origin", "*");
  return out;
}

// 后台统计页
async function dash(env) {
  const since = Date.now() - 864e5;
  const one = async (sql, ...a) => {
    const r = await env.DB.prepare(sql)
      .bind(...a).first();
    return (r && r.c) || 0;
  };
  const all = (sql, ...a) =>
    env.DB.prepare(sql).bind(...a).all();

  const cnt = "SELECT COUNT(*) c" +
    " FROM crawls";
  const sel24 = cnt + " WHERE ts > ?";
  const c24 = await one(sel24, since);
  const cAll = await one(cnt);
  const cBot = await one(
    "SELECT COUNT(DISTINCT bot) c" +
    " FROM crawls");

  const top = (await all(
    "SELECT bot, COUNT(*) c FROM crawls" +
    " GROUP BY bot ORDER BY c DESC" +
    " LIMIT 10")).results;

  const hsql = "SELECT (ts/3600000) h," +
    " COUNT(*) c FROM crawls" +
    " WHERE ts > ? GROUP BY h ORDER BY h";
  const hres = await all(hsql, since);
  const hours = hres.results;

  const rows = top.map((r) =>
    "<tr><td>" + r.bot + "</td><td>" +
    r.c + "</td></tr>").join("");

  const max = Math.max(1, ...hours.map(
    (h) => h.c));
  const bars = hours.map((h) => {
    const px = Math.round(
      h.c * 100 / max);
    const st = "height:" + px + "px";
    return "<div><i style=" +
      "\"" + st + "\"></i><b>" +
      h.c + "</b></div>";
  }).join("");

  const css =
    "body{background:#111;color:#eee;" +
    "font:16px/1.7 sans-serif;" +
    "padding:16px}" +
    ".c{background:#1e293b;" +
    "border-radius:10px;" +
    "padding:10px 14px;margin:6px 0}" +
    ".n{font-size:26px;font-weight:700}" +
    ".h{color:#94a3b8;font-size:13px}" +
    "table{width:100%;" +
    "border-collapse:collapse}" +
    "td{padding:6px;" +
    "border-bottom:1px solid #333}" +
    ".bar{display:flex;gap:4px;" +
    "align-items:flex-end;height:120px}" +
    ".bar div{flex:1;text-align:center}" +
    ".bar i{display:block;" +
    "background:#3b82f6;" +
    "border-radius:3px 3px 0 0}";

  const card = (t, v) =>
    "<div class=c><div class=h>" +
    t + "</div>" +
    "<div class=n>" + v + "</div></div>";
  const mode = CHARGE ? "收费" : "计数";

  const html = "<!doctype html>" +
    "<meta charset=\"utf-8\">" +
    "<meta name=viewport " +
    "content=\"width=device-width," +
    "initial-scale=1\">" +
    "<style>" + css + "</style>" +
    "<h2>AI 爬虫收费站</h2>" +
    "<div class=c>" +
    card("近 24 小时", c24) +
    card("累计", cAll) +
    card("爬虫种类", cBot) +
    card("模式", mode) +
    "</div>" +
    "<h3>近 24 小时趋势</h3>" +
    "<div class=bar>" + bars + "</div>" +
    "<h3>Top 爬虫</h3><table>" +
    (rows || "<tr><td>暂无</td></tr>") +
    "</table>";

  return new Response(html, {
    headers: {
      "content-type":
        "text/html; charset=utf-8",
    },
  });
}
项目:AI爬虫收费站-Cloudflare-Worker / test/worker.test.mjs整包114行
// 本地把 Worker 逻辑跑一遍:假 D1 + 假源站,验记账 / 402 / 后台
// 用法:node test/worker.test.mjs
import { readFileSync, writeFileSync, unlinkSync } from "node:fs";

// ── 假 D1:内存版,只认代码里用到的那几条 SQL
function fakeDB() {
  const rows = [];
  return {
    rows,
    prepare(sql) {
      const q = sql.replace(/\s+/g, " ").trim();
      const st = {
        args: [],
        bind(...a) { st.args = a; return st; },
        async run() {
          rows.push({ ts: st.args[0], bot: st.args[1], path: st.args[2] });
        },
        async first() { return { c: st.count() }; },
        async all() { return { results: st.list() }; },
        count() {
          if (q.includes("COUNT(DISTINCT")) return new Set(rows.map(r => r.bot)).size;
          if (q.includes("WHERE ts >")) return rows.filter(r => r.ts > st.args[0]).length;
          return rows.length;
        },
        list() {
          if (q.includes("GROUP BY bot")) {
            const m = {};
            rows.forEach(r => { m[r.bot] = (m[r.bot] || 0) + 1; });
            return Object.entries(m).map(([bot, c]) => ({ bot, c }))
              .sort((a, b) => b.c - a.c).slice(0, 10);
          }
          const m = {};
          rows.filter(r => r.ts > st.args[0]).forEach(r => {
            const h = Math.floor(r.ts / 3600000);
            m[h] = (m[h] || 0) + 1;
          });
          return Object.entries(m).map(([h, c]) => ({ h: +h, c }));
        },
      };
      return st;
    },
  };
}

let originHit = 0;
const realFetch = globalThis.fetch;
globalThis.fetch = async () => {
  originHit++;
  return new Response("<html>源站页面</html>", { status: 200 });
};

let pass = 0, fail = 0;
const ok = (name, cond, extra = "") => {
  if (cond) { pass++; console.log("  ✅ " + name); }
  else { fail++; console.log("  ❌ " + name + " " + extra); }
};

// ── 计数模式:直接用源码
const worker = (await import("../src/index.js")).default;
const env = { DB: fakeDB() };
const hit = (ua, path = "/") => worker.fetch(
  new Request("https://tol.example" + path,
    { headers: { "user-agent": ua } }), env);

console.log("① 计数模式(CHARGE=false)");
let r = await hit("Mozilla/5.0 (compatible; GPTBot/1.0)");
ok("爬虫拿到源站内容", (await r.text()).includes("源站页面"));
ok("爬虫被记账 1 条", env.DB.rows.length === 1, JSON.stringify(env.DB.rows));
ok("记的是 gptbot", env.DB.rows[0]?.bot === "gptbot");
ok("源站被反代命中", originHit === 1, "originHit=" + originHit);

console.log("② 普通浏览器");
await hit("Mozilla/5.0 (Windows NT 10.0) Chrome/120.0");
ok("普通浏览器不记账", env.DB.rows.length === 1);

console.log("③ 垃圾扫描路径");
await hit("Mozilla/5.0", "/.env");
await hit("Mozilla/5.0", "/wp-admin/setup.php");
ok("垃圾路径不记账", env.DB.rows.length === 1);

console.log("④ 多爬虫 + 后台");
await hit("Mozilla/5.0 (compatible; ClaudeBot/1.0)");
await hit("Mozilla/5.0 (compatible; GPTBot/1.0)", "/post/1");
const bad = await hit("Mozilla/5.0", "/dashboard");
ok("口令不对返 404", bad.status === 404);
const good = await hit("Mozilla/5.0",
  "/dashboard?key=改成你自己的口令");
const html = await good.text();
ok("口令对返 200", good.status === 200);
ok("后台累计显示 3", html.includes(">3<"), html.slice(0, 160));
ok("后台列出 gptbot / claudebot",
  html.includes("gptbot") && html.includes("claudebot"));

console.log("⑤ 收费模式(把 CHARGE 改成 true 再跑)");
const src = readFileSync(new URL("../src/index.js", import.meta.url), "utf8");
const tmp = new URL("./.charge.js", import.meta.url);
writeFileSync(tmp, src.replace("const CHARGE = false;", "const CHARGE = true;"));
const charged = (await import(tmp.href)).default;
const env2 = { DB: fakeDB() };
const hit2 = (ua, path = "/") => charged.fetch(
  new Request("https://tol.example" + path,
    { headers: { "user-agent": ua } }), env2);
const c1 = await hit2("Mozilla/5.0 (compatible; GPTBot/1.0)");
ok("爬虫拿到 402", c1.status === 402, "status=" + c1.status);
ok("402 带 x-payment 头", !!c1.headers.get("x-payment"));
ok("收费模式下依然记账", env2.DB.rows.length === 1);
const c2 = await hit2("Mozilla/5.0 (Windows NT 10.0) Chrome/120.0");
ok("普通浏览器照样放行", c2.status === 200);
unlinkSync(tmp);

globalThis.fetch = realFetch;
console.log(`\n结果:通过 ${pass},失败 ${fail}`);
process.exit(fail ? 1 : 0);
项目:AI爬虫收费站-Cloudflare-Worker / wrangler.toml整包10行
name = "ai-crawl-toll"
main = "src/index.js"
compatibility_date = "2025-06-01"

# 建库后把 database_id 换成你自己的
[[d1_databases]]
binding = "DB"
database_name = "crawlpay_db"
database_id = "填你的 D1 数据库 ID"
InfiPlot-NAS部署:一个能跑的完整工程,文件逐个列在下面,也可以直接下 zip 整包带走。
项目:InfiPlot-NAS部署 / .env.local整包26行
# ===== 文本:写剧情和对话(必填)=====
TEXT_BASE_URL=https://api.deepseek.com/v1
TEXT_API_KEY=sk-你的Key
TEXT_MODEL=deepseek-v4-flash

# ===== 图像:画场景(必填)=====
IMAGE_BASE_URL=https://api.runware.ai/v1
IMAGE_API_KEY=runware-你的Key
IMAGE_MODEL=runware:400@6
IMAGE_PROVIDER=runware

# ===== 视觉:看懂你点哪(必填)=====
VISION_BASE_URL=https://token-plan-sgp.xiaomimimo.com/v1
VISION_API_KEY=tp-你的Key
VISION_MODEL=mimo-v2.5

# ===== 配音(可选,留空就没声音)=====
# 小米这两项跟视觉同一地址、同一个 tp- Key
TTS_BASE_URL=https://token-plan-sgp.xiaomimimo.com/v1
TTS_API_KEY=tp-你的Key
TTS_SPEECH_MODEL=mimo-v2.5-tts

# ===== 其他 =====
# true = 跳过生图,用占位图
MOCK_IMAGE=false
项目:InfiPlot-NAS部署 / README.md整包58行
# InfiPlot · NAS 自部署

AI 实时生成内容的互动剧情游戏:剧情、场景图、角色配音都由模型现场生成,不是预设分支。
官方仓库:https://github.com/zonghaoyuan/infiplot (另有在线试玩 infiplot.com)

## 文件

```
docker-compose.yml   compose 文件(官方版基础上加了 container_name 和注释)
.env.local           环境变量模板,把 Key 填进去就能跑
```

## 三步跑起来

```bash
mkdir -p infiplot && cd infiplot
# 把这两个文件放进来,然后编辑 .env.local 填 Key
docker compose up -d
```

局域网浏览器打开 `http://NAS的IP:3000`。看日志:`docker compose logs -f infiplot`。

> 不用 compose 也行:
> `docker run -d -p 3000:3000 --env-file .env.local ghcr.io/zonghaoyuan/infiplot:latest`

## 四个 Key(最省事的组合)

| 用途 | 推荐 | 环境变量 |
|------|------|----------|
| 文本·剧情 | DeepSeek `deepseek-v4-flash` | `TEXT_*` |
| 图像·场景 | Runware `runware:400@6`(FLUX.2 klein 9B KV) | `IMAGE_*` |
| 视觉·点击 | 小米 MiMo `mimo-v2.5`(tp- Key) | `VISION_*` |
| 配音 | 小米 MiMo `mimo-v2.5-tts`(免费)/ StepFun | `TTS_*`(可留空) |

- 小米 Token Plan 一个 `tp-` Key 同时覆盖文本、视觉、配音(订阅制;配音本身免费额度较宽)。
- Runware 按张计费,官方给的参考价约 $0.00078/张(1792×1024、4 步)。
- 只想要文字、不想花生图钱:`MOCK_IMAGE=true`,返回占位图,剧情/语音/选项照常。
- 文本也能换成本地 Ollama,或 OpenCode 的包月套餐($10/月含多个开源模型)。

## 镜像拉不动(国内常见)

官方镜像在 `ghcr.io`。两个办法:

1. 给 Docker 配 ghcr 加速(registry-mirrors),配置后重启 Docker。
2. 用能通的加速前缀拉下来再改回原名:

```bash
docker pull <加速域名>/zonghaoyuan/infiplot:latest
docker tag <加速域名>/zonghaoyuan/infiplot:latest \
  ghcr.io/zonghaoyuan/infiplot:latest
```

## 官方配置文档

- 配置教程:https://github.com/zonghaoyuan/infiplot/blob/main/docs/configuration.md
- 完整变量模板:仓库根目录 `.env.example`(还有图片代理、Umami 统计、Supabase 登录等可选项)
- 可选的图片代理 Worker:https://github.com/zonghaoyuan/infiplot-image-proxy
项目:InfiPlot-NAS部署 / docker-compose.yml整包13行
services:
  infiplot:
    # 不写 :latest 就是最新版
    image: ghcr.io/zonghaoyuan/infiplot
    container_name: infiplot
    restart: unless-stopped
    ports:
      # 左边是 NAS 端口,占了就改
      - "3000:3000"
    env_file:
      # 读同目录下填好 Key 的那个文件
      - .env.local

相册与照片 5 段

volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
需要的变量(.env)
UPLOAD_LOCATION=   ← 这里要你填
services:
  immich-server:
    image: ghcr.io/immich-app/immich-server:release
    command: ['start.sh', 'immich']
    volumes:
      - ./library:/usr/src/app/upload
      - ./config:/config  # 保留给自定义配置(可选)
    env_file:
      - .env
    ports:
      - 2283:3001
    depends_on:
      - redis
      - database
‘    restart: unless-stopped

  immich-microservices:
    image: ghcr.io/immich-app/immich-server:release
    command: ['start.sh', 'immich-microservices']
    volumes:
      - ./library:/usr/src/app/upload
      - ./config:/config
    env_file:
      - .env
    depends_on:
      - redis
      - database
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    restart: unless-stopped

  database:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      POSTGRES_DB: immich
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    restart: unless-stopped
services:
  app:
    image: ghcr.io/hotio/demo-app
    container_name: demo-app
    volumes:
      # 配置和上传都要挂出来
      - /vol1/1000/docker/app:/config
      - /vol1/1000/photos:/data/upload

  app-db:
    image: postgres:16-alpine
    container_name: app-db
    volumes:
      # 数据库目录,主机侧写短点
      - /vol1/pg:/var/lib/postgresql/data

  memos:
    image: neosmemo/memos:stable
    container_name: memos
    volumes:
      # 具名卷:跨容器共享用
      - memos-data:/var/opt/memos

volumes:
  memos-data:
immich-server:
    image: ${SRV}
    container_name: immich_server
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
      - DB_HOSTNAME=immich-db
      - DB_USERNAME=postgres
      - DB_DATABASE_NAME=immich
      - DB_PASSWORD=${DBPASS}
      - REDIS_HOSTNAME=immich-redis
    volumes:
      - ${IML}:/usr/src/app/upload
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "2283:2283"
    depends_on:
      - immich-redis
      - immich-db
    restart: unless-stopped

  immich-machine-learning:
    image: ${ML}
    container_name: immich_ml
    environment:
      - TZ=${TZ}
    volumes:
      - ml-cache:/cache
    restart: unless-stopped

  immich-redis:
    image: ${REDIS}
    container_name: immich_redis
    restart: unless-stopped

  immich-db:
    image: ${DB}
    container_name: immich_db
    environment:
      - POSTGRES_PASSWORD=${DBPASS}
      - POSTGRES_USER=postgres
      - POSTGRES_DB=immich
    volumes:
      - ${IMD}:/var/lib/postgresql/data
    restart: unless-stopped
需要的变量(.env)
PUID=1000
PGID=1000
TZ=Asia/Shanghai
DB=${IMG}/postgres:${TAG}
DBPASS=改成你的强数据库密码
GH=ghcr.io
IMD=${ROOT}/immich/pgdata
IMG=${GH}/immich-app
IML=${ROOT}/immich/library
ML=${IMG}/immich-machine-learning:release
REDIS=docker.io/valkey/valkey:8-bookworm
ROOT=/vol1/1000/docker
SRV=${IMG}/immich-server:release
TAG=14-vectorchord0.4.3-pgvectors0.2.0
services:
  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ${JFD}:/config
      - ${DATA}/media:/media:ro
    ports:
      - "8096:8096"
    devices:
      - /dev/dri:/dev/dri
    restart: unless-stopped

  immich-server:
    image: ${SRV}
    container_name: immich_server
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
      - DB_HOSTNAME=immich-db
      - DB_USERNAME=postgres
      - DB_DATABASE_NAME=immich
      - DB_PASSWORD=${DBPASS}
      - REDIS_HOSTNAME=immich-redis
    volumes:
      - ${IML}:/usr/src/app/upload
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "2283:2283"
    depends_on:
      - immich-redis
      - immich-db
    restart: unless-stopped

  immich-machine-learning:
    image: ${ML}
    container_name: immich_ml
    environment:
      - TZ=${TZ}
    volumes:
      - ml-cache:/cache
    restart: unless-stopped

  immich-redis:
    image: ${REDIS}
    container_name: immich_redis
    restart: unless-stopped

  immich-db:
    image: ${DB}
    container_name: immich_db
    environment:
      - POSTGRES_PASSWORD=${DBPASS}
      - POSTGRES_USER=postgres
      - POSTGRES_DB=immich
    volumes:
      - ${IMD}:/var/lib/postgresql/data
    restart: unless-stopped

  navidrome:
    image: deluan/navidrome
    container_name: navidrome
    environment:
      - ND_MUSICFOLDER=/music
      - ND_SCANSCHEDULE=1h
      - TZ=${TZ}
    volumes:
      - ${NVD}:/data
      - ${DATA}/music:/music:ro
    ports:
      - "4533:4533"
    restart: unless-stopped

  paperless:
    image: ${PAP}
    container_name: paperless
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
      - PAPERLESS_REDIS=${PLR}
      - PAPERLESS_URL=https://p.abc.cn
    volumes:
      - ${PPD}:/usr/src/paperless/data
      - ${PPM}:/usr/src/paperless/media
      - ${PPC}:/usr/src/paperless/consume
    ports:
      - "8000:8000"
    depends_on:
      - plredis
    restart: unless-stopped

  plredis:
    image: redis:7
    container_name: plredis
    restart: unless-stopped

  vaultwarden:
    image: vaultwarden/server
    container_name: vaultwarden
    environment:
      - SIGNUPS_ALLOWED=true
      - DOMAIN=https://v.abc.cn
      - TZ=${TZ}
    volumes:
      - ${VWD}:/data
    ports:
      - "8081:80"
    restart: unless-stopped

  filebrowser:
    image: filebrowser/filebrowser
    container_name: filebrowser
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ${DATA}/srv:/srv
      - ${FBD}:/database
      - ${FBC}:/config
    ports:
      - "8082:80"
    restart: unless-stopped

volumes:
  ml-cache:
需要的变量(.env)
PUID=1000
PGID=1000
TZ=Asia/Shanghai
DATA=/vol1/1000/data
DB=${IMG}/postgres:${TAG}
DBPASS=改成你的强数据库密码
FBC=${ROOT}/filebrowser/cfg
FBD=${ROOT}/filebrowser/db
GH=ghcr.io
IMD=${ROOT}/immich/pgdata
IMG=${GH}/immich-app
IML=${ROOT}/immich/library
JFD=${ROOT}/jellyfin
ML=${IMG}/immich-machine-learning:release
NVD=${ROOT}/navidrome
PAP=${GH}/${PNX}/${PNX}:latest
PLR=redis://plredis:6379
PNX=paperless-ngx
PPC=${DATA}/scans
PPD=${ROOT}/paperless/data
PPM=${ROOT}/paperless/media
REDIS=docker.io/valkey/valkey:8-bookworm
ROOT=/vol1/1000/docker
SRV=${IMG}/immich-server:release
TAG=14-vectorchord0.4.3-pgvectors0.2.0
VWD=${ROOT}/vaultwarden

影音与下载 20 段

version: "3.8"

services:

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    restart: always
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./prowlarr/config:/config
    ports:
      - "9696:9696"
    network_mode: bridge

  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    restart: always
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./sonarr/config:/config
      - /volume1/downloads:/downloads
      - /volume1/video/tv:/tv
    ports:
      - "8989:8989"
    network_mode: bridge

  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    restart: always
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./radarr/config:/config
      - /volume1/downloads:/downloads
      - /volume1/video/movie:/movies
    ports:
      - "7878:7878"
    network_mode: bridge
version: "3.8"

services:
  jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
      - JELLYFIN_PublishedServerUrl=http://192.168.31.2:8096
    volumes:
      - ./config:/config
      - ./cache:/cache
      - /volume1/video/movies:/media/movies
      - /volume1/video/tv:/media/tv
    ports:
      - "8096:8096"
      - "8920:8920"
      - "7359:7359/udp"
      - "1900:1900/udp"
    network_mode: bridge
出处:NAS 影音全自动全家桶部署教程compose65行
version: "3.8"
services:
  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    restart: always
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./prowlarr/config:/config
    ports:
      - "9696:9696"
    network_mode: bridge
  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    restart: always
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./sonarr/config:/config
      - /volume1/downloads:/downloads
      - /volume1/video/tv:/tv
    ports:
      - "8989:8989"
    network_mode: bridge
  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    restart: always
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./radarr/config:/config
      - /volume1/downloads:/downloads
      - /volume1/video/movie:/movies
    ports:
      - "7878:7878"
    network_mode: bridge
  jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
      - JELLYFIN_PublishedServerUrl=http://192.168.31.2:8096
    volumes:
      - ./jellyfin/config:/config
      - ./jellyfin/cache:/cache
      - /volume1/video/movie:/media/movies
      - /volume1/video/tv:/media/tv
    ports:
      - "8096:8096"
      - "8920:8920"
      - "7359:7359/udp"
      - "1900:1900/udp"
    network_mode: bridge
version: "3.8"
services:
  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./prowlarr/config:/config
    ports:
      - "9696:9696"
    network_mode: bridge
  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./sonarr/config:/config
      - /volume1/downloads:/downloads
      - /volume1/video/tv:/tv
    ports:
      - "8989:8989"
    network_mode: bridge
  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./radarr/config:/config
      - /volume1/downloads:/downloads
      - /volume1/video/movie:/movies
    ports:
      - "7878:7878"
    network_mode: bridge
  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent/config:/config
      - /volume1/downloads:/downloads
    ports:
      - "8080:8080"
      - "6881:6881"
      - "6881:6881/udp"
    network_mode: bridge
  bazarr:
    image: lscr.io/linuxserver/bazarr:latest
    container_name: bazarr
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./bazarr/config:/config
      - /volume1/video/tv:/tv
      - /volume1/video/movie:/movies
    ports:
      - "6767:6767"
    network_mode: bridge
  jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
      - JELLYFIN_PublishedServerUrl=http://192.168.31.2:8096
    volumes:
      - ./jellyfin/config:/config
      - ./jellyfin/cache:/cache
      - /volume1/video/movie:/media/movies
      - /volume1/video/tv:/media/tv
    ports:
      - "8096:8096"
      - "8920:8920"
      - "7359:7359/udp"
      - "1900:1900/udp"
    network_mode: bridge
    # 有Intel核显开启硬解,无核显直接注释两行
    # devices:
    #   - /dev/dri/renderD128:/dev/dri/renderD128
version: "3.8"

networks:
  media_net:
    driver: bridge

services:
  redis:
    image: redis:7-alpine
    container_name: mp-redis
    restart: always
    mem_limit: 512m
    volumes:
      - ./redis:/data
    networks:
      - media_net

  postgres:
    image: postgres:18-alpine
    container_name: mp-pg
    restart: always
    volumes:
      - ./postgres:/var/lib/postgresql/data
    ports:
      - "5433:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: moviepilot
      TZ: Asia/Shanghai
    healthcheck:
      test: ["CMD-SHELL","pg_isready -U postgres -d moviepilot"]
      interval: 10s
      timeout: 5s
      retries: 5
    mem_limit: 1g
    networks:
      - media_net

  flaresolverr:
    image: ghcr.io/flaresolverr/flaresolverr:latest
    container_name: flaresolverr
    restart: always
    environment:
      TZ: Asia/Shanghai
      LOG_LEVEL: info
    ports:
      - "8191:8191"
    networks:
      - media_net

  moviepilot:
    image: jxxghp/moviepilot:latest
    container_name: moviepilot
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
      flaresolverr:
        condition: service_started
    ports:
      - "3000:3000"
    volumes:
      - ./mp_config:/config
      # 【必改】下载目录(与QB统一)
      - /mnt/download:/downloads
      # 【必改】最终媒体库目录(Emby/Jellyfin读取)
      - /mnt/media:/media
    environment:
      TZ: Asia/Shanghai
      PUID: 1000
      PGID: 1000
      DB_TYPE: postgres
      POSTGRES_HOST: postgres
      POSTGRES_PORT: 5432
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: moviepilot
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_DB: 0
      FLARESOLVERR_URL: http://flaresolverr:8191
    networks:
      - media_net

  mdc:
    image: mdcng/mdc:latest
    container_name: mdc
    restart: always
    ports:
      - "9208:9208"
    environment:
      TZ: Asia/Shanghai
      PUID: 1000
      PGID: 1000
      MDC_USERNAME: admin
      MDC_PASSWORD: admin
      FLARESOLVERR_URL: http://flaresolverr:8191
    volumes:
      - ./mdc_config:/config
      - /mnt/download:/downloads
      - /mnt/media:/media
    networks:
      - media_net
version: "3.8"
services:
  moviepilot:
    image: jxxghp/moviepilot:latest
    container_name: moviepilot
    ports:
      - "3000:3000"
    volumes:
      - ./config:/config
      - ./media:/media
      - ./downloads:/downloads
    environment:
      - TZ=Asia/Shanghai
    restart: always
services:
  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - /vol2/1000/docker/qbittorrent/config:/config
      - /vol2/1000/downloads:/downloads
    ports:
      - 8080:8080
      - 6881:6881
      - 6881:6881/udp
    restart: unless-stopped

  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - /vol2/1000/docker/sonarr/config:/config
      - /vol2/1000/downloads:/downloads
      - /vol2/1000/tv:/tv
    ports:
      - 8989:8989
    restart: unless-stopped

  jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - /vol2/1000/docker/jellyfin/config:/config
      - /vol2/1000/tv:/data/tvshows
    ports:
      - 8096:8096
    restart: unless-stopped
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    environment:
      - PUID=1000
      - PGID=1001
      - TZ=Asia/Shanghai
    volumes:
      - ./config/jellyfin:/config
      - ./media:/media
    ports:
      - "8097:8096"
    restart: unless-stopped
    networks:
      - media-net

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent
    container_name: qbittorrent
    environment:
      - PUID=1000
      - PGID=1001
      - TZ=Asia/Shanghai
      - WEBUI_PORT=8080
    volumes:
      - ./config/qbittorrent:/config
      - ./media:/media
    ports:
      - "8080:8080"
      - "6881:6881"
      - "6881:6881/udp"
    restart: unless-stopped
    networks:
      - media-net

  sonarr:
    image: ghcr.io/hotio/sonarr:latest
    container_name: sonarr
    environment:
      - PUID=1000
      - PGID=1001
      - TZ=Asia/Shanghai
    volumes:
      - ./config/sonarr:/config
      - ./media:/media
    ports:
      - "8989:8989"
    restart: unless-stopped
    networks:
      - media-net

  radarr:
    image: lscr.io/linuxserver/radarr
    container_name: radarr
    environment:
      - PUID=1000
      - PGID=1001
      - TZ=Asia/Shanghai
    volumes:
      - ./config/radarr:/config
      - ./media:/media
    ports:
      - "7878:7878"
    restart: unless-stopped
    networks:
      - media-net

  prowlarr:
    image: ghcr.io/hotio/prowlarr:latest
    container_name: prowlarr
    environment:
      - PUID=1000
      - PGID=1001
      - TZ=Asia/Shanghai
    volumes:
      - ./config/prowlarr:/config
    ports:
      - "9696:9696"
    restart: unless-stopped
    networks:
      - media-net

  recycler:
    image: python:3.12-alpine
    container_name: media-recycler
    environment:
      - TZ=Asia/Shanghai
      - JF_URL=http://jellyfin:8096
      - JF_TOKEN=改成你的Jellyfin令牌
      - RADARR_URL=http://radarr:7878
      - RADARR_KEY=改成你的Radarr密钥
      - SONARR_URL=http://sonarr:8989
      - SONARR_KEY=改成你的Sonarr密钥
      - KEEP_DAYS=30
      - DRY_RUN=1
    volumes:
      - ./recycler:/app
    working_dir: /app
    command: sh /app/loop.sh
    restart: unless-stopped
    networks:
      - media-net

networks:
  media-net:
    driver: bridge
services:
  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./config/qbittorrent:/config
      - /vol2/1000/downloads:/downloads
    ports:
      - "8080:8080"
      - "6881:6881"
      - "6881:6881/udp"
    restart: unless-stopped
    networks:
      - media-net

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./config/prowlarr:/config
    ports:
      - "9696:9696"
    restart: unless-stopped
    networks:
      - media-net

  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./config/sonarr:/config
      - /vol2/1000/tv:/tv
      - /vol2/1000/downloads:/downloads
    ports:
      - "8989:8989"
    restart: unless-stopped
    networks:
      - media-net

  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./config/radarr:/config
      - /vol2/1000/movies:/movies
      - /vol2/1000/downloads:/downloads
    ports:
      - "7878:7878"
    restart: unless-stopped
    networks:
      - media-net

  jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./config/jellyfin:/config
      - /vol2/1000/tv:/data/tvshows
      - /vol2/1000/movies:/data/movies
    ports:
      - "8096:8096"
    restart: unless-stopped
    networks:
      - media-net

  bazarr:
    image: lscr.io/linuxserver/bazarr:latest
    container_name: bazarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - ./config/bazarr:/config
      - /vol2/1000/tv:/tv
      - /vol2/1000/movies:/movies
      - /vol2/1000/downloads:/downloads
    ports:
      - "6767:6767"
    restart: unless-stopped
    networks:
      - media-net

networks:
  media-net:
    driver: bridge
# 三件套:Jellyfin/Syncthing/Portainer
# 位置 /vol1/1000/docker/nas-stack
# 启动 docker compose up -d

services:

  # ----- 1. 影音:Jellyfin
  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    restart: unless-stopped
    # host 网络:DLNA 和局域网发现才可用
    network_mode: host
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./jellyfin/config:/config
      - ./jellyfin/cache:/cache
      - ${MEDIA_DIR}:/media:ro
    # 把核显给容器,硬解才用得起来
    devices:
      - /dev/dri:/dev/dri

  # ----- 2. 同步:Syncthing
  syncthing:
    image: syncthing/syncthing
    container_name: syncthing
    restart: unless-stopped
    # 也用 host 网络,局域网自动发现更顺
    network_mode: host
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./syncthing/config:/var/syncthing
      - ${SYNC_DIR}:/mnt/sync

  # ----- 3. 面板:Portainer
  portainer:
    image: portainer/portainer-ce
    container_name: portainer
    restart: unless-stopped
    # 用 -H 把面板指向下面挂进来的 socket
    command: -H unix:///sock
    ports:
      - "9000:9000"
    volumes:
      # socket 挂到 /sock,再用 -H
      - /var/run/docker.sock:/sock
      - ./portainer/data:/data
需要的变量(.env)
PUID=1000
PGID=1000
TZ=Asia/Shanghai
MEDIA_DIR=/vol1/1000/media
SYNC_DIR=/vol1/1000/sync
services:
  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    # 左主机右容器,别写反
    ports:
      # 8080 被占,主机侧换 18080
      - "18080:8096"
      - "7359:7359/udp"
      # 只给本机访问的更安全
      - "127.0.0.1:18920:8920"
# 看主机上谁占着 8080
sudo ss -tlnp | grep 8080
# 看容器现在的端口映射
docker ps --format '{{.Names}} {{.Ports}}'
services:
  sonarr:
    image: ghcr.io/hotio/sonarr
    container_name: sonarr
    volumes:
      # 绝对路径,别用 ./
      - /vol1/1000/docker/sonarr:/config
      - /vol1/1000/media:/media
      - /vol1/1000/downloads:/downloads
mkdir -p /vol1/1000/docker/sonarr
cd /vol1/1000/docker/sonarr
docker compose up -d
# 查当前用户的 uid/gid
id
# 看挂载目录现在归谁
ls -ln /vol1/1000/docker/sonarr
services:
  sonarr:
    image: ghcr.io/hotio/sonarr
    container_name: sonarr
    # 换成 id 查到的数值
    environment:
      - PUID=1000
      - PGID=1000
      - UMASK=002
    volumes:
      - /vol1/1000/docker/sonarr:/config
      - /vol1/1000/media:/media

  app-db:
    image: redis:7-alpine
    container_name: app-db
    # 非 lsio 镜像用 user 指定
    user: "1000:1000"
    volumes:
      - /vol1/1000/docker/app/redis:/data
# 列出所有容器含已停止的
docker ps -a | grep jellyfin
# 删掉残留的同名容器
docker rm -f jellyfin
# 停旧容器并重建,配置才生效
docker compose down
docker compose pull
docker compose up -d
services:
  jellyfin:
    image: jellyfin/jellyfin
    # 项目名加服务名,避免撞名
    container_name: nas-jellyfin
    restart: unless-stopped
services:
  sonarr:
    image: ghcr.io/hotio/sonarr
    container_name: sonarr
    environment:
      # 时区必写,日志才对
      - TZ=Asia/Shanghai
      - LANG=C.UTF-8
      - LC_ALL=C.UTF-8
    volumes:
      - /vol1/1000/docker/sonarr:/config
      # 跟随主机时间,只读
      - /etc/localtime:/etc/localtime:ro
services:
  sonarr:
    # 卡 lscr.io 就换这行
    image: ghcr.io/hotio/sonarr
    container_name: sonarr
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - /vol4/1000/jellyfin:/config
      - /vol4/1000/media:/media
    ports:
      - "8096:8096"
    networks:
      - home

networks:
  home:
    driver: bridge
services:
  navidrome:
    image: deluan/navidrome
    container_name: navidrome
    restart: unless-stopped
    ports:
      - 4533:4533
    environment:
      - ND_MUSICFOLDER=/music
      - ND_SCANSCHEDULE=1h
    volumes:
      - ./data:/data
      - /vol1/1000/music:/music:ro
jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ${JFD}:/config
      - ${DATA}/media:/media:ro
    ports:
      - "8096:8096"
    devices:
      - /dev/dri:/dev/dri
    restart: unless-stopped
需要的变量(.env)
PUID=1000
PGID=1000
TZ=Asia/Shanghai
DATA=/vol1/1000/data
JFD=${ROOT}/jellyfin
ROOT=/vol1/1000/docker
navidrome:
    image: deluan/navidrome
    container_name: navidrome
    environment:
      - ND_MUSICFOLDER=/music
      - ND_SCANSCHEDULE=1h
      - TZ=${TZ}
    volumes:
      - ${NVD}:/data
      - ${DATA}/music:/music:ro
    ports:
      - "4533:4533"
    restart: unless-stopped
需要的变量(.env)
TZ=Asia/Shanghai
DATA=/vol1/1000/data
NVD=${ROOT}/navidrome
ROOT=/vol1/1000/docker

家庭监控与看护 6 段

version: "3.8"
services:
  mosquitto:
    image: eclipse-mosquitto:2
    restart: always
    ports: ["1883:1883", "9001:9001"]
    volumes: ["./mosquitto/config:/mosquitto/config", "./mosquitto/data:/mosquitto/data"]

  jellyfin:
    image: jellyfin/jellyfin
    restart: always
    ports: ["8096:8096"]
    volumes: ["./jellyfin/config:/config", "./jellyfin/cache:/cache", "/volume1/media:/media"]
    devices: ["/dev/dri/renderD128:/dev/dri/renderD128"]

  rtsp-server:
    image: bluenviron/mediamtx
    restart: always
    ports: ["8554:8554"]
    volumes: ["./mediamtx:/config", "/volume1/camera_record:/recordings"]

  hermes-ai:
    image: lmsys/fastchat
    restart: always
    ports: ["8080:8080"]
    volumes: ["/volume1/photo:/nas_photo", "/volume1/media:/nas_media"]

  n8n:
    image: n8nio/n8n
    restart: always
    ports: ["5678:5678"]
    volumes: ["./n8n:/root/.n8n"]
    environment: [N8N_SECURE_COOKIE=false]

  samba-game:
    image: dperson/samba
    restart: always
    ports: ["139:139", "445:445"]
    volumes: ["/volume1/game_library:/game"]
    environment: [TZ=Asia/Shanghai]
    command: >
      -u "gameuser;GamePass123"
      -s "GameLibrary;/game;yes;no;no;gameuser;gameuser;NAS全屋游戏库"

docker-compose up -d
services:
  frigate:
    image: ghcr.io/blakeblackshear/frigate
    container_name: frigate
    restart: unless-stopped
    shm_size: "256mb"
    devices:
      - /dev/bus/usb:/dev/bus/usb
      - /dev/dri:/dev/dri
    volumes:
      - ./config:/config
      - ./media:/media/frigate
      - /etc/localtime:/etc/localtime:ro
    ports:
      - 5000:5000
      - 8971:8971
      - 8554:8554
      - 8555:8555/tcp
    environment:
      TZ: Asia/Shanghai
      FRIGATE_RTSP_PASSWORD: pwd
services:
  mosquitto:
    image: eclipse-mosquitto:2
    container_name: mosquitto
    restart: unless-stopped
    volumes:
      - ./config/mosquitto/config:/mosquitto/config
      - ./config/mosquitto/data:/mosquitto/data
    ports:
      - "1883:1883"
    networks:
      - aihub

  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: unless-stopped
    environment:
      - OLLAMA_HOST=0.0.0.0
      - OLLAMA_KEEP_ALIVE=24h
      - OLLAMA_NUM_PARALLEL=1
      - OLLAMA_MAX_LOADED_MODELS=1
      - TZ=Asia/Shanghai
    volumes:
      - ./config/ollama:/root/.ollama
    ports:
      - "11434:11434"
    networks:
      - aihub

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    restart: unless-stopped
    depends_on:
      - ollama
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
      - FRIGATE_URL=http://frigate:5000
      - TZ=Asia/Shanghai
    volumes:
      - ./config/open-webui:/app/backend/data
    ports:
      - "3000:8080"
    networks:
      - aihub

  frigate:
    image: ghcr.io/blakeblackshear/frigate:stable
    container_name: frigate
    restart: unless-stopped
    stop_grace_period: 30s
    shm_size: "512mb"
    depends_on:
      - mosquitto
    devices:
      - /dev/dri/renderD128:/dev/dri/renderD128
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./config/frigate:/config
      - /vol2/1000/camera:/media/frigate
      - type: tmpfs
        target: /tmp/cache
        tmpfs:
          size: 1000000000
    ports:
      - "8971:8971"
      - "127.0.0.1:5000:5000"
      - "8554:8554"
      - "8555:8555/tcp"
      - "8555:8555/udp"
    environment:
      - FRIGATE_RTSP_PASSWORD=摄像头密码
      - TZ=Asia/Shanghai
    networks:
      - aihub

  double-take:
    image: ghcr.io/jakowenko/double-take:latest
    container_name: double-take
    restart: unless-stopped
    depends_on:
      - mosquitto
      - frigate
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - ./config/double-take:/.storage
    ports:
      - "3001:3000"
    networks:
      - aihub

  care-bridge:
    build: ./bridge
    container_name: care-bridge
    restart: unless-stopped
    depends_on:
      - mosquitto
      - ollama
      - frigate
    environment:
      - TZ=Asia/Shanghai
      - MQTT_HOST=mosquitto
      - MQTT_PORT=1883
      - MQTT_USER=mqtt
      - MQTT_PASSWORD=mqtt123
      - FRIGATE_URL=http://frigate:5000
      - OLLAMA_URL=http://ollama:11434
      - OLLAMA_MODEL=qwen3-vl:4b
      - CARE_CAMERA=keting
      - CARE_NIGHT_START=22
      - CARE_NIGHT_END=6
      - STILL_ALERT_SECONDS=600
      - DOWN_HOLD_SECONDS=20
      - DAILY_REPORT_HOUR=20
      - TELEGRAM_BOT_TOKEN=填你的机器人Token
      - TELEGRAM_CHAT_ID=填你的ChatID
      - DB_PATH=/data/care.db
      - LOG_PATH=/data/care.log
    volumes:
      - ./data:/data
    networks:
      - aihub

networks:
  aihub:
    driver: bridge

cd /vol2/1000/aihub
docker compose up -d --build
mqtt:
  host: mosquitto
  username: mqtt
  password: mqtt123
  topics:
    frigate: frigate/events
    matches: double-take/matches
    cameras: double-take/cameras

frigate:
  url: http://frigate:5000
  update_sub_labels: false
  stop_on_match: true
  min_area: 5000
  labels:
    - person
  attempts:
    latest: 10
    snapshot: 10
    mqtt: true
    delay: 0
  image:
    height: 500

detect:
  match:
    save: true
    confidence: 60
    min_area: 10000
  unknown:
    save: true
    confidence: 40

detectors:
  compreface:
    url: http://192.168.31.200:8000/api/v1
    key: 上一步复制的APIKey
    timeout: 15
    det_prob_threshold: 0.8
    opencv_face_required: false
    cameras:
      - menkou
      - keting
"""
title: 家庭安防查询
author: 辣丝刀客
version: 0.1.0
license: MIT
requirements: requests
"""

import os
from datetime import datetime, timedelta

import requests


FRIGATE_URL = os.getenv("FRIGATE_URL", "http://frigate:5000")


class Tools:

    def __init__(self):
        self.citation = False

    def get_recent_person_events(self, camera: str = "", limit: int = 10) -> str:
        """
        查询最近的人形监控事件。camera 参数传摄像头名称(如 menkou、keting),
        留空表示查全部摄像头。limit 是返回条数,默认 10。
        """
        params = {"limit": int(limit)}
        if camera:
            params["camera"] = camera
        resp = requests.get(f"{FRIGATE_URL}/api/events", params=params, timeout=20)
        resp.raise_for_status()
        events = resp.json()
        if not events:
            return "最近没有任何人形事件。"

        lines = []
        for item in events:
            name = item.get("sub_label") or "未识别"
            when = datetime.fromtimestamp(item.get("start_time", 0)).strftime("%m-%d %H:%M")
            lines.append(
                f"{when} {item.get('camera')} 检测到 {item.get('label')},人脸:{name}"
            )
        return "\n".join(lines)

    def request_daily_summary(self, hours: int = 12) -> str:
        """
        让 Frigate 用本地大模型给最近几小时的活动生成一份中文摘要报告。
        hours 是往前推的小时数,默认 12。
        """
        end_ts = int(datetime.now().timestamp())
        start_ts = int((datetime.now() - timedelta(hours=int(hours))).timestamp())
        resp = requests.post(
            f"{FRIGATE_URL}/api/review/summarize/start/{start_ts}/end/{end_ts}",
            timeout=60,
        )
        return f"已请求生成摘要,HTTP {resp.status_code}:{resp.text[:300]}"
services:
  go2rtc:
    image: alexxit/go2rtc:latest
    container_name: go2rtc
    restart: unless-stopped
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /vol4/1000/go2rtc:/config
    ports:
      - "1984:1984"
    networks:
      - nvr

  frigate:
    image: ghcr.io/blakeblackshear/frigate
    container_name: frigate
    restart: unless-stopped
    shm_size: "256mb"
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /vol4/1000/frigate:/config
      - /vol4/1000/nvr:/media/frigate
      - /dev/dri:/dev/dri
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "8971:8971"
    networks:
      - nvr

networks:
  nvr:
    driver: bridge

智能家居与语音 4 段

version: '3.8'
services:
  migpt:
    image: idootop/mi-gpt:latest
    container_name: migpt
    restart: always
    ports:
      - "36592:36592"
    volumes:
      - ./config:/app/config
      - ./data:/app/data
    environment:
      - MIGPT_AI_PLATFORM=doubao
      - "MIGPT_DOUBAO_API_KEY=\
        sk-你的火山方舟密钥"
      - "MIGPT_DOUBAO_MODEL=\
        ep-20251231181234-xxxx"
      - MIGPT_MI_USER_ID=你的小米UID
      - MIGPT_MI_DID=小爱音箱did
      - TZ=Asia/Shanghai
version: "3.8"

services:
  # NAS 本地豆包推理服务
  local-doubao:
    image: your-local-doubao:latest
    container_name: local-doubao
    restart: always
    ports:
      - "0.0.0.0:8080:8080"
    environment:
      # 模型目录,按实际路径改
      MODEL_PATH: "/models/doubao-offline"
      ENABLE_CLOUD_UPLOAD: "false"
      ENABLE_EXTERNAL_API: "false"
      INTRANET_ONLY: "true"
      LOG_LEVEL: "info"
    volumes:
      - ./doubao-models:/models
      - ./doubao-config:/config
      - ./doubao-logs:/logs
    networks:
      - lan-bridge
    deploy:
      resources:
        reservations:
          memory: 8G
    cap_drop:
      - NET_RAW

  # 米家本地桥接(内网控制)
  miot-local-bridge:
    image: miot-local-bridge:latest
    container_name: miot-local-bridge
    restart: always
    ports:
      - "0.0.0.0:8090:8090"
    environment:
      # 这里换成你的米家 TOKEN
      MIOT_TOKEN: "your-miot-token"
      DISABLE_MI_CLOUD: "true"
      SCAN_LAN_ONLY: "true"
      DEVICE_DB_PATH: "/db/devices.db"
    volumes:
      - ./miot-db:/db
      - ./miot-config:/config
    networks:
      - lan-bridge
    depends_on:
      - local-doubao

networks:
  lan-bridge:
    driver: bridge
services:
  homeassistant:
    image: ghcr.io/home-assistant/home-assistant:stable
    container_name: homeassistant
    restart: unless-stopped
    privileged: true            # 访问 USB 设备(Zigbee 网关等)必需
    network_mode: host          # 用 host 模式才能广播发现家里的设备
    volumes:
      - ./config:/config
    environment:
      TZ: Asia/Shanghai
services:
  homeassistant:
    image: ${HA}:stable
    container_name: homeassistant
    restart: unless-stopped
    network_mode: host
    privileged: true
    volumes:
      - ./config:/config
      - /run/dbus:/run/dbus:ro
      - /etc/localtime:/etc/localtime:ro
    env_file: .env
需要的变量(.env)
HA=ghcr.io/home-assistant/home-assistant

游戏与掌机 6 段

version: "3.8"
services:
steam-headless:
image: steamheadless/steam-headless:latest
container_name: Steam-Headless
restart: always
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
- VNC_PASSWD=123456
volumes:
- /mnt/SteamLibrary:/home/steam/.steam/steamapps
ports:
version: "3.8"
services:
  samba-game:
    image: dperson/samba
    container_name: samba-game
    restart: always
    ports:
      - "139:139"
      - "445:445"
    volumes:
      - /volume1/game_library:/game
    environment:
      - TZ=Asia/Shanghai
    command: >
      -u "gameuser;GamePass123"
      -s "GameLibrary;/game;yes;no;no;gameuser;gameuser;NAS全屋游戏库"
services:
  sunshine:
    image: lscr.io/linuxserver/sunshine:latest
    container_name: sunshine
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - /vol2/1000/docker/sunshine/config:/config
    ports:
      - 47984-48010:47984-48010/tcp
      - 47984-48010:47984-48010/udp
      - 47989:47989/tcp
      - 47990:47990/tcp
    devices:
      - /dev/dri:/dev/dri
    restart: unless-stopped
services:
  samba:
    image: dperson/samba:latest
    container_name: samba
    restart: unless-stopped
    ports:
      - "445:445"
      - "139:139"
    environment:
      - TZ=Asia/Shanghai
      - USERID=1000
      - GROUPID=1000
    volumes:
      - /vol2/1000/games:/games
    command:
      - -u
      - "handheld;改成你的强密码"
      - -s
      - "games;/games;yes;no;no;handheld"
services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:latest
    container_name: wg-easy
    environment:
      - LANG=zh
      - WG_HOST=你的域名或公网IP
      - WG_PORT=51820
      - WG_DEFAULT_ADDRESS=10.8.0.x
      - WG_DEFAULT_DNS=223.5.5.5
      - "WG_ALLOWED_IPS=\
        192.168.1.0/24,10.8.0.0/24"
      - WG_PERSISTENT_KEEPALIVE=25
      - "PASSWORD_HASH=\
        把上一步生成的哈希粘这里"
    volumes:
      - "/vol2/1000/docker/\
        wg-easy:/etc/wireguard"
    ports:
      - 51820:51820/udp
      - 51821:51821/tcp
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
      - net.ipv4.ip_forward=1
    restart: unless-stopped
services:
  samba:
    image: dockurr/samba:latest
    container_name: samba
    environment:
      - NAME=games
      - USER=nasuser
      - PASS=改成你的强密码
      - SHARE=/games
      - READONLY=false
      - PERMISSIONS=true
    volumes:
      - /vol2/1000/games:/games
    ports:
      - "445:445"
      - "139:139"
    restart: unless-stopped

  nfs:
    image: itsthenetwork/nfs-server-alpine
    container_name: nfs
    privileged: true
    environment:
      - SHARED_DIR=/games
      - PERMITTED=192.168.1.0/24
      - SYNC=true
    volumes:
      - /vol2/1000/games:/games
    ports:
      - "2049:2049"
    restart: unless-stopped

  syncthing:
    image: syncthing/syncthing:latest
    container_name: syncthing
    hostname: game-nas
    environment:
      - PUID=1000
      - PGID=1001
      - TZ=Asia/Shanghai
    volumes:
      - ./st:/var/syncthing
      - /vol2/1000/games/saves:/data/saves
    ports:
      - "8384:8384"
      - "22000:22000/tcp"
      - "22000:22000/udp"
      - "21027:21027/udp"
    restart: unless-stopped

  tailscale:
    image: tailscale/tailscale:latest
    container_name: tailscale
    hostname: game-nas
    environment:
      - TS_AUTHKEY=改成你的密钥
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ./ts:/var/lib/tailscale
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - NET_ADMIN
      - NET_RAW
    restart: unless-stopped

cd /vol2/1000/docker/games
docker compose up -d
docker compose logs -f samba

远程访问与穿透 1 段

services:
  tailscale:
    image: tailscale/tailscale:latest
    container_name: tailscale
    hostname: nas-gateway
    environment:
      - "TS_AUTHKEY=\
        tskey-auth-换成你自己的密钥"
      - TS_STATE_DIR=/var/lib/tailscale
      - "TS_EXTRA_ARGS=\
        --advertise-routes=\
        192.168.1.0/24"
      - TS_AUTH_ONCE=true
    volumes:
      - "/vol2/1000/docker/tailscale/\
        state:/var/lib/tailscale"
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - NET_ADMIN
      - NET_RAW
    network_mode: host
    restart: unless-stopped

备份与迁移 3 段

docker-compose 完整配置,改两个地方就能用:
services:
  syncthing:
    image: lscr.io/linuxserver/syncthing:latest
    container_name: syncthing
    hostname: syncthing
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - /vol2/1000/docker/syncthing/config:/config
      - /vol2/1000/docker/syncthing/saves:/data
    ports:
      - 8384:8384          # Web 管理界面
      - 22000:22000/tcp    # 同步端口
      - 22000:22000/udp
      - 21027:21027/udp    # 本地发现
    restart: unless-stopped
version: "3.8"

services:
  emu-sync:
    image: syncthing/syncthing:1.27
    container_name: emu-sync
    restart: always
    environment:
      # 改成 NAS 上的实际用户 ID
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    ports:
      # 8384 是管理界面端口
      - "8384:8384"
      - "22000:22000/tcp"
      - "22000:22000/udp"
      - "21027:21027/udp"
    volumes:
      # 配置目录(长格式,避免行超宽)
      - type: bind
        source: ./syncthing/config
        target: /var/syncthing/config
      - /vol1/1000/emu-saves:/emu-saves
services:
  syncthing:
    image: syncthing/syncthing:latest
    container_name: syncthing
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
      - /vol4/1000/sync:/var/syncthing
      - /vol4/1000/album:/data/photos
    ports:
      - "8384:8384"
      - "22000:22000/tcp"
      - "22000:22000/udp"
      - "21027:21027/udp"
    networks:
      - home

networks:
  home:
    driver: bridge
mkdir -p /vol4/1000/sync
mkdir -p /vol4/1000/album
docker compose up -d

AI 与自动化 9 段

services:
  postgres:
    image: postgres:17-alpine
    container_name: mumuainovel-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: mumuai_novel
      POSTGRES_USER: mumuai
      POSTGRES_PASSWORD: "改成你的强密码"
      TZ: Asia/Shanghai
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
    networks:
      - mumuainovel-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U mumuai -d mumuai_novel"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
  mumuainovel:
    image: mumujie/mumuainovel:latest
    container_name: mumuainovel
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "8832:8000"
    volumes:
      - ./data:/app/data
      - ./logs:/app/logs
      - ./.env:/app/.env
    networks:
      - mumuainovel-net
    environment:
      TZ: Asia/Shanghai
      DATABASE_HOST: postgres
      DATABASE_PORT: "5432"
      DATABASE_NAME: mumuai_novel
      DATABASE_USER: mumuai
      DATABASE_PASSWORD: "改成你的强密码"
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
networks:
  mumuainovel-net:
    driver: bridge
version: "3.8"

services:
  dsh:
    image: node:24-alpine
    container_name: deepseek-harness
    restart: always
    ports:
      - "3080:3080"
    environment:
      - TZ=Asia/Shanghai
      - DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
      - DSH_HOST=0.0.0.0
      - DSH_PORT=3080
    volumes:
      - ./config:/root/.dsh
      - ./workspace:/app/workspace
    command: >
      sh -c "npm install -g @deepseek-ai/dsh && dsh web --host 0.0.0.0 --port 3080"
version: "3.8"

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: unless-stopped
    ports:
      - "11434:11434"
    volumes:
      - ./ollama:/root/.ollama
    environment:
      - OLLAMA_HOST=0.0.0.0
      - OLLAMA_NUM_PARALLEL=4
      - OLLAMA_MAX_LOADED_MODELS=2
    networks:
      - arr_net

networks:
  arr_net:
    external: true
open-webui:
    image: ghcr.io/open-webui/open-webui:latest
    container_name: open-webui
    restart: unless-stopped
    ports:
      - "3000:8080"
    volumes:
      - ./open-webui:/app/backend/data
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    depends_on:
      - ollama
    networks:
      - arr_net

http://NAS-IP:3000
version: "3.8"

services:
  hermes:
    image: nousresearch/hermes-agent:latest
    container_name: hermes
    restart: unless-stopped
    command: gateway run
    ports:
      - "26429:26429"
    volumes:
      - ./hermes_data:/opt/data
      - /etc/localtime:/etc/localtime:ro
    environment:
      # 本地Ollama模型对接
      - MODEL_PROVIDER=ollama
      - MODEL_BASE_URL=http://ollama:11434/v1
      - MODEL_NAME=qwen3:8b
      - MODEL_API_KEY=dummy

      # AI自我训练/自我进化开关
      - SELF_IMPROVEMENT_ENABLE=true
      - SELF_IMPROVEMENT_MAX_PATCHES=3
      - SELF_IMPROVEMENT_ALLOW_FILE_WRITE=true
      - SELF_IMPROVEMENT_ALLOW_COMMAND=false

      # 微信安全策略(防风控、防乱入)
      - WEIXIN_DM_POLICY=pairing_approval
      - WEIXIN_GROUP_POLICY=disable
    deploy:
      resources:
        limits:
          memory: 6G
          cpus: "2.0"
    networks:
      - arr_net

networks:
  arr_net:
    external: true

docker compose up -d
http://你的NAS-IP:26429
services:
  hermes:
    image: nousresearch/hermes:latest
    ports:
      - "18642:18642"
      - "19119:19119"
    env_file:
      - ./.env
    volumes:
      - ./hermes-data:/root/.hermes
    restart: unless-stopped
version: "3.8"
services:
  服务自定义名:
    image: 镜像地址
    container_name: 容器名
    ports:
      - "主机端口:容器端口"
    volumes:
      - /你的NAS本地路径:/容器内路径
    environment:
      - TZ=Asia/Shanghai
services:
  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      - TZ=Asia/Shanghai
      - GENERIC_TIMEZONE=Asia/Shanghai
      - N8N_HOST=n8n.example.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=http
      - WEBHOOK_URL=http://n8n.example.com/
      - N8N_ENCRYPTION_KEY=change_me_random_32_char_string
    volumes:
      - ./n8n_data:/home/node/.n8n
services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: unless-stopped
    ports:
      - 11434:11434
    volumes:
      - ./models:/root/.ollama

效率工具与全家桶 22 段

services:
  libretv:
    image: ghcr.io/librespark/libretv
    container_name: libretv
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      # 访问密码(必填,改成自己的)
      - PASSWORD=change-me
      # 会话签名密钥(多实例必设)
      - PROXY_SECRET=change-me-too
      # 自建内网IPTV源才开
      # - LIVE_ALLOW_PRIVATE=1
      # 上游超时(毫秒)
      # - REQUEST_TIMEOUT=8000
      # 每源抓取页数
      # - SEARCH_MAX_PAGES=5
      # 排错日志
      # - DEBUG=1
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.你的域名.com"   # 有域名就填,自用可留空
      SIGNUPS_ALLOWED: "true"               # 注册完一个账号后记得改 false
      ADMIN_TOKEN: "改成你的一长串随机密码"    # 开启 /admin 后台管理
      WEBSOCKET_ENABLED: "true"            # 开启即可在浏览器自动填充
    volumes:
      - ./vw-data:/data
    ports:
      - 80:80
services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: unless-stopped
    ports:
      - 9443:9443     # HTTPS 管理界面,用浏览器打开会先要你设置管理员密码
      - 8000:8000     # Agent 端口(可选)
    volumes:
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock
services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    restart: unless-stopped
    environment:
      USER_UID: 1000
      USER_GID: 1000
      GITEA__server__DOMAIN: "NAS-IP:3000"
      GITEA__server__ROOT_URL: "http://NAS-IP:3000/"
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - 3000:3000
services:
  nextcloud:
    image: nextcloud:latest
    container_name: nextcloud
    restart: unless-stopped
    ports:
      - 8080:80
    volumes:
      - ./html:/var/www/html
      - ./data:/var/www/html/data
    environment:
      - NEXTCLOUD_ADMIN_USER=admin
      - NEXTCLOUD_ADMIN_PASSWORD=设置你的强密码
      - NEXTCLOUD_TRUSTED_DOMAINS=NAS-IP

  db:
    image: postgres:16-alpine
    container_name: nextcloud-db
    restart: unless-stopped
    environment:
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=nextcloud
      - POSTGRES_PASSWORD=给数据库设个独立密码
    volumes:
      - ./db:/var/lib/postgresql/data
services:
  linkwarden:
    image: ${LW}:latest
    container_name: linkwarden
    restart: unless-stopped
    env_file: .env
    ports:
      - 3000:3000
    volumes:
      - ./data:/data/data
    depends_on:
      - db
      - meili

  db:
    image: postgres:16-alpine
    container_name: lw-db
    restart: unless-stopped
    env_file: .env
    volumes:
      - ./pgdata:/var/lib/postgresql/data

  meili:
    image: getmeili/meilisearch:v1.11
    container_name: lw-meili
    restart: unless-stopped
    env_file: .env
    volumes:
      - ./meili:/meili_data
需要的变量(.env)
LW=ghcr.io/linkwarden/linkwarden
services:
  stirling-pdf:
    image: frooodle/s-pdf:latest
    container_name: stirling-pdf
    restart: unless-stopped
    ports:
      - 8181:8080
    environment:
      DOCKER_ENABLE_SECURITY: "false"
      INSTALL_BOOK_AND_ADVANCED: "true"
      LANGS: zh_CN
      TZ: Asia/Shanghai
    volumes:
      - ./configs:/configs
      - ./logs:/logs
      - ./pipeline:/pipeline
      - ./customFiles:/customFiles
services:
  mealie:
    image: ghcr.io/mealie-recipes/mealie
    container_name: mealie
    restart: unless-stopped
    ports:
      - 9925:9000
    environment:
      ALLOW_SIGNUP: "true"
      BASE_URL: http://10.0.0.2:9925
      TZ: Asia/Shanghai
    volumes:
      - ./data:/app/data
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    container_name: mealie-db
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: pw
      POSTGRES_USER: mealie
      POSTGRES_DB: mealie
      TZ: Asia/Shanghai
    volumes:
      - ./pgdata:/var/lib/postgresql/data
services:
  calibre-web:
    image: linuxserver/calibre-web
    container_name: calibre-web
    restart: unless-stopped
    ports:
      - 8083:8083
    environment:
      PUID: 1000
      PGID: 1000
      TZ: Asia/Shanghai
    volumes:
      - ./config:/config
      - ./books:/books
services:
  backrest:
    image: ghcr.io/garethgeorge/backrest
    container_name: backrest
    restart: unless-stopped
    ports:
      - 9898:9898
    environment:
      BACKREST_DATA: /data
      BACKREST_CONFIG: /data/config.json
      XDG_CACHE_HOME: /cache
      TZ: Asia/Shanghai
    volumes:
      - ./data:/data
      - ./cache:/cache
      - /vol1/1000:/userdata:ro
services:
  docmost:
    image: docmost/docmost:latest
    container_name: docmost
    restart: unless-stopped
    env_file: .env
    ports:
      - 3080:3000
    volumes:
      - ./data:/app/data/storage
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    container_name: docmost-db
    restart: unless-stopped
    env_file: .env
    volumes:
      - ./pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    container_name: docmost-redis
    restart: unless-stopped
    volumes:
      - ./redis:/data
services:
  webserver:
    image: ${APP}
    container_name: paperless
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      broker:
        condition: service_started
      gotenberg:
        condition: service_started
      tika:
        condition: service_started
    ports:
      - "8000:8000"
    volumes:
      - ${DATA}/data:${PB}/data
      - ${DATA}/media:${PB}/media
      - ${CONSUME}:${PB}/consume
      - ${DATA}/export:${PB}/export
    environment:
      - TZ
      - PUID
      - PGID
      - PAPERLESS_DBHOST
      - PAPERLESS_DBPORT
      - PAPERLESS_DBNAME
      - PAPERLESS_DBUSER
      - PAPERLESS_DBPASS
      - PAPERLESS_REDIS
      - PAPERLESS_SECRET_KEY
      - PAPERLESS_TIME_ZONE
      - PAPERLESS_OCR_LANGUAGE
      - PAPERLESS_URL
      - PAPERLESS_TIKA_ENABLED
      - PAPERLESS_TIKA_ENDPOINT
      - PAPERLESS_TIKA_GOTENBERG_ENDPOINT
    networks:
      - pl

  db:
    image: ${DB}
    container_name: paperless_db
    restart: unless-stopped
    environment:
      - TZ
      - POSTGRES_DB=paperless
      - POSTGRES_USER=paperless
      - POSTGRES_PASSWORD=${DBPASS}
    volumes:
      - ${DATA}/pgdata:${PGD}
    healthcheck:
      test: pg_isready -U paperless
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - pl

  broker:
    image: ${CACHE}
    container_name: paperless_redis
    restart: unless-stopped
    volumes:
      - ${DATA}/redis:/data
    networks:
      - pl

  gotenberg:
    image: ${GOT}
    container_name: paperless_gotenberg
    restart: unless-stopped
    networks:
      - pl

  tika:
    image: ${TIKA}
    container_name: paperless_tika
    restart: unless-stopped
    networks:
      - pl

networks:
  pl:
    driver: bridge
需要的变量(.env)
APP=${REG}/paperless-ngx:${TAG}
CACHE=docker.io/redis:7
CONSUME=/vol2/1000/scan
DATA=/vol2/1000/paperless
DB=docker.io/postgres:16
DBPASS=改成你的强数据库密码
GOT=docker.io/gotenberg/gotenberg:8
PB=/usr/src/paperless
PGD=/var/lib/postgresql/data
REG=ghcr.io/paperless-ngx
TAG=2.14.5
TIKA=docker.io/apache/tika:latest
services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - 3001:3001
    volumes:
      - ./data:/app/data
services:
  vaultwarden:
    image: vaultwarden/server
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      # 先开着,注册完改成 false
      - SIGNUPS_ALLOWED=true
      # 换成你的访问地址(必须 https)
      - DOMAIN=https://v.abc.cn
    ports:
      - 8081:80
    volumes:
      - ./data:/data
services:
  gitea:
    image: gitea/gitea:1
    container_name: gitea
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__server__DOMAIN=10.0.0.5
      - GITEA__server__SSH_PORT=2222
    ports:
      - 3002:3000
      - 2222:22
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
services:
  homepage:
    image: ghcr.io/gethomepage/homepage
    container_name: homepage
    restart: unless-stopped
    ports:
      - 3000:3000
    environment:
      - HOMEPAGE_ALLOWED_HOSTS=nas.lan
    volumes:
      - ./config:/app/config
services:
  dozzle:
    image: amir20/dozzle
    container_name: dozzle
    restart: unless-stopped
    ports:
      - 8083:8080
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
        read_only: true
services:
  stirling-pdf:
    image: stirlingtools/stirling-pdf
    container_name: stirling-pdf
    restart: unless-stopped
    ports:
      - 8084:8080
    environment:
      - DOCKER_ENABLE_SECURITY=false
      - LANGS=zh_CN
    volumes:
      - ./configs:/configs
      - ./logs:/logs
services:
  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: unless-stopped
    command: --label-enable --cleanup
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
        read_only: true
services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - 3001:3001
    volumes:
      - ./data:/app/data

  vaultwarden:
    image: vaultwarden/server
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      # 先开着,注册完改成 false
      - SIGNUPS_ALLOWED=true
      # 换成你的访问地址(必须 https)
      - DOMAIN=https://v.abc.cn
    ports:
      - 8081:80
    volumes:
      - ./data:/data

  gitea:
    image: gitea/gitea:1
    container_name: gitea
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__server__DOMAIN=10.0.0.5
      - GITEA__server__SSH_PORT=2222
    ports:
      - 3002:3000
      - 2222:22
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro

  navidrome:
    image: deluan/navidrome
    container_name: navidrome
    restart: unless-stopped
    ports:
      - 4533:4533
    environment:
      - ND_MUSICFOLDER=/music
      - ND_SCANSCHEDULE=1h
    volumes:
      - ./data:/data
      - /vol1/1000/music:/music:ro

  homepage:
    image: ghcr.io/gethomepage/homepage
    container_name: homepage
    restart: unless-stopped
    ports:
      - 3000:3000
    environment:
      - HOMEPAGE_ALLOWED_HOSTS=nas.lan
    volumes:
      - ./config:/app/config

  dozzle:
    image: amir20/dozzle
    container_name: dozzle
    restart: unless-stopped
    ports:
      - 8083:8080
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
        read_only: true

  stirling-pdf:
    image: stirlingtools/stirling-pdf
    container_name: stirling-pdf
    restart: unless-stopped
    ports:
      - 8084:8080
    environment:
      - DOCKER_ENABLE_SECURITY=false
      - LANGS=zh_CN
    volumes:
      - ./configs:/configs
      - ./logs:/logs

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: unless-stopped
    command: --label-enable --cleanup
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
        read_only: true
paperless:
    image: ${PAP}
    container_name: paperless
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
      - PAPERLESS_REDIS=${PLR}
      - PAPERLESS_URL=https://p.abc.cn
    volumes:
      - ${PPD}:/usr/src/paperless/data
      - ${PPM}:/usr/src/paperless/media
      - ${PPC}:/usr/src/paperless/consume
    ports:
      - "8000:8000"
    depends_on:
      - plredis
    restart: unless-stopped

  plredis:
    image: redis:7
    container_name: plredis
    restart: unless-stopped
需要的变量(.env)
PUID=1000
PGID=1000
TZ=Asia/Shanghai
DATA=/vol1/1000/data
GH=ghcr.io
PAP=${GH}/${PNX}/${PNX}:latest
PLR=redis://plredis:6379
PNX=paperless-ngx
PPC=${DATA}/scans
PPD=${ROOT}/paperless/data
PPM=${ROOT}/paperless/media
ROOT=/vol1/1000/docker
vaultwarden:
    image: vaultwarden/server
    container_name: vaultwarden
    environment:
      - SIGNUPS_ALLOWED=true
      - DOMAIN=https://v.abc.cn
      - TZ=${TZ}
    volumes:
      - ${VWD}:/data
    ports:
      - "8081:80"
    restart: unless-stopped

  filebrowser:
    image: filebrowser/filebrowser
    container_name: filebrowser
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ${DATA}/srv:/srv
      - ${FBD}:/database
      - ${FBC}:/config
    ports:
      - "8082:80"
    restart: unless-stopped
需要的变量(.env)
PUID=1000
PGID=1000
TZ=Asia/Shanghai
DATA=/vol1/1000/data
FBC=${ROOT}/filebrowser/cfg
FBD=${ROOT}/filebrowser/db
ROOT=/vol1/1000/docker
VWD=${ROOT}/vaultwarden

其他 3 段

services:
  app-db:
    image: postgres:16-alpine
    container_name: app-db
    environment:
      # 含 # 和 @ 必须加引号
      POSTGRES_PASSWORD: "a#b@c2026"
      POSTGRES_USER: "app"
      POSTGRES_DB: "appdb"
      # 数字也建议加引号
      PUID: "1000"
      TZ: "Asia/Shanghai"
      # 整段 URL 一起加引号
      APP_BASE_URL: "https://nas.a.cn"
# 确认容器里真实拿到的值
docker inspect app-db \
  --format '{{json .Config.Env}}'
services:
  app-db:
    image: postgres:16-alpine
    container_name: app-db
    environment:
      POSTGRES_PASSWORD: "a#b@c2026"
      POSTGRES_USER: "app"
      POSTGRES_DB: "appdb"
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -U app -d appdb
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  app:
    image: ghcr.io/hotio/demo-app
    container_name: demo-app
    depends_on:
      app-db:
        condition: service_healthy
    restart: unless-stopped
services:
  nginx:
    image: nginx:alpine
    container_name: nas-nginx
    ports:
      - "443:443"
    extra_hosts:
      # 让容器能访问宿主机
      - host.docker.internal:host-gateway