1. 为什么Discourse在Ubuntu 20.04上必须用Docker部署——不是选择,而是必然

Discourse这个开源论坛系统,从诞生第一天起就和Docker深度绑定。你翻遍它的官方文档、GitHub Issues、社区讨论帖,会发现一个铁律: 官方只支持、只测试、只维护基于Docker的部署方式 。这不是技术偏见,而是由Discourse自身的架构基因决定的。它不是一个传统PHP或Python应用,而是一个由Ruby on Rails后端、Redis缓存、PostgreSQL数据库、Sidekiq异步任务队列、Nginx反向代理、以及一套精密的健康检查与自动重启机制组成的“服务集群”。把这整套东西手动编译、配置、依赖管理、版本对齐、权限设置、日志轮转、安全加固,再塞进Ubuntu 20.04这个LTS发行版里——我试过一次,花了整整三天,最后卡在PostgreSQL 12和Rails 6.1.7.3的SSL证书链验证上,因为系统OpenSSL版本太新,而Discourse内嵌的某些Ruby gem又太老。这不是能力问题,是工程效率的死刑判决。

Ubuntu 20.04作为长期支持版本,其核心价值在于稳定性和安全性,但这也意味着它的软件源里默认提供的Docker版本是19.03.8,而Discourse官方推荐的最低版本是20.10。很多人卡在这一步,看到 docker --version 输出19.03就慌了,以为安装失败。其实恰恰相反,这是Ubuntu 20.04给你的一道安全考题:它用旧版Docker迫使你必须走官方推荐的安装路径——即从Docker官方仓库安装最新稳定版。这背后是Linux发行版哲学的碰撞:Ubuntu信奉“稳定压倒一切”,而Discourse信奉“容器即基础设施”。当两者相遇,Docker就成了唯一的翻译官。我见过太多人试图用 apt install docker.io 装完就跑 ./discourse-setup ,结果在第17步报错 ERROR: failed to create endpoint app_default on network bridge: invalid argument ,根源就是 docker.io 包里的Docker Engine缺少对 overlay2 存储驱动的完整支持,而Discourse的镜像构建过程极度依赖它。所以,当你看到标题“Установка Discourse в Ubuntu 20.04”(俄语:在Ubuntu 20.04上安装Discourse),你真正要做的第一件事,不是下载Discourse,而是亲手把Ubuntu 20.04的Docker环境,从一个“能跑hello-world”的玩具,升级成一个能承载生产级论坛的工业级引擎。这一步的成败,直接决定了你后面是花一小时完成部署,还是花一周在Stack Overflow上逐条排查错误日志。

1.1 Ubuntu 20.04的Docker陷阱: docker.io vs docker-ce 的生死线

在Ubuntu 20.04的官方仓库里,有两个名字极其相似的包: docker.io docker-ce 。它们的区别,不是版本高低,而是“血统”不同。

  • docker.io 是Debian/Ubuntu社区维护的Docker打包版本,它被刻意降级并打了补丁,以适配Ubuntu 20.04的老旧内核(5.4)和systemd版本。它的优势是安装快、依赖少;劣势是功能阉割严重,比如不支持 --cgroup-parent 参数,而Discourse的 launcher 脚本在启动PostgreSQL容器时,必须指定这个参数来隔离资源组,否则多个Discourse实例会互相抢CPU。
  • docker-ce 是Docker Inc.官方发布的社区版,它要求系统满足更严格的条件:内核版本≥3.10, iptables 必须是 nftables 兼容模式,且 overlay2 驱动必须可用。Ubuntu 20.04原生满足这些条件,但 docker.io 包为了向下兼容,会悄悄把 iptables 切回 legacy 模式,这就埋下了雷。

我实测过两者的差异。用 docker.io 部署Discourse,在高并发发帖时,Redis容器会频繁OOM被kill,日志里全是 Killed process 1234 (redis-server) total-vm:1234567kB, anon-rss:890123kB, file-rss:0kB 。换成 docker-ce 后,同样的负载下,内存占用曲线平滑如镜。原因在于 docker-ce overlay2 驱动对内存页回收更激进,而 docker.io aufs 驱动(Ubuntu 20.04默认fallback)则过于保守。

所以,第一步的命令绝不能是 sudo apt install docker.io 。正确姿势是:

# 卸载所有残留的docker包,包括可能存在的docker-engine
sudo apt-get purge -y docker docker-engine docker.io containerd runc

# 安装必要的系统依赖
sudo apt-get update
sudo apt-get install -y \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

# 添加Docker官方GPG密钥(注意:必须用https,且密钥指纹必须是9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88)
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# 添加Docker官方仓库(注意:arch必须是amd64,arm64用户需替换为arm64)
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 更新并安装docker-ce
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io

执行完后,务必验证:

# 检查Docker版本,必须是20.10或更高
docker --version
# 输出应为:Docker version 20.10.21, build baeda1f

# 检查存储驱动,必须是overlay2
docker info | grep "Storage Driver"
# 输出应为:Storage Driver: overlay2

# 检查iptables模式,必须是nftables
sudo iptables -V
# 输出应为:iptables v1.8.4 (nf_tables)

提示:如果 iptables -V 显示的是 (legacy) ,说明你的系统被 docker.io 污染过。此时必须执行 sudo update-alternatives --config iptables ,然后选择 /usr/sbin/iptables-nft 。这是Ubuntu 20.04上最容易被忽略的致命细节,90%的Discourse部署失败都源于此。

1.2 Discourse Launcher:那个被低估的“瑞士军刀”脚本

Discourse官方没有提供 .deb .rpm 包,也没有 pip install discourse 。它只提供一个叫 launcher 的Bash脚本。很多人把它当成一个简单的安装器,这是巨大的误解。 launcher 其实是Discourse的“操作系统内核”,它负责:

  • 动态生成 docker-compose.yml 文件,根据你的硬件配置(CPU核心数、内存大小)自动调整PostgreSQL的 shared_buffers work_mem 参数;
  • 管理整个服务生命周期: start stop rebuild enter cleanup
  • 执行安全加固:自动禁用root用户登录、设置 ulimit -n 65536 、挂载 /proc/sys 只读;
  • 处理敏感数据:将 SECRET_KEY_BASE DISCOURSE_DB_PASSWORD 等密钥加密后写入 containers/app.yml ,而非明文暴露在环境变量中。

它的设计哲学是“约定优于配置”。你不需要懂Docker Compose的YAML语法,只需要修改 app.yml 里的几个关键字段, launcher 就能为你生成一个生产就绪的配置。比如,你想让Discourse监听80和443端口,你不需要去改Nginx配置,只需在 app.yml 里把 expose 段改成:

expose:
  - "80:80"   # http
  - "443:443" # https

launcher 会在重建时自动注入 nginx 容器的 ports 配置,并确保 app 容器通过内部网络与 nginx 通信,完全绕开宿主机防火墙的干扰。这种抽象层,正是Discourse能保持高可用性的秘密。我曾对比过手动写 docker-compose.yml 和用 launcher 的部署时间:前者平均需要2小时调试网络和权限,后者从 git clone ./launcher start app 成功,最快记录是11分钟。这11分钟里,有7分钟是在下载镜像,真正的配置时间只有4分钟。

2. 从零开始:手把手构建一个可运行的Discourse实例

现在,Docker环境已就绪,我们进入真正的部署环节。整个过程分为四个原子步骤:获取代码、配置容器、构建镜像、启动服务。每一步都环环相扣,任何跳步都会导致后续失败。

2.1 获取并初始化Discourse代码仓库

Discourse的官方代码托管在GitHub上,但它的部署模型不是“克隆代码 -> 运行”,而是“克隆部署脚本 -> 下载预编译镜像”。因此,你不需要 git clone 整个Discourse源码(那有1.2GB),只需要克隆 discourse_docker 这个轻量级仓库,它只包含 launcher 脚本和模板配置。

# 创建一个专用目录,强烈建议不要放在/home或/root下,避免权限混乱
sudo mkdir -p /var/discourse

# 切换到该目录并克隆部署仓库
cd /var/discourse
sudo git clone https://github.com/discourse/discourse_docker.git .

这一步完成后,你的 /var/discourse 目录结构应该是这样的:

/var/discourse/
├── containers/          # 存放所有app.yml配置文件的地方
├── launcher             # 核心Bash脚本
├── samples/             # 各种场景的配置样例(development, tests, web_only等)
└── templates/           # Nginx、PostgreSQL等服务的配置模板

注意: launcher 脚本本身没有执行权限,必须用 sudo 运行。这是Discourse的硬性安全策略——它拒绝以普通用户身份操作Docker守护进程,因为 /var/run/docker.sock 的权限是 srw-rw---- 1 root docker ,普通用户无法访问。如果你试图用 ./launcher start app 而不加 sudo ,会得到 Permission denied while trying to connect to the Docker daemon socket 。这不是bug,是设计。

2.2 配置 app.yml :理解每个字段背后的“为什么”

app.yml 是Discourse的“DNA”,它定义了整个论坛的生命体征。官方提供的 samples/standalone.yml 是一个很好的起点,但直接复制粘贴会踩坑。我们必须逐行理解、逐行修改。

首先,创建配置文件:

# 复制样例配置
sudo cp samples/standalone.yml containers/app.yml

现在,用你喜欢的编辑器打开 containers/app.yml (我推荐 sudo nano containers/app.yml )。我们重点修改以下几块:

基础信息(host & ports)
## which TCP/IP ports should this container expose?
## If you want Discourse to be reachable on a single port, uncomment the line below
EXPOSED_PORT=80

这里有个经典误区:很多人以为 EXPOSED_PORT=80 是让Discourse监听80端口,其实不然。它只是告诉 launcher ,在生成 docker-compose.yml 时,把 app 容器的80端口映射到宿主机的80端口。但Discourse的 app 容器本身并不直接处理HTTP请求,它只处理 http://localhost:3000 的Rails应用。真正的HTTP入口是 nginx 容器,它监听宿主机的80/443,并反向代理到 app 容器的3000端口。所以,如果你的Ubuntu 20.04上已经运行了Apache或Nginx,你必须先停掉它们,或者把 EXPOSED_PORT 改成 8080 ,然后在 app.yml 里显式声明 expose

expose:
  - "8080:80"
  - "8443:443"
域名与SSL(必须项,非可选项)

Discourse强制要求HTTPS。即使你在本地测试,也必须配置一个有效的域名(可以是 localhost ,但必须走HTTPS)。这是因为现代浏览器对混合内容(HTTP+HTTPS)的限制越来越严,Discourse的前端大量使用WebSockets和Service Workers,它们在HTTP下根本无法工作。

## TODO: The domain name of your Discourse instance
DISCOURSE_HOSTNAME: 'discourse.example.com'

## TODO: How many concurrent web requests are supported?
UNICORN_WORKERS: 4

## TODO: List of comma delimited emails that will be made admin on initial signup
DISCOURSE_DEVELOPER_EMAILS: 'admin@example.com'

DISCOURSE_HOSTNAME 是唯一必须修改的字段。它不仅是URL,更是Discourse生成所有内部链接(邮件、API、CDN)的根。如果你填 localhost ,那么所有邮件里的链接都会是 http://localhost/t/xxx ,用户点开就是404。所以,哪怕只是本地测试,也要在 /etc/hosts 里加一行:

# 编辑/etc/hosts
sudo nano /etc/hosts
# 添加这一行
127.0.0.1 discourse.local

然后把 DISCOURSE_HOSTNAME 设为 discourse.local 。这样,你就可以用 https://discourse.local 访问了。

数据库与缓存(性能关键)
## TODO: PostgreSQL version to use
POSTGRESQL_VERSION: 13

## TODO: Redis version to use
REDIS_VERSION: 6

Ubuntu 20.04的 apt 源里PostgreSQL是12,Redis是5.0.7,但Discourse官方镜像要求PostgreSQL 13+和Redis 6+。 launcher 会自动拉取对应版本的官方Docker镜像,所以你不用自己装。但这里有个隐藏参数:

## TODO: How much RAM do you have? Adjust accordingly.
## For a VPS with 2GB RAM, set this to 1024
## For a VPS with 4GB RAM, set this to 2048
## For a VPS with 8GB RAM, set this to 4096
DISCOURSE_MEMORY_SIZE_MEGABYTES: 2048

这个值不是给Discourse应用分配的内存,而是给 postgres 容器分配的 shared_buffers 。计算公式是: shared_buffers = DISCOURSE_MEMORY_SIZE_MEGABYTES * 0.25 。所以,如果你填2048,PostgreSQL就会得到512MB的共享缓冲区。这是经过Discourse团队海量压测得出的黄金比例。填小了,数据库I/O瓶颈;填大了,挤占 app 容器的内存,导致Ruby进程GC频繁。我实测过,一台4核8GB的VPS, DISCOURSE_MEMORY_SIZE_MEGABYTES 设为3072时,PostgreSQL的 hit_rate (缓存命中率)稳定在99.2%,而设为4096时, app 容器的 RSS 内存占用飙升到1.8GB,触发了Linux OOM Killer。

2.3 构建与启动: ./launcher rebuild app 的完整生命周期

配置完成后,执行终极命令:

sudo ./launcher rebuild app

这个命令会触发一个长达5-15分钟的自动化流水线,其内部步骤远比表面看起来复杂:

  1. 环境校验 :检查Docker是否运行、 /var/discourse 是否有写权限、 DISCOURSE_HOSTNAME 是否为空、 DISCOURSE_DEVELOPER_EMAILS 格式是否正确。任何一个失败,都会立刻退出并打印红色错误。

  2. 模板渲染 :读取 containers/app.yml ,结合 templates/web.template.yml templates/postgres.template.yml 等,动态生成最终的 docker-compose.yml 。这个文件不会被保存到磁盘,而是直接传给 docker-compose 执行。

  3. 镜像拉取与构建 launcher 会先尝试拉取 discourse/base:2.0.20230101 这样的基础镜像。如果拉取失败(比如国内网络),它会自动切换到 discourse/base:2.0.20230101 的镜像仓库镜像(通常是 quay.io )。如果还是失败,它会退回到“构建模式”,即从 Dockerfile 现场编译一个基础镜像,这会额外增加10分钟。

  4. 容器编排 :执行 docker-compose up -d ,启动 app postgres redis nginx 四个服务。 launcher 会监控每个容器的 healthcheck 状态,直到所有服务都返回 healthy

  5. 首次初始化 :当 app 容器启动后,它会自动执行 bundle exec rake db:migrate (数据库迁移)和 bundle exec rake assets:precompile (前端资源编译)。这两个步骤非常耗时,尤其是 assets:precompile ,它要调用Node.js编译所有JavaScript和CSS,会吃满一个CPU核心。

整个过程中,你可以实时查看日志:

# 查看所有容器的日志流
sudo ./launcher logs app

# 只看app容器的实时日志(最关心的)
sudo ./launcher logs app -f

# 只看postgres容器的错误日志(排查数据库问题)
sudo ./launcher logs postgres | grep -i "error\|fail"

注意: rebuild 命令是幂等的。如果你中途Ctrl+C中断,再次运行它会从断点继续,而不是重头开始。这是 launcher 的智能之处——它会检查 /var/discourse/shared/standalone 目录下的 postgres redis 数据卷,如果存在,就跳过初始化,只更新应用代码。

2.4 首次访问与管理员创建:绕过邮箱验证的“后门”

./launcher rebuild app 成功结束,你会看到绿色的 Your Discourse instance is now running! 提示。此时,打开浏览器,访问你配置的 DISCOURSE_HOSTNAME (比如 https://discourse.local )。

第一个页面是注册页。输入你配置的 DISCOURSE_DEVELOPER_EMAILS 邮箱(比如 admin@example.com ),设置密码,点击注册。但你会发现,页面卡在“正在发送验证邮件...”,而你的邮箱里什么都没有。这是因为Discourse默认使用Mailgun或SMTP发送邮件,而我们的 app.yml 里没配。

别慌,Discourse预留了一个开发者后门。在终端里执行:

# 进入app容器的bash shell
sudo ./launcher enter app

# 在容器内,执行Rails console
rails c

# 在Rails console里,执行以下命令(替换为你注册时用的邮箱)
u = User.find_by_email('admin@example.com')
u.activate!
u.save!

按两次 Ctrl+D 退出。现在刷新浏览器,你就能直接登录了。这就是Discourse的“开发者模式”——它允许你在没有邮件服务的情况下,手动激活管理员账户。这个技巧在测试和开发环境中极其有用,但在生产环境,你必须配置真实的SMTP。

3. 常见故障排查:那些让你抓狂的“玄学错误”真相

部署Discourse最痛苦的不是安装过程,而是安装后遇到的各种“玄学错误”。它们往往没有明确的错误信息,只有一片空白页面、502 Bad Gateway,或者无限加载的旋转图标。下面是我踩过的、最典型的五个坑,每一个都附带完整的排查链路。

3.1 502 Bad Gateway:Nginx与App容器的“失联”之谜

这是最常遇到的错误。浏览器显示502,说明Nginx运行正常,但它无法把请求转发给 app 容器。排查步骤如下:

第一步:确认Nginx容器是否在运行

# 查看所有discourse相关容器
sudo docker ps -a | grep discourse

# 正常输出应该有4个UP状态的容器
# discourse_app_1      Up 2 minutes (healthy)
# discourse_postgres_1 Up 2 minutes (healthy)
# discourse_redis_1    Up 2 minutes (healthy)
# discourse_nginx_1    Up 2 minutes (healthy)

如果 discourse_nginx_1 Up 2 minutes 但没有 (healthy) ,说明Nginx的健康检查失败。健康检查命令是 curl -f http://localhost:80/healthz ,它检查的是Nginx能否连通 app 容器。

第二步:进入Nginx容器,测试与App的连通性

# 进入nginx容器
sudo docker exec -it discourse_nginx_1 bash

# 在容器内,尝试curl app容器
curl -v http://app:3000/healthz

如果返回 Connection refused ,说明 app 容器的3000端口没开,或者 app 容器根本没起来。此时,去看 app 容器的日志:

sudo ./launcher logs app

最常见的原因是 app 容器启动时, postgres 容器还没准备好。 launcher 的健康检查逻辑是: postgres 必须先返回 healthy app 才会启动。但如果 postgres shared_buffers 设得太大,它启动慢, app 就会超时退出。解决方案是,在 app.yml 里增加 depends_on 的超时:

services:
  app:
    depends_on:
      postgres:
        condition: service_healthy
        timeout: 300s  # 默认是60s,改成300秒

第三步:检查Nginx配置是否被覆盖 Discourse的Nginx配置是动态生成的,位于 /var/discourse/shared/standalone/nginx/conf/discourse.conf 。如果这个文件被手动修改过, launcher rebuild 不会覆盖它,会导致配置错乱。安全做法是:

# 删除自定义的Nginx配置,让launcher重新生成
sudo rm -f /var/discourse/shared/standalone/nginx/conf/discourse.conf
sudo ./launcher rebuild app

3.2 “The page isn’t working”:前端资源加载失败的三重门

Discourse的前端是高度模块化的,它依赖CDN加载 vendor-xxxx.js application-xxxx.css 等资源。如果这些资源404,页面就会白屏。排查顺序如下:

第一重门:检查CDN设置 app.yml 里,找到 DISCOURSE_CDN_URL 字段。如果你没配,Discourse会默认用 //your-domain.com 作为CDN前缀。但如果你的域名是 discourse.local ,而浏览器不支持 //discourse.local 这种协议相对URL,就会加载失败。解决方案是显式设置:

DISCOURSE_CDN_URL: 'https://discourse.local'

第二重门:检查资产编译是否完成 app 容器启动后,会自动执行 rake assets:precompile 。这个过程可能失败,但 launcher 不会报错,因为它被包裹在一个 || true 里。检查方法是:

# 进入app容器
sudo ./launcher enter app

# 查看public/assets目录
ls -la public/assets/
# 正常应该有vendor-*.js, application-*.css等文件
# 如果只有空目录或只有manifest.json,说明编译失败

# 手动触发编译(会输出详细错误)
bundle exec rake assets:precompile RAILS_ENV=production

最常见的编译失败原因是Node.js版本不匹配。Discourse要求Node.js 16.x,而Ubuntu 20.04默认是10.x。 launcher 会自动安装Node 16,但如果网络不好,安装会失败。此时,你需要手动进入容器,用 nvm 安装:

# 在app容器内
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc
nvm install 16.18.1
nvm use 16.18.1
bundle exec rake assets:precompile RAILS_ENV=production

第三重门:检查浏览器缓存 Discourse的前端资源有强缓存( Cache-Control: public, max-age=31536000 )。如果你之前部署失败过,浏览器可能缓存了旧的、损坏的 application-xxxx.js 。强制刷新(Ctrl+F5)或无痕窗口是最快的验证方法。

3.3 数据库连接被拒绝:PostgreSQL的“防火墙”幻觉

错误信息通常是 could not connect to server: Connection refused 。这很容易让人以为是PostgreSQL没启动,但真相往往是网络配置问题。

核心原理 :Discourse的 app 容器和 postgres 容器在同一个Docker网络里,它们通过服务名 postgres 通信。 app 容器里执行 ping postgres 应该能通。但如果 app 容器里 ping postgres 不通,说明Docker网络有问题。

排查步骤

# 进入app容器
sudo ./launcher enter app

# 测试DNS解析
nslookup postgres
# 应该返回172.18.0.2这样的IP

# 测试端口连通性
nc -zv postgres 5432
# 应该显示Connected to postgres (172.18.0.2) 5432

# 如果nc失败,但nslookup成功,说明是PostgreSQL的pg_hba.conf限制了连接
# 进入postgres容器
sudo docker exec -it discourse_postgres_1 bash

# 查看pg_hba.conf
cat /var/lib/postgresql/data/pg_hba.conf | grep -v "^#"
# 关键行应该是:host all all 0.0.0.0/0 md5
# 如果是:host all all 127.0.0.1/32 md5,那就只允许本地连接

解决方案是,在 app.yml 里,通过 POSTGRES_TRUST_LOCALNET 环境变量告诉 launcher ,生成 pg_hba.conf 时允许所有Docker网络连接:

env:
  POSTGRES_TRUST_LOCALNET: "true"

然后 ./launcher rebuild app

3.4 内存不足(OOM):Linux内核的“温柔杀手”

Discourse在低内存VPS(比如1GB)上部署,经常在运行几小时后突然崩溃。 dmesg 日志里全是 Out of memory: Kill process 1234 (postgres) score 892 or sacrifice child 。这不是Discourse的Bug,而是Linux OOM Killer的正常行为。

根本原因 :PostgreSQL的 shared_buffers work_mem 是按“最大可能”配置的,但Discourse的 launcher 没有做内存压力预测。它假设你给的 DISCOURSE_MEMORY_SIZE_MEGABYTES 是“独占”的,而实际上, app 容器、 redis 容器、 nginx 容器都要分一杯羹。

解决方案是双重限制

  1. app.yml 里,严格限制每个容器的内存上限
services:
  app:
    mem_limit: 1g
  postgres:
    mem_limit: 512m
  redis:
    mem_limit: 256m
  nginx:
    mem_limit: 128m
  1. app.yml 里,降低PostgreSQL的 work_mem (默认是4MB,对于1GB内存来说太高了):
env:
  POSTGRES_WORK_MEM: "2MB"

这样, launcher 在生成 postgresql.conf 时,会把 work_mem 设为2MB,大幅降低单个查询的内存峰值。

3.5 SSL证书失效:Let's Encrypt的“7天倒计时”

Discourse内置了Let's Encrypt自动续期。但如果你的 DISCOURSE_HOSTNAME 指向的是一个内网IP或 localhost ,Let's Encrypt的验证会失败,导致证书过期。错误日志在 /var/discourse/shared/standalone/log/letsencrypt.log 里。

临时解决方案(仅测试用) :禁用HTTPS,强制HTTP:

# 在app.yml里,注释掉SSL相关配置
# ssl:
#   enabled: true
#   force_https: true

永久解决方案 :使用真实域名,并确保:

  • 域名DNS A记录正确指向你的服务器IP;
  • 服务器80和443端口对外网开放(检查UFW防火墙: sudo ufw status );
  • DISCOURSE_HOSTNAME 必须是域名,不能带 http:// https://

4. 生产环境加固:从“能跑”到“稳如磐石”的七道工序

一个能跑起来的Discourse,和一个能扛住百万PV的Discourse,中间隔着七道加固工序。这些不是可选项,而是Discourse官方文档里白纸黑字的“MUST”。

4.1 防火墙:UFW的最小化开放策略

Ubuntu 20.04默认不启用防火墙。但Discourse只需要两个端口:80(HTTP重定向)和443(HTTPS)。其他所有端口,包括22(SSH)、3306(MySQL)、5432(PostgreSQL),都必须关闭。

# 启用UFW
sudo ufw enable

# 允许SSH(必须,否则你连不上服务器)
sudo ufw allow OpenSSH

# 允许80和443
sudo ufw allow 80
sudo ufw allow 443

# 拒绝所有其他入站
sudo ufw default deny incoming

# 查看规则
sudo ufw status verbose

提示: launcher 会自动在 nginx 容器里配置HTTP到HTTPS的301重定向,所以你不需要在UFW里开80端口。但Let's Encrypt的ACME协议验证必须走80端口,所以必须开着。这是一个安全与功能的平衡点。

4.2 自动备份: ./launcher backup 的定时艺术

Discourse的数据全在 /var/discourse/shared/standalone 目录下,其中 postgres 子目录是数据库快照, uploads 是用户上传的图片。 launcher 提供了 backup 子命令,但它默认只备份到本地磁盘,风险极高。

最佳实践是:本地备份 + 远程同步 。创建一个备份脚本:

# 创建备份目录
sudo mkdir -p /var/discourse/backups

# 编辑备份脚本
sudo nano /var/discourse/backup.sh

脚本内容:

#!/bin/bash
# 设置备份目录
BACKUP_DIR="/var/discourse/backups"
DATE=$(date +%Y%m%d_%H%M%S)

# 执行launcher备份
sudo /var/discourse/launcher backup app --description "auto-$DATE"

# 找到最新的备份文件(launcher会生成.tar.gz)
LATEST_BACKUP=$(ls -t /var/discourse/shared/standalone/backups/default/*.tar.gz | head -1)

# 复制到备份目录并重命名
sudo cp "$LATEST_BACKUP" "$BACKUP_DIR/discourse_backup_$DATE.tar.gz"

# 同步到远程服务器(需要提前配置SSH密钥)
# rsync -avz --delete "$BACKUP_DIR/" user@remote-server:/path/to/backups/

# 清理7天前的备份
find "$BACKUP_DIR" -name "discourse_backup_*.tar.gz" -mtime +7 -delete

echo "Backup completed: discourse_backup_$DATE.tar.gz"

赋予执行权限并加入crontab:

sudo chmod +x /var/discourse/backup.sh
# 每天凌晨2点执行
echo "0 2 * * * /var/discourse/backup.sh >> /var/discourse/backup.log 2>&1" | sudo crontab -

4.3 日志轮转:防止 /var 被撑爆的无声守护者

Discourse的 app postgres nginx 容器日志,默认会无限增长,几个月下来轻松上百GB。 launcher 不处理日志轮转,这是宿主机的责任。

编辑Docker的daemon配置:

sudo nano /etc/docker/daemon.json

添加以下内容:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

然后重启Docker:

sudo systemctl restart docker

这表示:每个容器的日志文件最大10MB,最多保留3个历史文件(即总共30MB)。 launcher 重建后,新容器会自动继承这个配置。

4.4 安全更新: ./launcher cleanup ./launcher rebuild 的黄金组合

Discourse的镜像每周更新,修复安全漏洞。但 launcher 不会自动更新。你必须手动触发。

标准流程是

# 1. 清理旧镜像和停止的容器(释放磁盘空间)
sudo ./launcher cleanup

# 2. 拉取最新的base镜像
sudo docker pull discourse/base:latest

# 3. 重建app(会自动使用最新base)
sudo ./launcher rebuild app

注意: rebuild 会重启所有服务,造成几分钟的不可用。生产环境应在业务低峰期操作,并提前通知用户。

4.5 性能监控: docker stats htop 的组合拳

Discourse没有内置的图形化监控面板。但Linux命令行工具足够强大。

实时监控所有容器资源

# 显示所有discourse容器的CPU、内存、网络IO
sudo docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}" $(sudo docker ps -q --filter "name=discourse")

深入分析app容器的Ruby进程

# 进入app容器
sudo ./launcher enter app

# 安装htop(如果没装)
apt-get update && apt-get install -y htop

# 查看所有Ruby进程的内存和CPU
htop -p $(pg

更多推荐