.env 或密钥管理系统。0. 部署边界#
这套 Outline 自用部署由几个组件组成:
| 组件 | 角色 |
|---|---|
| Outline | 知识库主服务 |
| PostgreSQL | Outline 和 Keycloak 数据库 |
| Redis | Outline 后台队列和缓存 |
| MinIO | Outline 附件对象存储 |
| Keycloak | OIDC 登录入口 |
| OpenLDAP | 内部用户目录,可选 |
| OpenResty | HTTPS 入口和反向代理 |
| ALLinSSL | 证书申请和续期管理 |
整体链路如下:
flowchart LR Browser --> OpenResty OpenResty --> Outline OpenResty --> Keycloak OpenResty --> MinIO Outline --> PostgreSQL Outline --> Redis Outline --> MinIO Outline --> Keycloak Keycloak --> OpenLDAP
1. 证书先收口#
证书由 ALLinSSL 统一管理,OpenResty 只引用证书文件。这样做的好处是证书续期和反向代理配置分开,后续迁移服务时也不用重新梳理每个站点的 ACME 逻辑。

建议先准备这些域名:
| 域名 | 用途 |
|---|---|
outline.example.com | Outline 主入口 |
auth.example.com | Keycloak OIDC 入口 |
minio-outline.example.com | MinIO S3 API |
console-minio-outline.example.com | MinIO Console |
上线前验证:
openssl x509 -in /etc/ssl/example/fullchain.pem -noout -dates -subject
nginx -t
systemctl reload openresty
2. PostgreSQL 扩展#
Outline 依赖 PostgreSQL 扩展,常见包括 uuid-ossp、unaccent、pg_trgm。如果 PostgreSQL 来自一键安装脚本或精简编译包,扩展可能需要手工安装。
数据库内先检查:
SELECT name, default_version, installed_version
FROM pg_available_extensions
WHERE name IN ('uuid-ossp', 'unaccent', 'pg_trgm');
创建扩展:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
如果扩展文件不存在,需要回到 PostgreSQL 源码 contrib 目录编译安装:
sudo apt-get install -y libossp-uuid-dev
cd /data/tools/oneinstack/src/postgresql-12.7/contrib/uuid-ossp
make
make install
service postgresql restart
不要跳过这个检查。扩展缺失时,Outline 可能在迁移或启动阶段直接失败。
3. 数据库和账号#
Keycloak 和 Outline 使用独立数据库与账号:
CREATE DATABASE keycloak_db;
CREATE USER keycloak_user WITH PASSWORD :'keycloak_db_password';
GRANT ALL PRIVILEGES ON DATABASE keycloak_db TO keycloak_user;
CREATE DATABASE outlinedb;
CREATE USER outlinedbuser WITH PASSWORD :'outline_db_password';
GRANT ALL PRIVILEGES ON DATABASE outlinedb TO outlinedbuser;
如果因为扩展创建权限需要临时提权,变更后要复查权限,不要长期让应用账号保留不必要的超级用户权限。
\c outlinedb
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
4. Keycloak 和 LDAP#
Keycloak 可以参考单独的 Compose 项目部署。Outline 只关心 OIDC Client 这一层:
| 配置项 | 示例 |
|---|---|
| Client ID | outline |
| Redirect URI | https://outline.example.com/auth/oidc.callback |
| Auth URI | https://auth.example.com/realms/master/protocol/openid-connect/auth |
| Token URI | https://auth.example.com/realms/master/protocol/openid-connect/token |
| UserInfo URI | https://auth.example.com/realms/master/protocol/openid-connect/userinfo |
| Scope | openid profile email |
| Username Claim | email |
OpenLDAP 可作为 Keycloak 的用户源。示例 Compose:
services:
openldap:
image: osixia/openldap:latest
restart: always
environment:
LDAP_ORGANISATION: "example"
LDAP_DOMAIN: "example.com"
LDAP_ADMIN_PASSWORD: "${LDAP_ADMIN_PASSWORD:?set LDAP_ADMIN_PASSWORD}"
LDAP_CONFIG_PASSWORD: "${LDAP_CONFIG_PASSWORD:?set LDAP_CONFIG_PASSWORD}"
volumes:
- ./data:/var/lib/ldap
- ./config:/etc/ldap/slapd.d
ports:
- "389:389"
phpldapadmin:
image: osixia/phpldapadmin:latest
restart: always
links:
- openldap
environment:
PHPLDAPADMIN_HTTPS: "false"
PHPLDAPADMIN_LDAP_HOSTS: "openldap"
ports:
- "30000:80"
真实 LDAP 管理员密码不要写进 Compose 文件,使用 .env 注入。
5. MinIO 和 Outline Compose#
.env 示例:
OUTLINE_IMAGE_TAG=outlinewiki/outline:0.85.1
OUTLINE_MINIO_IMAGE_TAG=minio/minio:RELEASE.2023-08-04T17-40-21Z
OUTLINE_DB_NAME=outlinedb
OUTLINE_DB_USER=outlinedbuser
OUTLINE_DB_PASSWORD=CHANGE_ME_OUTLINE_DB_PASSWORD
OUTLINE_DB_PASSWORD_URLENCODED=CHANGE_ME_URL_ENCODED_OUTLINE_DB_PASSWORD
OUTLINE_DB_SSLMODE=require
OUTLINE_URL=https://outline.example.com
OUTLINE_HOSTNAME=outline.example.com
OUTLINE_MINIO_ADMIN=minioadmin
OUTLINE_MINIO_ADMIN_PASSWORD=CHANGE_ME_MINIO_ROOT_PASSWORD
OUTLINE_MINIO_USER=bucket.data
OUTLINE_MINIO_USER_PASSWORD=CHANGE_ME_MINIO_USER_PASSWORD
OUTLINE_MINIO_BUCKET_NAME=data
OUTLINE_MINIO_HOSTNAME=minio-outline.example.com
OUTLINE_MINIO_CONSOLE_URL=https://console-minio-outline.example.com
OUTLINE_SECRET_KEY=CHANGE_ME_OPENSSL_RAND_HEX_32
OUTLINE_UTILS_SECRET=CHANGE_ME_OPENSSL_RAND_HEX_32
OUTLINE_AWS_S3_UPLOAD_BUCKET_URL=https://minio-outline.example.com/data
OUTLINE_OIDC_CLIENT_SECRET=CHANGE_ME_OIDC_CLIENT_SECRET
OUTLINE_OIDC_AUTH_URI=https://auth.example.com/realms/master/protocol/openid-connect/auth
OUTLINE_OIDC_TOKEN_URI=https://auth.example.com/realms/master/protocol/openid-connect/token
OUTLINE_OIDC_USERINFO_URI=https://auth.example.com/realms/master/protocol/openid-connect/userinfo
OUTLINE_OIDC_DISPLAY_NAME=Keycloak
OUTLINE_OIDC_USERNAME_CLAIM=email
OUTLINE_OIDC_SCOPES=openid profile email
OUTLINE_REDIS_IMAGE_TAG=redis:7.0
FILE_STORAGE_UPLOAD_MAX_SIZE=26214400
DATABASE_URL 会把用户名、密码、主机和库名拼成 URI。数据库密码如果包含 @、#、:、/、?、& 等字符,必须先做 URL 编码后写入 OUTLINE_DB_PASSWORD_URLENCODED。原始密码可以保留在 OUTLINE_DB_PASSWORD 里供人工维护,连接串只使用编码后的值。
生产环境必须优先让 PostgreSQL 开启 TLS,并保持 OUTLINE_DB_SSLMODE=require。不要在跨主机、公网、共享网络或不受控内网里使用 disable,否则数据库账号、会话和业务数据可能以明文在链路上传输。只有数据库和 Outline 在同一台主机、同一 Docker 网络,或链路已由专用内网/VPN 隔离且经过风险确认时,才允许临时改成 disable;变更记录里要写清楚隔离依据和恢复到 TLS 的时间点。
可以用下面的命令生成 URL 编码值:
python3 - <<'PY'
from urllib.parse import quote
import os
print(quote(os.environ["OUTLINE_DB_PASSWORD"], safe=""))
PY
生成两个 Outline secret:
openssl rand -hex 32
openssl rand -hex 32
Compose 主体。MinIO 初始化这段不要用 || true 吞掉关键失败;只对“已存在”这种幂等情况做显式判断。
services:
minio:
image: ${OUTLINE_MINIO_IMAGE_TAG}
command: server /data --console-address :9001
volumes:
- ./data/minio:/data
environment:
MINIO_ROOT_USER: ${OUTLINE_MINIO_ADMIN}
MINIO_ROOT_PASSWORD: ${OUTLINE_MINIO_ADMIN_PASSWORD}
MINIO_BROWSER_REDIRECT_URL: ${OUTLINE_MINIO_CONSOLE_URL}
ports:
- "9000:9000"
- "9001:9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
restart: always
minio-init:
image: ${OUTLINE_MINIO_IMAGE_TAG}
depends_on:
minio:
condition: service_healthy
entrypoint: /bin/sh
command:
- -c
- |
set -eu
export MC_HOST_minio="http://${OUTLINE_MINIO_ADMIN}:${OUTLINE_MINIO_ADMIN_PASSWORD}@minio:9000"
mc ls minio/${OUTLINE_MINIO_BUCKET_NAME} >/dev/null 2>&1 || mc mb minio/${OUTLINE_MINIO_BUCKET_NAME}
mc admin user info minio ${OUTLINE_MINIO_USER} >/dev/null 2>&1 || mc admin user add minio ${OUTLINE_MINIO_USER} ${OUTLINE_MINIO_USER_PASSWORD}
cat > outline-policy.json <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": ["arn:aws:s3:::${OUTLINE_MINIO_BUCKET_NAME}"]
},
{
"Effect": "Allow",
"Action": [
"s3:AbortMultipartUpload",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListMultipartUploadParts",
"s3:PutObject"
],
"Resource": ["arn:aws:s3:::${OUTLINE_MINIO_BUCKET_NAME}/*"]
}
]
}
POLICY
mc admin policy info minio OutlineAppFullAccess >/dev/null 2>&1 || mc admin policy create minio OutlineAppFullAccess outline-policy.json
if ! mc admin policy entities minio --user ${OUTLINE_MINIO_USER} | grep -q 'OutlineAppFullAccess'; then
mc admin policy attach minio OutlineAppFullAccess --user=${OUTLINE_MINIO_USER}
fi
mc admin user info minio ${OUTLINE_MINIO_USER}
environment:
OUTLINE_MINIO_ADMIN: ${OUTLINE_MINIO_ADMIN}
OUTLINE_MINIO_ADMIN_PASSWORD: ${OUTLINE_MINIO_ADMIN_PASSWORD}
OUTLINE_MINIO_USER: ${OUTLINE_MINIO_USER}
OUTLINE_MINIO_USER_PASSWORD: ${OUTLINE_MINIO_USER_PASSWORD}
OUTLINE_MINIO_BUCKET_NAME: ${OUTLINE_MINIO_BUCKET_NAME}
restart: "no"
redis:
image: ${OUTLINE_REDIS_IMAGE_TAG}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
restart: always
outline:
image: ${OUTLINE_IMAGE_TAG}
environment:
DATABASE_URL: postgres://${OUTLINE_DB_USER}:${OUTLINE_DB_PASSWORD_URLENCODED}@postgres.example.internal:5432/${OUTLINE_DB_NAME}?sslmode=${OUTLINE_DB_SSLMODE}&connect_timeout=10
PGSSLMODE: ${OUTLINE_DB_SSLMODE}
REDIS_URL: redis://redis:6379
URL: ${OUTLINE_URL}
PORT: 3000
SECRET_KEY: ${OUTLINE_SECRET_KEY}
UTILS_SECRET: ${OUTLINE_UTILS_SECRET}
FORCE_HTTPS: "false"
ENABLE_UPDATES: "true"
AWS_REGION: eu-east-1
AWS_ACCESS_KEY_ID: ${OUTLINE_MINIO_USER}
AWS_SECRET_ACCESS_KEY: ${OUTLINE_MINIO_USER_PASSWORD}
AWS_S3_UPLOAD_BUCKET_URL: ${OUTLINE_AWS_S3_UPLOAD_BUCKET_URL}
AWS_S3_ACCELERATE_URL: ${OUTLINE_AWS_S3_UPLOAD_BUCKET_URL}
AWS_S3_UPLOAD_BUCKET_NAME: ${OUTLINE_MINIO_BUCKET_NAME}
AWS_S3_UPLOAD_MAX_SIZE: ${FILE_STORAGE_UPLOAD_MAX_SIZE}
FILE_STORAGE_UPLOAD_MAX_SIZE: ${FILE_STORAGE_UPLOAD_MAX_SIZE}
AWS_S3_FORCE_PATH_STYLE: "false"
AWS_S3_ACL: private
OIDC_CLIENT_ID: outline
OIDC_CLIENT_SECRET: ${OUTLINE_OIDC_CLIENT_SECRET}
OIDC_AUTH_URI: ${OUTLINE_OIDC_AUTH_URI}
OIDC_TOKEN_URI: ${OUTLINE_OIDC_TOKEN_URI}
OIDC_USERINFO_URI: ${OUTLINE_OIDC_USERINFO_URI}
OIDC_DISPLAY_NAME: ${OUTLINE_OIDC_DISPLAY_NAME}
OIDC_USERNAME_CLAIM: ${OUTLINE_OIDC_USERNAME_CLAIM}
OIDC_SCOPES: ${OUTLINE_OIDC_SCOPES}
ports:
- "3000:3000"
healthcheck:
test: timeout 10s bash -c ':> /dev/tcp/127.0.0.1/3000' || exit 1
interval: 10s
timeout: 5s
retries: 3
start_period: 90s
restart: always
depends_on:
minio:
condition: service_healthy
redis:
condition: service_healthy
minio-init:
condition: service_completed_successfully
启动前检查:
docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail=100 outline
Review Gate:
| 检查项 | 命令 | 继续条件 | 停止条件 |
|---|---|---|---|
.env 权限 | stat -c '%a %n' .env | 600 或更严格 | 密钥文件可被普通用户读取 |
| Compose 渲染 | docker compose config | 变量全部展开,服务完整 | 变量缺失或 URL 拼错 |
| MinIO bucket | docker compose exec minio mc ls minio/${OUTLINE_MINIO_BUCKET_NAME} | bucket 存在 | bucket 不存在 |
| MinIO 用户 | docker compose exec minio mc admin user info minio ${OUTLINE_MINIO_USER} | 用户存在且启用 | 用户缺失或禁用 |
| Outline 日志 | docker compose logs --tail=100 outline | 没有数据库/OIDC/S3 初始化错误 | 关键依赖报错 |
6. OpenResty 反向代理#
Outline 入口重点是 WebSocket 和转发头:
server {
listen 443 ssl;
server_name outline.example.com;
ssl_certificate /etc/ssl/example/fullchain.pem;
ssl_certificate_key /etc/ssl/example/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000/;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
}
MinIO API 和 Console 分开代理。Console 要保留 WebSocket 头:
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:9001/;
}
Keycloak 入口要让 X-Forwarded-Proto 和外部 HTTPS 地址一致,否则 OIDC 回调容易跳错地址。
7. 验证矩阵#
| 检查项 | 命令 / 操作 | 通过标准 |
|---|---|---|
| PostgreSQL 扩展 | \dx | uuid-ossp、unaccent、pg_trgm 存在 |
| Redis | docker compose exec redis redis-cli ping | 返回 PONG |
| MinIO | curl -f http://127.0.0.1:9000/minio/health/live | 返回 200 |
| MinIO 权限 | 用 Outline 上传附件,或用同一 AK/SK 上传测试对象 | 对象写入成功 |
| Outline | curl -I http://127.0.0.1:3000 | 有 HTTP 响应 |
| OIDC | 浏览器点击 Keycloak 登录 | 能跳转回 Outline |
| 附件上传 | 上传一张图片 | MinIO bucket 里能看到对象 |
| HTTPS | curl -I https://outline.example.com | 证书有效,状态正常 |
8. 回滚#
备份关键数据:
pg_dump -Fc outlinedb > outlinedb-before-change.dump
tar czf outline-compose-backup-$(date +%F-%H%M%S).tar.gz .env compose.yaml data/minio
停止应用:
docker compose down
如果只是代理配置错误,优先回滚 OpenResty 配置并 reload,不要动数据库和对象存储。
nginx -t
systemctl reload openresty
如果 MinIO 初始化失败,先停 Outline,保留 data/minio 现场,再修复 bucket、用户和 policy。不要直接删除 MinIO 数据目录,除非已经确认没有用户附件。
9. 常见问题#
Outline 启动失败,日志里提示数据库扩展缺失
回到 PostgreSQL 里创建 uuid-ossp、unaccent、pg_trgm。如果 pg_available_extensions 里都没有,先补安装扩展文件。
登录 Keycloak 后回调失败
检查 Outline 的 URL、OIDC Redirect URI、OpenResty 转发头和 Keycloak hostname。四者必须按同一个外部 HTTPS 地址收敛。
附件上传失败
先查 MinIO bucket、访问密钥、AWS_S3_UPLOAD_BUCKET_URL、反向代理最大上传大小和浏览器控制台。Outline 到 MinIO 的服务端连接和浏览器到 MinIO 的外部 URL 都要通。
MinIO Console 打不开或卡住
多数是 WebSocket 头没传。Console 入口需要 Upgrade 和 Connection "upgrade"。
