惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Know Your Adversary
Know Your Adversary
WordPress大学
WordPress大学
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
阮一峰的网络日志
阮一峰的网络日志
G
Google Developers Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
F
Fortinet All Blogs
博客园 - 聂微东
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
J
Java Code Geeks
Vercel News
Vercel News
N
Netflix TechBlog - Medium
大猫的无限游戏
大猫的无限游戏
MyScale Blog
MyScale Blog
罗磊的独立博客
博客园 - 三生石上(FineUI控件)
酷 壳 – CoolShell
酷 壳 – CoolShell
D
DataBreaches.Net
Hugging Face - Blog
Hugging Face - Blog
M
MIT News - Artificial intelligence
T
The Blog of Author Tim Ferriss
小众软件
小众软件
The GitHub Blog
The GitHub Blog
量子位
V
Visual Studio Blog
博客园_首页
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
C
CERT Recently Published Vulnerability Notes
The Cloudflare Blog
Spread Privacy
Spread Privacy
P
Proofpoint News Feed
T
Threat Research - Cisco Blogs
Simon Willison's Weblog
Simon Willison's Weblog
U
Unit 42
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
NISL@THU
NISL@THU
C
Cisco Blogs
T
Threatpost
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
Secure Thoughts
The Hacker News
The Hacker News
Attack and Defense Labs
Attack and Defense Labs
IT之家
IT之家
Help Net Security
Help Net Security
G
GRAHAM CLULEY
Jina AI
Jina AI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Outline Wiki 自架教學(一):Docker 安裝、Gitea OIDC 與 AI 知識庫準備
Let's Write · 2026-06-26 · via DEV Community

Outline Wiki 自架教學(一):Docker 安裝、Gitea OIDC 與 AI 知識庫準備

本篇要解決的問題

公司開發團隊裡,想架一套內部的知識庫,又要能支援 AI,常見的痛點有幾個:

  • 文件資料留在自己的機器上,不對外,以免洩露商業機密。
  • 想要加上 AI?得付 $$,比如 Notion,或是也可以自架的 Docmost。
  • Outline Wiki 裝起來很難,August 失敗過一次,最後是靠 Claude Opus 4.8 的輔助才成功。

本篇筆記文,會用一份 Docker Compose 檔,把 Outline,以及需要的 PostgreSQL、Redis、Caddy 全包進去,並針對上面這些坑一次閃掉。

另外也先說明本文裡的基本設定:

  1. 本機已裝好 Docker。
  2. 對內 PORT 是 3002,對外是 3001

關於 Outline + AI 的補充說明:

自架版 Outline 沒有內建的「AI Answers」功能,網路上常看到的 OPENAI_API_KEY 對自架版是無效的。想要 Outline 加上 AI,正解是用 MCP 外接,讓 Claude / Codex 透過 Outline 的 API 來操作(作法將寫在下一篇)。


步驟 1:安裝 Gitea

第一個管理員帳號要用 Gitea 登入,所以得先有一台 Gitea。

Gitea 本身也能用 Docker 自架,這部分 August 之前寫過,直接看這篇:

安裝 Gitea

只要你的 Gitea 已經能正常登入,就可以往下走。

Outline 並不限定 Gitea,任何相容 OIDC 的供應商(Keycloak、Authentik、Google⋯)都行,差別只在 OIDC_* 那幾個網址。


步驟 2:從 Gitea 取得 Client ID 與 Secret

Outline 走 OIDC 登入,需要在 Gitea 建立一個「OAuth2 應用程式」,拿到 Client IDClient Secret

進入 Gitea 後,到 設定(Settings)→ 應用程式(Applications),找到「管理 OAuth2 應用程式」區塊。

在 Gitea 的「設定 → 應用程式」頁面,找到「管理 OAuth2 應用程式」的建立表單

在 Gitea 的「設定 → 應用程式」頁面,找到「管理 OAuth2 應用程式」的建立表單

需要填兩個欄位:

  • 應用程式名稱:填一個自己記得註是做什麼的名稱,如:Outline Wiki Login
  • 重新導向 URI:要填的 PORT 是有對外加上憑證,比如本篇用的是 3001
https://<你的IP或網域>:3001/auth/oidc.callback

例如主機 IP 是 192.168.11.111,就填 https://192.168.11.111:3001/auth/oidc.callback。這個 IP 與埠號要跟等一下 Compose 裡的 URL= 完全一致。

Gitea OAuth2 應用程式表單,填入應用程式名稱與重新導向 URI(重點是 Redirect URI 結尾為 /auth/oidc.callback)

Gitea OAuth2 應用程式表單,填入應用程式名稱與重新導向 URI(重點是 Redirect URI 結尾為 /auth/oidc.callback)

按下「建立應用程式」後,Gitea 會顯示 Client IDClient Secret

Gitea 建立完成後顯示的 Client ID 與 Client Secret 畫面。

Gitea 建立完成後顯示的 Client ID 與 Client Secret 畫面。

要注意,Client Secret 只會完整顯示這一次,先複製存好。等一下要填進 Compose 的 OIDC_CLIENT_IDOIDC_CLIENT_SECRET


步驟 3-1:安裝 Outline Wiki:Windows 版

Windows 用 Docker Desktop,這裡用具名 volume(由 Docker 管理)存附件,啟動最單純。

先新增一個空資料夾,接著我們要放進下面的 2 個檔案。

Caddyfile

資料夾內新增一個檔案,檔名:Caddfile

Caddyfile 負責「對外用 HTTPS、把流量轉給 Outline」。

檔案內容複製貼上以下,再把 192.168.11.111 換成我們 Windows 主機的區網 IP:

{
  default_sni 192.168.11.111
}

192.168.11.111:3001 {
  tls internal
  reverse_proxy outline:3002
}

  • tls internal:Caddy 自己簽一張憑證。因為我們用的是區網 IP,本來就申請不到正式憑證,自簽夠用。
  • 3001 是對外埠、outline:3002 是 Outline 容器內部埠,兩者分工,注意不能寫成一樣。

docker-compose.yml

資料夾內再新增一個檔案,檔名:docker-compose.yml

檔案內容複製貼上以下:

# =============================================================================
# Outline Wiki — Docker Compose 教學範例
# =============================================================================
# 架構:
#   outline   → 主應用 (Node.js)
#   caddy     → 反向代理 + 自動 HTTPS
#   postgres  → 資料庫
#   (外部)    → Redis,負責快取 / WebSocket / 背景工作佇列
#
# ⚠️ 發佈前務必把所有 <...> 佔位符換成你自己的值。
#    範例裡的密鑰、密碼、IP 全是假的,不要直接拿去用。
# =============================================================================

services:
  # ---------------------------------------------------------------------------
  # Outline 主應用
  # ---------------------------------------------------------------------------
  outline:
    image: outlinewiki/outline:latest
    container_name: outline
    restart: unless-stopped
    environment:
      - NODE_ENV=production

      # --- 安全密鑰 (必填) ---------------------------------------------------
      # 兩把都要用 32-byte 隨機 hex 字串。產生方式:
      #   openssl rand -hex 32
      # ⚠️ 換掉後不要再改,改了會讓既有 session / 加密資料失效。
      - SECRET_KEY=<openssl rand -hex 32 產生>
      - UTILS_SECRET=<openssl rand -hex 32 產生>

      # --- 資料庫連線 (必填) -------------------------------------------------
      # 格式:postgres://帳號:密碼@主機:埠/資料庫名
      # 主機名 postgres = 下面那個 service 名稱,Docker 內部 DNS 解析。
      - DATABASE_URL=postgres://outline_user:<DB_密碼>@postgres:5432/outline
      - PGSSLMODE=disable   # 同一台 Docker 內部連線,不需 SSL

      # --- Redis 連線 (必填) -------------------------------------------------
      # 格式:redis://:密碼@主機:埠/DB編號
      # 這裡指向下面內建的 redis service,主機名 redis = service 名。
      - REDIS_URL=redis://redis:6379/1

      # --- 對外網址 (必填) ---------------------------------------------------
      # 使用者瀏覽器看到的完整網址,要跟 Caddy / 反代對外位置一致。
      - URL=https://<你的網域或IP>:8443
      - PORT=3019            # 容器內部監聽埠 (給 Caddy 反代用,不直接對外)
      - FORCE_HTTPS=false    # HTTPS 由 Caddy 處理,這裡關掉避免重導迴圈

      # --- 檔案儲存 (必填) ---------------------------------------------------
      # local = 存本機磁碟 (搭配下面 volumes)。也可改 s3 接物件儲存。
      - FILE_STORAGE=local
      - FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
      # 上傳/匯入大小上限 (bytes)。預設約 1MB,匯入大 zip (如從
      # Docmost/Notion 匯出) 會跳「file too large」。調大解決。
      # 算法: MB × 1048576。下面 = 250MB。
      - FILE_STORAGE_UPLOAD_MAX_SIZE=262144000            # 附件上限
      - FILE_STORAGE_IMPORT_MAX_SIZE=262144000            # 單篇匯入
      - FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE=262144000  # zip 整包匯入 (關鍵)

      # --- 登入: Gitea OIDC (選填) -------------------------------------------
      # 任何相容 OIDC 的供應商都行 (Gitea / Keycloak / Authentik / Google...)。
      # client id/secret 從你的 OIDC 供應商後台拿。
      - OIDC_CLIENT_ID=<OIDC_CLIENT_ID>
      - OIDC_CLIENT_SECRET=<OIDC_CLIENT_SECRET>
      - OIDC_AUTH_URI=http://<OIDC主機>/login/oauth/authorize
      - OIDC_TOKEN_URI=http://<OIDC主機>/login/oauth/access_token
      - OIDC_USERINFO_URI=http://<OIDC主機>/login/oauth/userinfo
      - OIDC_USERNAME_CLAIM=preferred_username   # 用哪個欄位當帳號
      - OIDC_DISPLAY_NAME=Gitea                   # 登入按鈕顯示文字
      - OIDC_SCOPES=openid profile email

      # --- SMTP 寄信 (選填) --------------------------------------------------
      # 用途:magic-link 登入信 + 通知信。
      # Gmail 範例:密碼要用「應用程式密碼」,不是帳號密碼。
      # (Google 帳號 → 安全性 → 兩步驟驗證 → 應用程式密碼)
      - SMTP_HOST=smtp.gmail.com
      - SMTP_PORT=587
      - SMTP_SECURE=false    # 587 用 STARTTLS,所以這裡 false
      - SMTP_USERNAME=<你的gmail@gmail.com>
      - SMTP_PASSWORD=<Gmail 應用程式密碼>
      - SMTP_FROM_EMAIL=<你的gmail@gmail.com>
      - SMTP_REPLY_EMAIL=<你的gmail@gmail.com>

    volumes:
      # 用具名 volume,由 Docker 管理。注意:這裡存的是「附件/上傳檔」
      # (圖片、PDF...),不是文章本體。文章內容存在 PostgreSQL,不是 .md 檔。
      - outline_data:/var/lib/outline/data
    depends_on:
      postgres:
        condition: service_healthy   # 等 DB 健康檢查通過才啟動 outline
      redis:
        condition: service_healthy   # 等 Redis 就緒

  # ---------------------------------------------------------------------------
  # Redis — 快取 / WebSocket / 背景工作佇列 (內建獨立 service)
  # ---------------------------------------------------------------------------
  redis:
    image: redis:7-alpine
    container_name: outline_redis
    restart: unless-stopped
    volumes:
      - redis_data:/data   # 持久化 (選用,Outline 主要當快取,掉了也能重建)
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]   # 回 PONG 才算就緒
      interval: 10s
      timeout: 5s
      retries: 5

  # ---------------------------------------------------------------------------
  # Caddy — 反向代理,自動處理 HTTPS 憑證
  # ---------------------------------------------------------------------------
  caddy:
    image: caddy:2
    container_name: outline_caddy
    restart: unless-stopped
    ports:
      - "8443:8443"   # 對外開的埠 (主機:容器)。實際反代規則寫在 Caddyfile。
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro   # :ro = 唯讀掛載
      - caddy_data:/data       # 存 TLS 憑證,別刪,刪了要重新申請
      - caddy_config:/config
    depends_on:
      - outline

  # ---------------------------------------------------------------------------
  # PostgreSQL
  # ---------------------------------------------------------------------------
  postgres:
    image: pgvector/pgvector:pg16
    container_name: outline_postgres
    restart: unless-stopped
    environment:
      # 這三個要跟上面 DATABASE_URL 裡的帳號/密碼/DB名一致。
      - POSTGRES_USER=outline_user
      - POSTGRES_PASSWORD=<DB_密碼>
      - POSTGRES_DB=outline
    volumes:
      - pg_data:/var/lib/postgresql/data   # 資料庫實體資料,千萬別誤刪
    ports:
      # 只綁 127.0.0.1 = 僅本機可連,不對外曝露 DB。安全做法。
      - "127.0.0.1:5432:5432"
    healthcheck:
      # 確認 DB 真的可接受連線,outline 才會啟動 (見上面 depends_on)。
      test: ["CMD-SHELL", "pg_isready -U outline_user -d outline"]
      interval: 10s
      timeout: 5s
      retries: 5

# -----------------------------------------------------------------------------
# 具名 volume — 由 Docker 管理,容器刪掉資料還在。
# -----------------------------------------------------------------------------
volumes:
  outline_data:
  pg_data:
  redis_data:
  caddy_data:
  caddy_config:

接著要把所有 <...> 的部份換成自己的值,要改這幾項:

  • SECRET_KEYUTILS_SECRET:開啟終端機,執行兩次 openssl rand -hex 32 產生 32-byte 隨機字串。沒有 openssl 的話,那就用 Claude、ChatGPT 等 AI,對話中讓它們生成 2 組使用。
  • DATABASE_URLPOSTGRES_PASSWORD:兩邊的密碼要一致。
  • URL:填 https://<你的IP>:3001,IP 要跟 Caddyfile 一樣。
  • OIDC_CLIENT_IDOIDC_CLIENT_SECRET:填上一段從 Gitea 拿到的值。
  • OIDC_AUTH_URI / OIDC_TOKEN_URI / OIDC_USERINFO_URI:把 <OIDC主機> 換成 Gitea 網址。
  • (選填)SMTP:要寄邀請信 / magic-link 才需要填。

啟動

docker-compose.yml 所在資料夾開終端機:

docker compose up -d

第一次啟動 Outline 會自動跑資料庫遷移(migration),會需要多等個十幾秒。

看到 outline 正常起來後,瀏覽器打開 https://<你的IP>:3001

瀏覽器會跳「您的連線不是私人連線 / 不安全」警告。

這是因為 tls internal 用的是自簽憑證,屬於正常現象。點「進階 → 繼續前往」就能進到 Outline。

進站後點 Continue with Gitea 用 Gitea 帳號登入,第一個登入的人就是管理員。

之後的成員,在 Outline 後台用 Email 邀請即可(這也是為什麼上面要設定 SMTP)。


步驟 3-2:安裝 Outline Wiki:Ubuntu 版

Ubuntu 版跟 Windows 版幾乎一樣,只有兩個差別:

  1. 附件改用 bind mount(./data:附件直接落在專案資料夾,搬家 / 備份複製整個資料夾就帶走,不必像具名 volume 還要 tar 撈。
  2. OIDC 的 Gitea 可以在另一台機器,只要這台 Ubuntu 連得到即可。

檔案

一樣是兩個檔案:docker-compose.ymlCaddyfile

Caddyfile 內容(把 IP 換成你的 Ubuntu 主機 IP):

{
  default_sni 192.168.11.111
}

192.168.11.111:3001 {
  tls internal
  reverse_proxy outline:3002
}

docker-compose.yml 的內容複製貼上以下:

# =============================================================================
# Outline Wiki — Ubuntu 單主機部署版 (教學範例)
# =============================================================================
# 跟 Windows 版的差異:
#   1. 內建 Redis service — 不依賴外部 Redis 主機,一台 Ubuntu 全包。
#   2. 全部服務同一台主機,對外只露 Caddy 的 8443。
#   3. OIDC 仍可指向「另一台」Gitea/OIDC 主機,只要這台網路連得到。
#
# 架構:
#   outline   → 主應用 (Node.js)
#   redis     → 快取 / WebSocket / 背景佇列
#   caddy     → 反向代理 + 自動 HTTPS
#   postgres  → 資料庫
#
# ⚠️ 發佈前把所有 <...> 佔位符換成你自己的值。範例的密鑰/密碼/IP 全是假的。
#
# 部署步驟:
#   1. 整個資料夾複製到 Ubuntu (含 Caddyfile)。
#   2. 全域搜尋取代 <你的Ubuntu主機IP> → 實際 IP。
#   3. docker compose up -d
# =============================================================================

services:
  # ---------------------------------------------------------------------------
  # Outline 主應用
  # ---------------------------------------------------------------------------
  outline:
    image: outlinewiki/outline:latest
    container_name: outline
    restart: unless-stopped
    environment:
      - NODE_ENV=production

      # --- 安全密鑰 (必填) ---------------------------------------------------
      # 兩把都用 32-byte 隨機 hex。產生: openssl rand -hex 32
      # ⚠️ 換掉後別再改,改了會讓既有 session / 加密資料失效。
      - SECRET_KEY=<openssl rand -hex 32 產生>
      - UTILS_SECRET=<openssl rand -hex 32 產生>

      # --- 資料庫連線 (必填) -------------------------------------------------
      # 格式:postgres://帳號:密碼@主機:埠/資料庫名
      # 主機名 postgres = 下面的 service 名,Docker 內部 DNS 解析。
      - DATABASE_URL=postgres://outline_user:<DB_密碼>@postgres:5432/outline
      - PGSSLMODE=disable   # 同台 Docker 內部連線,不需 SSL

      # --- Redis 連線 (必填) -------------------------------------------------
      # ★ Ubuntu 版重點:指向「內建」redis service,主機名 redis = service 名。
      #   不像基本版要填外部 Redis IP/密碼。
      - REDIS_URL=redis://redis:6379/1

      # --- 對外網址 (必填) ---------------------------------------------------
      # 使用者瀏覽器看到的完整網址,要跟 Caddy 對外位置一致。
      - URL=https://<你的Ubuntu主機IP>:8443
      - PORT=3019            # 容器內部監聽埠 (給 Caddy 反向代理,不直接對外)
      - FORCE_HTTPS=false    # HTTPS 由 Caddy 處理,這裡關掉避免重導迴圈

      # --- 檔案儲存 (必填) ---------------------------------------------------
      - FILE_STORAGE=local
      - FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
      # 上傳/匯入大小上限 (bytes)。預設約 1MB,匯入大 zip 會跳「file too large」。
      # 算法: MB × 1048576。下面 = 250MB。
      - FILE_STORAGE_UPLOAD_MAX_SIZE=262144000            # 附件上限
      - FILE_STORAGE_IMPORT_MAX_SIZE=262144000            # 單篇匯入
      - FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE=262144000  # zip 整包匯入 (關鍵)

      # --- 登入: Gitea OIDC (選填) -------------------------------------------
      # 任何相容 OIDC 供應商皆可 (Gitea / Keycloak / Authentik / Google...)。
      # ★ Ubuntu 版注意:OIDC 主機可在「另一台」機器,確認這台 Ubuntu 連得到。
      - OIDC_CLIENT_ID=<OIDC_CLIENT_ID>
      - OIDC_CLIENT_SECRET=<OIDC_CLIENT_SECRET>
      - OIDC_AUTH_URI=http://<OIDC主機>/login/oauth/authorize
      - OIDC_TOKEN_URI=http://<OIDC主機>/login/oauth/access_token
      - OIDC_USERINFO_URI=http://<OIDC主機>/login/oauth/userinfo
      - OIDC_USERNAME_CLAIM=preferred_username   # 用哪個欄位當帳號
      - OIDC_DISPLAY_NAME=Gitea                   # 登入按鈕顯示文字
      - OIDC_SCOPES=openid profile email

      # --- SMTP 寄信 (選填) --------------------------------------------------
      # 用途:magic-link 登入信 + 通知信。
      # Gmail 範例:密碼要用「應用程式密碼」,不是帳號密碼。
      # (Google 帳號 → 安全性 → 兩步驟驗證 → 應用程式密碼)
      - SMTP_HOST=smtp.gmail.com
      - SMTP_PORT=587
      - SMTP_SECURE=false    # 587 用 STARTTLS,所以 false
      - SMTP_USERNAME=<你的gmail@gmail.com>
      - SMTP_PASSWORD=<Gmail 應用程式密碼>
      - SMTP_FROM_EMAIL=<你的gmail@gmail.com>
      - SMTP_REPLY_EMAIL=<你的gmail@gmail.com>

    volumes:
      # 存的是「附件/上傳檔」(圖片、PDF...),不是文章本體 (文章在 PostgreSQL)。
      # ★ 用 bind mount: 附件直接落在專案資料夾 ./data,搬家複製即帶走。
      #   對比 named volume (outline_data:) 要 tar 才能撈出,bind 好搬很多。
      #   注意:DB 不要 bind,DB 檔不能直接 copy,一律用 pg_dump 備份。
      - ./data:/var/lib/outline/data
    depends_on:
      postgres:
        condition: service_healthy   # 等 DB 健康檢查通過才啟動
      redis:
        condition: service_healthy   # 等 Redis 就緒

  # ---------------------------------------------------------------------------
  # Redis — 內建獨立 service (★ Ubuntu 版才有,基本版用外部 Redis)
  # ---------------------------------------------------------------------------
  redis:
    image: redis:7-alpine
    container_name: outline_redis
    restart: unless-stopped
    volumes:
      - redis_data:/data   # 持久化 (選用,Outline 主要當快取,掉了也能重建)
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]   # 回 PONG 才算就緒
      interval: 10s
      timeout: 5s
      retries: 5

  # ---------------------------------------------------------------------------
  # Caddy — 反向代理,自動處理 HTTPS 憑證
  # ---------------------------------------------------------------------------
  caddy:
    image: caddy:2
    container_name: outline_caddy
    restart: unless-stopped
    ports:
      - "8443:8443"   # 對外唯一開的埠 (主機:容器)。反代規則寫在 Caddyfile。
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro   # :ro = 唯讀掛載
      - caddy_data:/data       # 存 TLS 憑證,別刪,刪了要重新申請
      - caddy_config:/config
    depends_on:
      - outline

  # ---------------------------------------------------------------------------
  # PostgreSQL — pgvector 映像 (支援向量搜尋)
  # ---------------------------------------------------------------------------
  postgres:
    image: pgvector/pgvector:pg16
    container_name: outline_postgres
    restart: unless-stopped
    environment:
      # 這三個要跟上面 DATABASE_URL 的帳號/密碼/DB名一致。
      - POSTGRES_USER=outline_user
      - POSTGRES_PASSWORD=<DB_密碼>
      - POSTGRES_DB=outline
    volumes:
      - pg_data:/var/lib/postgresql/data   # 資料庫實體資料,千萬別誤刪
    ports:
      # 只綁 127.0.0.1 = 僅本機可連,不對外曝露 DB。安全做法。
      - "127.0.0.1:5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U outline_user -d outline"]
      interval: 10s
      timeout: 5s
      retries: 5

# -----------------------------------------------------------------------------
# 具名 volume — 由 Docker 管理,容器刪掉資料還在。
# -----------------------------------------------------------------------------
volumes:
  # outline_data 改用 bind mount (./data),不在這裡宣告。
  pg_data:
  redis_data:
  caddy_data:
  caddy_config:

要修改的部份,如:密鑰、DB、OIDC、SMTP,都與 Windows 版填法完全相同。

啟動

docker-compose.yml 所在資料夾開終端機:

docker compose up -d

確認與登入流程跟 Windows 版一樣:瀏覽器打開 https://<你的IP>:3001,遇到憑證警告點「繼續前往」,再用 Gitea 登入。


結語

照著上面的步驟做,我們就會有一套 完全自架 + 免費 + 可以用 MCP 接上 AI 的 Outline Wiki。

怎麼用 MCP 接上 Claude、Codex,下一篇會說明。