diff options
| author | shu <nvcsadjkojf@outlook.com> | 2026-07-27 00:22:33 +0800 |
|---|---|---|
| committer | shu <nvcsadjkojf@outlook.com> | 2026-07-27 00:22:33 +0800 |
| commit | aaa0cf501ce44a7c7c54c7c5c6485cfb7f60e583 (patch) | |
| tree | da94ef66e9830d09961e577f87d36e63a6076ce5 | |
| parent | 338cc101f127e4841689c1f81227ee033e57ad28 (diff) | |
Add Docker Compose deployment
| -rw-r--r-- | .env.example | 5 | ||||
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | DOCKER.md | 162 | ||||
| -rw-r--r-- | compose.yaml | 100 | ||||
| -rw-r--r-- | docker/authelia/configuration.template.yml | 66 | ||||
| -rw-r--r-- | docker/authelia/users_database.template.yml | 8 | ||||
| -rw-r--r-- | docker/caddy/Caddyfile | 44 | ||||
| -rw-r--r-- | docker/caddy/Dockerfile | 11 | ||||
| -rw-r--r-- | docker/tiddlywiki/Dockerfile | 13 | ||||
| -rwxr-xr-x | scripts/init-docker.sh | 157 |
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' |
