aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorshu <nvcsadjkojf@outlook.com>2026-07-27 00:22:33 +0800
committershu <nvcsadjkojf@outlook.com>2026-07-27 00:22:33 +0800
commitaaa0cf501ce44a7c7c54c7c5c6485cfb7f60e583 (patch)
treeda94ef66e9830d09961e577f87d36e63a6076ce5
parent338cc101f127e4841689c1f81227ee033e57ad28 (diff)
Add Docker Compose deployment
-rw-r--r--.env.example5
-rw-r--r--.gitignore5
-rw-r--r--DOCKER.md162
-rw-r--r--compose.yaml100
-rw-r--r--docker/authelia/configuration.template.yml66
-rw-r--r--docker/authelia/users_database.template.yml8
-rw-r--r--docker/caddy/Caddyfile44
-rw-r--r--docker/caddy/Dockerfile11
-rw-r--r--docker/tiddlywiki/Dockerfile13
-rwxr-xr-xscripts/init-docker.sh157
10 files changed, 571 insertions, 0 deletions
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..32ae7e5
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+BASE_DOMAIN=tdwiki.home.arpa
+PUID=1000
+PGID=1000
+TZ=Asia/Shanghai
+GOPROXY=https://goproxy.cn,direct
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fc56b0d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.env
+data/
+secrets/
+docker/authelia/configuration.yml
+docker/authelia/users_database.yml
diff --git a/DOCKER.md b/DOCKER.md
new file mode 100644
index 0000000..f39eb75
--- /dev/null
+++ b/DOCKER.md
@@ -0,0 +1,162 @@
+# Docker Compose 部署
+
+该部署包含四个服务:
+
+- `tiddlywiki`:运行 TiddlyWiki 5.4.1,只在 Compose 内网监听 `8080`。
+- `caddy`:唯一对外服务,提供内部 HTTPS、反向代理和 WebDAV。
+- `authelia`:统一处理 Wiki 浏览器登录和 WebDAV Basic 认证。
+- `redis`:保存 Authelia 登录会话。
+
+默认局域网域名:
+
+- Wiki:`https://wiki.tdwiki.home.arpa`
+- WebDAV:`https://dav.tdwiki.home.arpa`
+- 登录:`https://auth.tdwiki.home.arpa`
+
+## 初始化
+
+复制并按需修改环境配置:
+
+```bash
+cp .env.example .env
+```
+
+如果当前用户不是 UID/GID `1000`,修改 `.env` 中的 `PUID` 和 `PGID`:
+
+```bash
+id -u
+id -g
+```
+
+`GOPROXY` 只在构建带 WebDAV 插件的 Caddy 时使用。无法访问默认镜像源时可以在 `.env` 中替换为其他可信 Go 模块代理。
+
+运行初始化脚本,按提示设置个人用户名和密码:
+
+```bash
+./scripts/init-docker.sh
+```
+
+脚本会生成:
+
+- `secrets/` 中的 Authelia 随机密钥。
+- `docker/authelia/configuration.yml`。
+- `docker/authelia/users_database.yml`,密码只保存为 Argon2id 哈希。
+
+这些文件均被 `.gitignore` 排除。
+
+## 局域网 DNS
+
+在路由器、AdGuard Home、Pi-hole 或本机 hosts 中,将三个域名指向 Docker 主机 IP。
+
+例如 Docker 主机地址为 `192.168.1.20`:
+
+```text
+192.168.1.20 wiki.tdwiki.home.arpa
+192.168.1.20 dav.tdwiki.home.arpa
+192.168.1.20 auth.tdwiki.home.arpa
+```
+
+每台客户端都必须能够解析这些名称。
+
+## 启动
+
+```bash
+docker compose up -d --build
+docker compose ps
+```
+
+仅 Caddy 暴露宿主机的 `8081`(HTTP)和 `443`(HTTPS)。TiddlyWiki、Authelia、Redis 均不能从宿主机端口直接访问。日常直接使用上述 HTTPS 地址;HTTP 跳转入口为 `http://域名:8081`。
+
+## 信任内部证书
+
+Caddy 首次启动后会生成内部 CA:
+
+```text
+data/caddy/data/caddy/pki/authorities/local/root.crt
+```
+
+该目录由容器中的 root 用户管理。启动后将根证书导出为当前用户可读取的文件:
+
+```bash
+docker compose cp \
+ caddy:/data/caddy/pki/authorities/local/root.crt \
+ data/caddy/root.crt
+chmod 0644 data/caddy/root.crt
+```
+
+Linux 可以安装到系统信任库:
+
+```bash
+sudo cp data/caddy/root.crt \
+ /usr/local/share/ca-certificates/tdwiki-caddy.crt
+sudo update-ca-certificates
+```
+
+手机、Windows、macOS 和使用独立证书库的 Firefox 也需要导入该根证书。不要把 `root.key` 分发给任何客户端。
+
+## 登录
+
+访问 `https://wiki.tdwiki.home.arpa` 时会跳转到 Authelia。用户名和密码在同一个页面输入。
+
+Wiki 与 WebDAV 都使用 `one_factor`,因此 WebDAV 客户端可以使用同一个个人用户名和密码:
+
+```text
+https://dav.tdwiki.home.arpa/
+```
+
+WebDAV 的 Basic 凭据只通过 HTTPS 传输。客户端必须先信任 Caddy 内部 CA。
+
+可以使用 curl 验证:
+
+```bash
+curl --cacert data/caddy/root.crt \
+ -u '用户名:密码' \
+ -X PROPFIND \
+ -H 'Depth: 0' \
+ https://dav.tdwiki.home.arpa/
+```
+
+## 文件目录
+
+`files/` 同时挂载到:
+
+- Caddy:读写,作为 WebDAV 根目录。
+- TiddlyWiki:只读,用于 `/files/...` 和 `/api/files`。
+
+WebDAV 创建的文件可能由容器中的 root 用户持有。如果宿主机需要直接编辑,可在停止服务后修正权限:
+
+```bash
+sudo chown -R "$(id -u):$(id -g)" files
+```
+
+## 修改账户密码
+
+重新生成用户数据库并重启 Authelia:
+
+```bash
+./scripts/init-docker.sh --force-user
+docker compose restart authelia
+```
+
+## 更换域名
+
+修改 `.env` 的 `BASE_DOMAIN`,然后重新运行初始化脚本:
+
+```bash
+./scripts/init-docker.sh
+docker compose up -d --build
+```
+
+脚本会按新域名重新生成 Authelia 配置。对应 DNS 和客户端证书名称也必须同步更新。
+
+## 备份
+
+至少备份以下内容:
+
+- `tiddlers/`
+- `files/`
+- `data/authelia/`
+- `docker/authelia/users_database.yml`
+- `secrets/`
+
+`data/caddy/` 可以重新生成,但重新生成后客户端需要再次安装新的内部 CA。
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..5a909c3
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,100 @@
+name: tdwiki
+
+services:
+ tiddlywiki:
+ build:
+ context: .
+ dockerfile: docker/tiddlywiki/Dockerfile
+ restart: unless-stopped
+ init: true
+ user: "${PUID:-1000}:${PGID:-1000}"
+ command:
+ - .
+ - --listen
+ - host=0.0.0.0
+ - port=8080
+ - gzip=yes
+ - use-browser-cache=yes
+ volumes:
+ - ./tiddlers:/wiki/tiddlers
+ - ./files:/wiki/files:ro
+ - ./tiddlywiki.info:/wiki/tiddlywiki.info:ro
+ expose:
+ - "8080"
+ healthcheck:
+ test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/"]
+ interval: 10s
+ timeout: 3s
+ retries: 12
+ start_period: 10s
+ security_opt:
+ - no-new-privileges:true
+
+ redis:
+ image: redis:7.4-alpine
+ restart: unless-stopped
+ command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
+ volumes:
+ - redis-data:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 10s
+ timeout: 3s
+ retries: 5
+ security_opt:
+ - no-new-privileges:true
+
+ authelia:
+ image: authelia/authelia:4.39.20
+ restart: unless-stopped
+ depends_on:
+ redis:
+ condition: service_healthy
+ volumes:
+ - ./docker/authelia:/config:ro
+ - ./data/authelia:/var/lib/authelia
+ - ./secrets:/secrets:ro
+ environment:
+ TZ: "${TZ:-Asia/Shanghai}"
+ AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: /secrets/authelia_jwt_secret
+ AUTHELIA_SESSION_SECRET_FILE: /secrets/authelia_session_secret
+ AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: /secrets/authelia_storage_encryption_key
+ expose:
+ - "9091"
+ healthcheck:
+ test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:9091/api/health"]
+ interval: 10s
+ timeout: 3s
+ retries: 12
+ start_period: 10s
+ security_opt:
+ - no-new-privileges:true
+
+ caddy:
+ build:
+ context: .
+ dockerfile: docker/caddy/Dockerfile
+ args:
+ GOPROXY: "${GOPROXY:-https://goproxy.cn,direct}"
+ restart: unless-stopped
+ depends_on:
+ tiddlywiki:
+ condition: service_healthy
+ authelia:
+ condition: service_healthy
+ environment:
+ BASE_DOMAIN: "${BASE_DOMAIN:-tdwiki.home.arpa}"
+ ports:
+ - "8081:80"
+ - "443:443"
+ - "443:443/udp"
+ volumes:
+ - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
+ - ./files:/srv/files
+ - ./data/caddy/data:/data
+ - ./data/caddy/config:/config
+ security_opt:
+ - no-new-privileges:true
+
+volumes:
+ redis-data:
diff --git a/docker/authelia/configuration.template.yml b/docker/authelia/configuration.template.yml
new file mode 100644
index 0000000..bd29e00
--- /dev/null
+++ b/docker/authelia/configuration.template.yml
@@ -0,0 +1,66 @@
+server:
+ address: tcp://0.0.0.0:9091/
+
+log:
+ level: info
+ format: text
+
+theme: auto
+
+identity_validation:
+ reset_password: {}
+
+authentication_backend:
+ password_reset:
+ disable: true
+ file:
+ path: /config/users_database.yml
+ watch: true
+ password:
+ algorithm: argon2
+ argon2:
+ variant: argon2id
+ iterations: 3
+ memory: 65536
+ parallelism: 4
+ key_length: 32
+ salt_length: 16
+
+access_control:
+ default_policy: deny
+ rules:
+ - domain: auth.__BASE_DOMAIN__
+ policy: bypass
+ - domain:
+ - wiki.__BASE_DOMAIN__
+ - dav.__BASE_DOMAIN__
+ policy: one_factor
+ subject:
+ - group:tdwiki
+
+session:
+ name: authelia_session
+ same_site: lax
+ inactivity: 1h
+ expiration: 12h
+ remember_me: 30d
+ cookies:
+ - domain: __BASE_DOMAIN__
+ authelia_url: https://auth.__BASE_DOMAIN__
+ default_redirection_url: https://wiki.__BASE_DOMAIN__
+ redis:
+ host: redis
+ port: 6379
+
+regulation:
+ max_retries: 5
+ find_time: 2m
+ ban_time: 10m
+
+storage:
+ local:
+ path: /var/lib/authelia/db.sqlite3
+
+notifier:
+ filesystem:
+ filename: /var/lib/authelia/notification.txt
diff --git a/docker/authelia/users_database.template.yml b/docker/authelia/users_database.template.yml
new file mode 100644
index 0000000..2ea7159
--- /dev/null
+++ b/docker/authelia/users_database.template.yml
@@ -0,0 +1,8 @@
+users:
+ __USERNAME__:
+ disabled: false
+ displayname: "__DISPLAY_NAME__"
+ password: "__PASSWORD_HASH__"
+ email: "__EMAIL__"
+ groups:
+ - tdwiki
diff --git a/docker/caddy/Caddyfile b/docker/caddy/Caddyfile
new file mode 100644
index 0000000..aee104b
--- /dev/null
+++ b/docker/caddy/Caddyfile
@@ -0,0 +1,44 @@
+{
+ admin off
+}
+
+(authelia) {
+ forward_auth authelia:9091 {
+ uri /api/authz/forward-auth
+ copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
+ }
+}
+
+auth.{$BASE_DOMAIN:tdwiki.home.arpa} {
+ tls internal
+ encode zstd gzip
+ reverse_proxy authelia:9091
+}
+
+wiki.{$BASE_DOMAIN:tdwiki.home.arpa} {
+ tls internal
+ encode zstd gzip
+ import authelia
+
+ header {
+ X-Content-Type-Options nosniff
+ Referrer-Policy same-origin
+ }
+
+ reverse_proxy tiddlywiki:8080
+}
+
+dav.{$BASE_DOMAIN:tdwiki.home.arpa} {
+ tls internal
+ import authelia
+
+ header {
+ X-Content-Type-Options nosniff
+ }
+
+ route {
+ webdav {
+ root /srv/files
+ }
+ }
+}
diff --git a/docker/caddy/Dockerfile b/docker/caddy/Dockerfile
new file mode 100644
index 0000000..9f06d02
--- /dev/null
+++ b/docker/caddy/Dockerfile
@@ -0,0 +1,11 @@
+FROM caddy:2.11.4-builder AS builder
+
+ARG GOPROXY=https://proxy.golang.org,direct
+ENV GOPROXY=${GOPROXY}
+
+RUN xcaddy build \
+ --with github.com/mholt/caddy-webdav@fa2f366b0d75e54c2e381c0aefc3a8df8bf5794b
+
+FROM caddy:2.11.4-alpine
+
+COPY --from=builder /usr/bin/caddy /usr/bin/caddy
diff --git a/docker/tiddlywiki/Dockerfile b/docker/tiddlywiki/Dockerfile
new file mode 100644
index 0000000..cfee4aa
--- /dev/null
+++ b/docker/tiddlywiki/Dockerfile
@@ -0,0 +1,13 @@
+FROM node:22-alpine
+
+ARG TIDDLYWIKI_VERSION=5.4.1
+
+RUN npm install --global "tiddlywiki@${TIDDLYWIKI_VERSION}" \
+ && npm cache clean --force
+
+WORKDIR /wiki
+USER node
+
+EXPOSE 8080
+
+ENTRYPOINT ["tiddlywiki"]
diff --git a/scripts/init-docker.sh b/scripts/init-docker.sh
new file mode 100755
index 0000000..4bae29b
--- /dev/null
+++ b/scripts/init-docker.sh
@@ -0,0 +1,157 @@
+#!/bin/sh
+
+set -eu
+
+SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+PROJECT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
+FORCE_USER=no
+
+if [ "${1:-}" = "--force-user" ]; then
+ FORCE_USER=yes
+elif [ "$#" -gt 0 ]; then
+ printf 'Usage: %s [--force-user]\n' "$0" >&2
+ exit 1
+fi
+
+if [ ! -f "$PROJECT_DIR/.env" ]; then
+ cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env"
+ printf 'Created .env from .env.example\n'
+fi
+
+set -a
+. "$PROJECT_DIR/.env"
+set +a
+
+BASE_DOMAIN=${BASE_DOMAIN:-tdwiki.home.arpa}
+
+case "$BASE_DOMAIN" in
+ *[!a-zA-Z0-9.-]*|.*|*.)
+ printf 'Invalid BASE_DOMAIN: %s\n' "$BASE_DOMAIN" >&2
+ exit 1
+ ;;
+esac
+
+mkdir -p \
+ "$PROJECT_DIR/data/authelia" \
+ "$PROJECT_DIR/data/caddy/data" \
+ "$PROJECT_DIR/data/caddy/config" \
+ "$PROJECT_DIR/secrets"
+
+chmod 700 "$PROJECT_DIR/secrets"
+
+generate_secret() {
+ file=$1
+ bytes=$2
+ if [ ! -s "$file" ]; then
+ umask 077
+ openssl rand -hex "$bytes" > "$file"
+ printf 'Generated %s\n' "${file#"$PROJECT_DIR/"}"
+ fi
+}
+
+generate_secret "$PROJECT_DIR/secrets/authelia_jwt_secret" 32
+generate_secret "$PROJECT_DIR/secrets/authelia_session_secret" 32
+generate_secret "$PROJECT_DIR/secrets/authelia_storage_encryption_key" 32
+
+sed "s/__BASE_DOMAIN__/$BASE_DOMAIN/g" \
+ "$PROJECT_DIR/docker/authelia/configuration.template.yml" \
+ > "$PROJECT_DIR/docker/authelia/configuration.yml"
+
+USERS_FILE="$PROJECT_DIR/docker/authelia/users_database.yml"
+
+if [ ! -f "$USERS_FILE" ] || [ "$FORCE_USER" = yes ]; then
+ USERNAME=${AUTHELIA_USERNAME:-}
+ DISPLAY_NAME=${AUTHELIA_DISPLAY_NAME:-}
+ EMAIL=${AUTHELIA_EMAIL:-}
+ PASSWORD=${AUTHELIA_PASSWORD:-}
+
+ if [ -z "$USERNAME" ]; then
+ printf 'Authelia username [admin]: '
+ read -r USERNAME
+ USERNAME=${USERNAME:-admin}
+ fi
+
+ case "$USERNAME" in
+ *[!a-zA-Z0-9_.-]*)
+ printf 'Username may only contain letters, digits, dot, underscore, and hyphen.\n' >&2
+ exit 1
+ ;;
+ esac
+
+ if [ -z "$DISPLAY_NAME" ]; then
+ printf 'Display name [%s]: ' "$USERNAME"
+ read -r DISPLAY_NAME
+ DISPLAY_NAME=${DISPLAY_NAME:-$USERNAME}
+ fi
+
+ if [ -z "$EMAIL" ]; then
+ printf 'Email [%s@%s]: ' "$USERNAME" "$BASE_DOMAIN"
+ read -r EMAIL
+ EMAIL=${EMAIL:-$USERNAME@$BASE_DOMAIN}
+ fi
+
+ case "$DISPLAY_NAME$EMAIL" in
+ *'"'*|*'\'*)
+ printf 'Display name and email cannot contain quote or backslash characters.\n' >&2
+ exit 1
+ ;;
+ esac
+
+ if [ -z "$PASSWORD" ]; then
+ if [ ! -t 0 ]; then
+ printf 'Set AUTHELIA_PASSWORD for non-interactive initialization.\n' >&2
+ exit 1
+ fi
+ printf 'Password: '
+ stty -echo
+ read -r PASSWORD
+ stty echo
+ printf '\nConfirm password: '
+ stty -echo
+ read -r PASSWORD_CONFIRM
+ stty echo
+ printf '\n'
+ if [ "$PASSWORD" != "$PASSWORD_CONFIRM" ]; then
+ printf 'Passwords do not match.\n' >&2
+ exit 1
+ fi
+ fi
+
+ if [ "${#PASSWORD}" -lt 12 ]; then
+ printf 'Password must contain at least 12 characters.\n' >&2
+ exit 1
+ fi
+
+ HASH_OUTPUT=$(docker run --rm authelia/authelia:4.39.20 \
+ authelia crypto hash generate argon2 \
+ --password "$PASSWORD" \
+ --no-confirm)
+ PASSWORD_HASH=${HASH_OUTPUT#Digest: }
+ unset PASSWORD PASSWORD_CONFIRM HASH_OUTPUT
+
+ escape_sed() {
+ printf '%s' "$1" | sed 's/[|&]/\\&/g'
+ }
+
+ USERNAME_ESCAPED=$(escape_sed "$USERNAME")
+ DISPLAY_NAME_ESCAPED=$(escape_sed "$DISPLAY_NAME")
+ EMAIL_ESCAPED=$(escape_sed "$EMAIL")
+ PASSWORD_HASH_ESCAPED=$(escape_sed "$PASSWORD_HASH")
+
+ sed \
+ -e "s|__USERNAME__|$USERNAME_ESCAPED|g" \
+ -e "s|__DISPLAY_NAME__|$DISPLAY_NAME_ESCAPED|g" \
+ -e "s|__EMAIL__|$EMAIL_ESCAPED|g" \
+ -e "s|__PASSWORD_HASH__|$PASSWORD_HASH_ESCAPED|g" \
+ "$PROJECT_DIR/docker/authelia/users_database.template.yml" \
+ > "$USERS_FILE"
+ chmod 600 "$USERS_FILE"
+ printf 'Created Authelia user %s\n' "$USERNAME"
+fi
+
+docker compose --project-directory "$PROJECT_DIR" config --quiet
+
+printf '\nInitialization complete.\n'
+printf 'Add wiki.%s, dav.%s, and auth.%s to local DNS, then run:\n' \
+ "$BASE_DOMAIN" "$BASE_DOMAIN" "$BASE_DOMAIN"
+printf ' docker compose up -d --build\n'