项目: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);
相册与照片 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
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