【HedgeDoc】内网服务器搭建指南

本文档为技术人员提供在内网环境中部署 HedgeDoc 服务器的完整方案,支持 百人跨项目协作


📋 部署环境要求

1. 硬件要求

组件 最低配置 推荐配置 (百人) 说明
CPU 2核 4核 (Intel/AMD) 需支持虚拟化
内存 4GB 8-16GB DDR4 建议ECC内存
存储 100GB HDD 200GB SSD 数据盘建议RAID1
网络 千兆网卡 千兆网卡 内网专用

2. 软件环境

软件 版本要求 安装方式 备注
操作系统 CentOS 7.9+ / Ubuntu 20.04 LTS ISO镜像安装 最小化安装
Docker 20.10.0+ 官方仓库 配置国内镜像源
Docker Compose 2.0.0+ GitHub Releases 独立二进制文件
Git 2.25.0+ yum/apt安装 用于版本管理

🚀 部署步骤

阶段一:服务器初始化

1.1 系统配置
bash 复制代码
# 1. 更新系统
sudo yum update -y  # CentOS
# 或
sudo apt update && sudo apt upgrade -y  # Ubuntu

# 2. 设置主机名
sudo hostnamectl set-hostname hedgedoc-server
echo "192.168.10.100 hedgedoc-server" | sudo tee -a /etc/hosts

# 3. 关闭防火墙(内网环境可关闭)
sudo systemctl stop firewalld
sudo systemctl disable firewalld
# 或配置防火墙规则
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --reload

# 4. 禁用SELinux
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config

# 5. 优化系统参数
cat >> /etc/sysctl.conf << EOF
# HedgeDoc性能优化
vm.swappiness = 10
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65000
fs.file-max = 1000000
EOF
sudo sysctl -p
1.2 Docker环境部署
bash 复制代码
# 1. 安装Docker(CentOS示例)
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce docker-ce-cli containerd.io

# 2. 配置Docker镜像加速(内网可配置私有仓库)
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json << EOF
{
  "registry-mirrors": [
    "https://docker.mirrors.ustc.edu.cn",
    "https://hub-mirror.c.163.com"
  ],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  },
  "data-root": "/data/docker",
  "exec-opts": ["native.cgroupdriver=systemd"]
}
EOF

# 3. 启动Docker
sudo systemctl start docker
sudo systemctl enable docker

# 4. 安装Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" \
  -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

阶段二:HedgeDoc服务部署

2.1 创建部署目录结构
bash 复制代码
# 创建主目录
mkdir -p /opt/hedgedoc
cd /opt/hedgedoc

# 创建目录结构
mkdir -p {data/{postgres,redis},uploads,logs/{app,nginx},backup,config,ssl}

# 设置权限
chmod 755 /opt/hedgedoc/uploads
chown -R 1000:1000 /opt/hedgedoc/uploads  # HedgeDoc容器用户
2.2 生成SSL证书(内网自签名)
bash 复制代码
# 生成私钥和证书
cd /opt/hedgedoc/ssl

# 生成CA私钥
openssl genrsa -out ca.key 4096

# 生成CA证书
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=Company/CN=Internal CA"

# 生成服务器私钥
openssl genrsa -out hedgedoc.key 2048

# 生成证书签名请求
openssl req -new -key hedgedoc.key -out hedgedoc.csr \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=Company/CN=docs.internal.com" \
  -reqexts SAN \
  -config <(cat /etc/ssl/openssl.cnf \
    <(printf "\n[SAN]\nsubjectAltName=DNS:docs.internal.com,DNS:*.internal.com"))

# 使用CA签名
openssl x509 -req -days 3650 -in hedgedoc.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out hedgedoc.crt -extensions SAN \
  -extfile <(cat /etc/ssl/openssl.cnf \
    <(printf "\n[SAN]\nsubjectAltName=DNS:docs.internal.com,DNS:*.internal.com"))

# 验证证书
openssl verify -CAfile ca.crt hedgedoc.crt
2.3 创建docker-compose.yml
yaml 复制代码
# /opt/hedgedoc/docker-compose.yml
version: '3.8'

services:
  # PostgreSQL数据库
  database:
    image: postgres:15-alpine
    container_name: hedgedoc-postgres
    restart: always
    environment:
      POSTGRES_USER: hedgedoc
      POSTGRES_PASSWORD: ${DB_PASSWORD:-ChangeMe123!}
      POSTGRES_DB: hedgedoc
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
      - ./config/postgresql.conf:/etc/postgresql/postgresql.conf:ro
    command: >
      postgres
      -c config_file=/etc/postgresql/postgresql.conf
      -c shared_buffers=2GB
      -c effective_cache_size=6GB
    networks:
      - hedgedoc-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U hedgedoc"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Redis缓存
  redis:
    image: redis:7-alpine
    container_name: hedgedoc-redis
    restart: always
    command: >
      redis-server
      --appendonly yes
      --maxmemory 2gb
      --maxmemory-policy allkeys-lru
      --save 900 1
      --save 300 10
      --save 60 10000
    volumes:
      - ./data/redis:/data
      - ./config/redis.conf:/usr/local/etc/redis/redis.conf:ro
    networks:
      - hedgedoc-net
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  # HedgeDoc主应用
  app:
    image: quay.io/hedgedoc/hedgedoc:2.2.0
    container_name: hedgedoc-app
    restart: always
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      # 基础配置
      CMD_DOMAIN: docs.internal.com
      CMD_PORT: 3000
      CMD_PROTOCOL_USESSL: "true"
      CMD_URL_ADDPORT: "false"
      
      # 数据库配置
      CMD_DB_URL: postgres://hedgedoc:${DB_PASSWORD:-ChangeMe123!}@database:5432/hedgedoc
      CMD_DB_POOL_MIN: 5
      CMD_DB_POOL_MAX: 50
      
      # Redis配置
      CMD_REDIS_URL: redis://redis:6379
      CMD_CACHE_TTL: 7200
      
      # 会话和Cookie
      CMD_SESSION_SECRET: ${SESSION_SECRET:-ChangeMe456!}
      CMD_SESSION_LIFETIME: 604800000
      CMD_COOKIE_POLICY: "lax"
      CMD_COOKIE_DOMAIN: ".internal.com"
      
      # 安全配置
      CMD_ALLOW_ANONYMOUS: "false"
      CMD_ALLOW_EMAIL_REGISTER: "false"
      CMD_ALLOW_FREEURL: "true"
      CMD_DEFAULT_PERMISSION: "limited"
      CMD_REQUIRE_VERIFIED_EMAIL: "false"
      
      # 钉钉集成
      CMD_OAUTH2_PROVIDERS: "dingtalk"
      CMD_OAUTH2_DINGTALK_CLIENT_ID: ${DINGTALK_CLIENT_ID}
      CMD_OAUTH2_DINGTALK_CLIENT_SECRET: ${DINGTALK_CLIENT_SECRET}
      CMD_OAUTH2_DINGTALK_BASE_URL: https://oapi.dingtalk.com
      
      # 文件上传
      CMD_UPLOAD_TYPE: filesystem
      CMD_UPLOAD_PATH: /hedgedoc/uploads
      CMD_UPLOAD_MAX_FILESIZE: 52428800
      
      # 性能配置
      CMD_RATE_LIMIT_API_ENABLE: "true"
      CMD_RATE_LIMIT_API_MAX: 200
      CMD_RATE_LIMIT_API_WINDOWMS: 600000
      
      # 日志配置
      CMD_LOG_LEVEL: "info"
      CMD_LOG_TRANSPORTS: "console,file"
      CMD_LOG_FILE: "/hedgedoc/logs/app.log"
      
    volumes:
      - ./uploads:/hedgedoc/uploads
      - ./logs/app:/hedgedoc/logs
      - ./ssl/hedgedoc.crt:/hedgedoc/ssl.crt:ro
      - ./ssl/hedgedoc.key:/hedgedoc/ssl.key:ro
    networks:
      - hedgedoc-net
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "https://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Nginx反向代理
  nginx:
    image: nginx:1.23-alpine
    container_name: hedgedoc-nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./config/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl/hedgedoc.crt:/etc/nginx/ssl/hedgedoc.crt:ro
      - ./ssl/hedgedoc.key:/etc/nginx/ssl/hedgedoc.key:ro
      - ./logs/nginx:/var/log/nginx
    depends_on:
      - app
    networks:
      - hedgedoc-net
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 60s
      timeout: 10s
      retries: 3

networks:
  hedgedoc-net:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.22.0.0/24
2.4 创建Nginx配置
nginx 复制代码
# /opt/hedgedoc/config/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 4096;
    multi_accept on;
    use epoll;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'rt=$request_time uct="$upstream_connect_time"';
    
    access_log /var/log/nginx/access.log main;
    
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 1000;
    client_max_body_size 100M;
    
    # 上游服务器
    upstream hedgedoc_backend {
        server app:3000;
        keepalive 32;
    }
    
    # 重定向HTTP到HTTPS
    server {
        listen 80;
        server_name docs.internal.com;
        return 301 https://$server_name$request_uri;
    }
    
    # HTTPS服务器
    server {
        listen 443 ssl http2;
        server_name docs.internal.com;
        
        ssl_certificate /etc/nginx/ssl/hedgedoc.crt;
        ssl_certificate_key /etc/nginx/ssl/hedgedoc.key;
        
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:50m;
        ssl_session_timeout 1d;
        ssl_session_tickets off;
        
        # 安全头
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
        
        # 性能优化
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $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_buffering off;
        proxy_cache off;
        proxy_read_timeout 600s;
        proxy_connect_timeout 75s;
        
        # 健康检查
        location /health {
            proxy_pass https://hedgedoc_backend/health;
            access_log off;
            allow 192.168.10.0/24;
            deny all;
        }
        
        # 静态资源缓存
        location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
            proxy_pass https://hedgedoc_backend;
            expires 30d;
            add_header Cache-Control "public, immutable";
            access_log off;
        }
        
        # 主应用
        location / {
            proxy_pass https://hedgedoc_backend;
            
            # 限制请求频率
            limit_req zone=api burst=30 nodelay;
            limit_req_status 429;
        }
    }
    
    # 请求限制
    limit_req_zone $binary_remote_addr zone=api:20m rate=20r/s;
}
2.5 创建环境变量文件
bash 复制代码
# /opt/hedgedoc/.env
# 生成随机密码
DB_PASSWORD=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
SESSION_SECRET=$(openssl rand -base64 64)

cat > /opt/hedgedoc/.env << EOF
# 数据库密码
DB_PASSWORD=${DB_PASSWORD}

# 会话密钥
SESSION_SECRET=${SESSION_SECRET}

# 钉钉应用信息(需提前在钉钉后台创建)
DINGTALK_CLIENT_ID=dingxxxxxxxxxxxxxxx
DINGTALK_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# 内网域名
CMD_DOMAIN=docs.internal.com

# 内网IP段
CMD_TRUST_PROXY=192.168.10.0/24

# 可选:内网邮箱配置
# CMD_EMAIL=true
# CMD_EMAIL_HOST=smtp.internal.com
# CMD_EMAIL_PORT=25
# CMD_EMAIL_FROM=noreply@internal.com
EOF

# 保护环境变量文件
chmod 600 /opt/hedgedoc/.env

阶段三:启动与初始化

3.1 启动服务
bash 复制代码
cd /opt/hedgedoc

# 拉取镜像
docker-compose pull

# 启动服务
docker-compose up -d

# 查看启动状态
docker-compose ps
docker-compose logs -f --tail=100 app

# 检查服务健康
curl -k https://docs.internal.com/health
3.2 数据库初始化
bash 复制代码
# 等待数据库启动
sleep 30

# 创建数据库索引(优化查询性能)
docker-compose exec database psql -U hedgedoc hedgedoc << EOF
-- 创建性能优化索引
CREATE INDEX IF NOT EXISTS idx_notes_ownerid ON notes("ownerId");
CREATE INDEX IF NOT EXISTS idx_notes_alias ON notes(alias);
CREATE INDEX IF NOT EXISTS idx_notes_updatedat ON notes("updatedAt");
CREATE INDEX IF NOT EXISTS idx_revisions_noteid ON revisions("noteId");
CREATE INDEX IF NOT EXISTS idx_revisions_createdat ON revisions("createdAt");
CREATE INDEX IF NOT EXISTS idx_tags_noteid ON tags("noteId");
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

-- 查看表空间使用
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) 
FROM pg_tables 
WHERE schemaname NOT IN ('pg_catalog', 'information_schema') 
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
EOF

🔧 运维管理

1. 日常维护脚本

bash 复制代码
#!/bin/bash
# /opt/hedgedoc/scripts/maintenance.sh

set -e

echo "=== HedgeDoc 日常维护 $(date) ==="

# 1. 检查容器状态
echo "检查容器状态..."
docker-compose ps

# 2. 查看日志大小
echo "日志文件大小..."
du -sh /opt/hedgedoc/logs/*/

# 3. 数据库维护
echo "数据库维护..."
docker-compose exec database psql -U hedgedoc hedgedoc << EOF
VACUUM ANALYZE;
EOF

# 4. 清理Docker资源
echo "清理Docker资源..."
docker system prune -f --filter "until=24h"

# 5. 备份检查
echo "检查备份..."
ls -lh /opt/hedgedoc/backup/ | tail -5

echo "维护完成"

2. 备份脚本

bash 复制代码
#!/bin/bash
# /opt/hedgedoc/scripts/backup.sh

BACKUP_DIR="/opt/hedgedoc/backup/$(date +%Y%m%d_%H%M%S)"
mkdir -p $BACKUP_DIR

echo "开始备份: $(date)" | tee $BACKUP_DIR/backup.log

# 1. 备份数据库
echo "备份数据库..." | tee -a $BACKUP_DIR/backup.log
docker-compose exec -T database pg_dump -U hedgedoc --clean --create hedgedoc > $BACKUP_DIR/hedgedoc.sql

# 2. 备份上传文件
echo "备份上传文件..." | tee -a $BACKUP_DIR/backup.log
rsync -av --delete /opt/hedgedoc/uploads/ $BACKUP_DIR/uploads/ >> $BACKUP_DIR/backup.log 2>&1

# 3. 备份配置文件
echo "备份配置文件..." | tee -a $BACKUP_DIR/backup.log
cp /opt/hedgedoc/docker-compose.yml $BACKUP_DIR/
cp /opt/hedgedoc/.env $BACKUP_DIR/
cp -r /opt/hedgedoc/config $BACKUP_DIR/
cp -r /opt/hedgedoc/ssl $BACKUP_DIR/

# 4. 备份Redis数据
echo "备份Redis数据..." | tee -a $BACKUP_DIR/backup.log
docker-compose exec redis redis-cli SAVE
docker cp hedgedoc-redis:/data/dump.rdb $BACKUP_DIR/redis_dump.rdb

# 5. 压缩备份
echo "压缩备份..." | tee -a $BACKUP_DIR/backup.log
cd /opt/hedgedoc/backup
tar -czf hedgedoc_backup_$(date +%Y%m%d_%H%M%S).tar.gz -C $BACKUP_DIR .

# 6. 清理旧备份(保留30天)
find /opt/hedgedoc/backup -name "hedgedoc_backup_*.tar.gz" -mtime +30 -delete

# 7. 记录备份信息
echo "备份统计:" | tee -a $BACKUP_DIR/backup.log
du -sh $BACKUP_DIR | tee -a $BACKUP_DIR/backup.log
ls -lh /opt/hedgedoc/backup/hedgedoc_backup_*.tar.gz | tail -1 | tee -a $BACKUP_DIR/backup.log

echo "备份完成: $(date)" | tee -a $BACKUP_DIR/backup.log

3. 监控脚本

bash 复制代码
#!/bin/bash
# /opt/hedgedoc/scripts/monitor.sh

# 监控指标输出到Prometheus格式(可选)
METRICS_FILE="/tmp/hedgedoc_metrics.prom"

# 容器状态
CONTAINER_COUNT=$(docker-compose ps -q | wc -l)
echo "hedgedoc_containers_total $CONTAINER_COUNT" > $METRICS_FILE

# 服务健康
HEALTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://docs.internal.com/health || echo "0")
echo "hedgedoc_health_status $HEALTH_STATUS" >> $METRICS_FILE

# 内存使用
MEM_USAGE=$(docker stats --no-stream --format "{{.MemUsage}}" hedgedoc-app | cut -d '/' -f 1 | tr -d 'MiB' | tr -d ' ')
echo "hedgedoc_memory_usage_bytes $MEM_USAGE" >> $METRICS_FILE

# 数据库连接
DB_CONNECTIONS=$(docker-compose exec database psql -U hedgedoc -t -c "SELECT count(*) FROM pg_stat_activity WHERE datname='hedgedoc';" 2>/dev/null || echo "0")
echo "hedgedoc_database_connections $DB_CONNECTIONS" >> $METRICS_FILE

# 用户活跃度
ACTIVE_USERS=$(docker-compose exec database psql -U hedgedoc -t -c "SELECT COUNT(DISTINCT user_id) FROM revisions WHERE \"updatedAt\" > NOW() - INTERVAL '1 hour';" 2>/dev/null || echo "0")
echo "hedgedoc_active_users $ACTIVE_USERS" >> $METRICS_FILE

🚨 故障排查

常见问题及解决方案

问题现象 可能原因 解决方案
服务无法启动 端口冲突 `netstat -tlnp
数据库连接失败 密码错误/网络问题 检查 .env 文件,验证网络连通性
钉钉登录失败 应用配置错误 检查钉钉应用回调地址和密钥
上传文件失败 权限问题 chown -R 1000:1000 /opt/hedgedoc/uploads
内存占用过高 内存泄漏/配置不当 调整JVM参数,重启服务
SSL证书错误 证书格式/路径错误 验证证书路径和权限

诊断命令

bash 复制代码
# 查看所有容器日志
docker-compose logs --tail=100

# 查看特定服务日志
docker-compose logs -f app

# 进入容器调试
docker-compose exec app sh

# 检查网络连通性
docker-compose exec app ping database

# 检查数据库连接
docker-compose exec database psql -U hedgedoc -c "\l"

# 性能监控
docker stats --no-stream

📊 性能优化建议

1. 数据库优化

sql 复制代码
-- 定期执行(每月)
VACUUM FULL ANALYZE;
REINDEX TABLE notes, revisions, users;

-- 调整参数(根据实际负载)
ALTER SYSTEM SET shared_buffers = '4GB';
ALTER SYSTEM SET work_mem = '64MB';
ALTER SYSTEM SET effective_cache_size = '12GB';

2. 应用层优化

yaml 复制代码
# 在docker-compose.yml中调整
app:
  deploy:
    resources:
      limits:
        cpus: '4'
        memory: 8G
      reservations:
        cpus: '2'
        memory: 4G
  environment:
    NODE_OPTIONS: "--max-old-space-size=6144"
    CMD_DB_POOL_MAX: 100
    CMD_CACHE_TTL: 14400

3. Nginx优化

nginx 复制代码
# 增加缓冲区
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;

# 启用Gzip
gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;

🔄 更新与升级

升级流程

bash 复制代码
# 1. 备份当前系统
cd /opt/hedgedoc
./scripts/backup.sh

# 2. 拉取新版本镜像
docker-compose pull

# 3. 停止服务
docker-compose down

# 4. 启动新版本
docker-compose up -d

# 5. 验证升级
docker-compose logs --tail=100 app
curl -k https://docs.internal.com/health

🎯 部署检查清单

部署完成后检查项

  • 所有容器正常运行:docker-compose ps
  • 服务健康检查通过:curl -k https://docs.internal.com/health
  • SSL证书有效:浏览器访问验证
  • 钉钉登录正常:测试登录流程
  • 文件上传功能正常:测试上传图片
  • 实时协作正常:两台设备同时编辑
  • 备份脚本运行正常:执行一次备份测试
  • 监控告警配置:配置基础监控

📞 技术支持

内部联系方式

  • 运维团队:运维组内线 1001
  • 钉钉支持:钉钉应用管理员
  • 应用开发:开发团队负责人

外部资源


注意:本文档为内网部署专用方案,所有配置均针对内部网络环境优化。如需映射到外网,需额外配置防火墙、安全组和DDOS防护。