Mastodon 4.7.3 Docker Compose 部署指南
用官方镜像与官方 compose 编排(web/streaming/sidekiq/db/redis 五服务)从零部署 Mastodon 联邦社交实例:密钥生成、数据库迁移、tootctl 建管理员、注册发嘟时间线实测,并完整沉淀 web 服务 Created 未启动的排障实录。
Mastodon 4.7.3 Docker Compose 部署指南
目标:用官方镜像
ghcr.io/mastodon/mastodon:v4.7.3与官方 docker-compose 编排,从零部署一套自托管 Mastodon 联邦社交实例(web / streaming / sidekiq / PostgreSQL / Redis 五服务),完成密钥生成、数据库迁移、管理员创建、发帖与时间线实测。已在 Ubuntu 22.04(x86_64)服务器、Mastodon v4.7.3 上全量实测通过,文中所有命令与回显均为实测原文。
一、环境信息
本文所有命令与输出均在以下环境实测采集:
- 操作系统:Ubuntu 22.04 LTS(x86_64),6 核 / 20GB 内存
- Docker:29.9.2(含 Docker Compose v5.6.1 插件)
- Mastodon:v4.7.3(2026-10-26 检索时的官方最新稳定版)
- 数据库:PostgreSQL 14.24(官方镜像
postgres:14-alpine,官方 compose 固定 14 系) - 缓存/队列:Redis 7.4.11(官方镜像
redis:7-alpine) - Web 进程:Ruby 4.0.6 + Puma 8.0.2(启动日志实测回显,见 4.8)
- Streaming 进程:Node.js(独立镜像
ghcr.io/mastodon-streaming:v4.7.3) - 实例域名:
mastodon.local(测试实例;生产环境规划见 4.3 的 LOCAL_DOMAIN 说明) - 部署目录:
/opt/mastodon(本文示例路径,读者可自定) - 端口:web
127.0.0.1:8135 → 3000,streaming127.0.0.1:8133 → 4000(均绑回环,前置反向代理后对外) - 五服务常驻内存实测(启动并完成发帖/媒体上传后采样):
| 服务 | 内存占用 | 说明 |
|---|---|---|
| web | 489.4 MiB | Puma,WEB_CONCURRENCY=1 / MAX_THREADS=5 |
| sidekiq | 354.7 MiB | 后台任务 |
| streaming | 66.1 MiB | 实时推送,STREAMING_CLUSTER_NUM=1 |
| db | 48.6 MiB | PostgreSQL 14,空库+少量测试数据 |
| redis | 4.2 MiB | 队列+缓存 |
| 合计 | 约 963 MiB | 2GB 内存虚拟机即可起步,建议 ≥4GB |
💡 技术拓展:Mastodon 的部署制品不是单一镜像,而是「两个应用镜像 + 两个基础服务镜像」的组合——web 与 sidekiq 共用同一个 Rails 镜像(
ghcr.io/mastodon/mastodon),streaming 是独立的 Node.js 镜像(ghcr.io/mastodon/mastodon-streaming)。这类「多进程分镜像」的架构对比其他社交服务端(如 GoToSocial 的单二进制)资源占用明显更高,选型背景见《Mastodon 4.7.3 介绍与选型指南》(https://www.quzuo.net/document/gzKZrJqqJRK5) 第七章。
二、镜像选型说明
官方在 ghcr.io 发布两个应用镜像,数据库与缓存直接复用 PostgreSQL/Redis 官方镜像(与官方 docker-compose.yml 固定写法一致):
| 镜像 | 本实测 tag | 解压后体积 | 用途 |
|---|---|---|---|
ghcr.io/mastodon/mastodon | v4.7.3 | 1.4GB | web(Puma)与 sidekiq 共用,命令行参数区分进程 |
ghcr.io/mastodon/mastodon-streaming | v4.7.3 | 463MB | streaming(Node.js 实时推送) |
postgres:14-alpine | 14.24 | 414MB | 主存储,官方 compose 固定 14 系 |
redis:7-alpine | 7.4.11 | 57.8MB | 队列/缓存/订阅 |
选型要点:
| tag 写法 | 说明 | 建议 |
|---|---|---|
v4.7.3 | 与 GitHub Release 一一对应的版本锁 tag | ✅ 生产推荐,升级可控 |
v4.7 系历史 tag | 官方对每个补丁版单独出 tag | 回滚用 |
latest | 跟随最新发布 | 不建议,升级不可控 |
关键差异:官方强绑定「镜像版本 = 代码版本」,数据库迁移(db:migrate)随版本推进,因此锁定具体版本 tag 是唯一稳妥选择;latest 在升级窗口期可能拉到未兼容的版本组合。
⚠️ 网络说明(镜像获取降级链实测):本环境直连 ghcr.io 拉取实测不可用(连接超时),已改走代理
http://192.168.1.62:7897获取四个镜像后docker load载入(mastodon 主镜像下载约 723MB、streaming 约 210MB、postgres14-alpine 约 229MB)。若你的服务器可直连 ghcr.io,直接docker compose pull即可;代理为当前实测可用渠道。
三、前置条件
-
Docker 与 Compose 插件:宿主机安装 Docker 24+ 与
docker composev2 插件。验证:docker compose version实测回显:
Docker Compose version v5.6.1 -
端口规划:web 8135、streaming 8133 绑定在
127.0.0.1(建议前置反向代理对外)。确认空闲:ss -ltn | grep -E ':8135|:8133'实测回显(无输出 = 端口空闲):
-
资源:五服务常驻合计约 963MiB 内存(见第一章表),加上系统与其他服务,建议 ≥4GB 内存、预留 10GB 磁盘(镜像 2.3GB + 数据目录)。
-
域名与身份规划:
LOCAL_DOMAIN是实例的联邦身份(@用户名@域名的后半段),一旦开放联邦后不可安全更改,必须一次定型。本文用mastodon.local演示。 -
SMTP 规划:生产实例的注册确认、通知邮件依赖 SMTP;本文实测环境未接入外部邮件服务,相关影响与验证见第十二章 FAQ 12.3。
-
Docker 网段规划:自定义网络建议显式指定子网,先查看已占用网段再选空闲槽位,避免与其他业务栈冲突:
docker network ls
四、部署步骤
4.1 建立部署目录与编排文件
创建部署目录并写入编排文件:
mkdir -p /opt/mastodon && cd /opt/mastodon
/opt/mastodon/docker-compose.yml(在官方 v4.7.3 编排基础上补齐显式项目名、容器名、子网与端口,逐项说明见第八章):
name: mastodon-4.7.3-stack
services:
db:
restart: always
image: postgres:14-alpine
container_name: mastodon-4.7.3-db
shm_size: 256mb
networks:
- internal_network
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
volumes:
- ./postgres14:/var/lib/postgresql/data
environment:
- 'POSTGRES_HOST_AUTH_METHOD=trust'
redis:
restart: always
image: redis:7-alpine
container_name: mastodon-4.7.3-redis
networks:
- internal_network
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
volumes:
- ./redis:/data
web:
image: ghcr.io/mastodon/mastodon:v4.7.3
container_name: mastodon-4.7.3-web
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
networks:
- external_network
- internal_network
healthcheck:
test: ['CMD-SHELL', "curl -s --noproxy localhost localhost:3000/health | grep -q 'OK' || exit 1"]
ports:
- '127.0.0.1:8135:3000'
depends_on:
- db
- redis
volumes:
- ./public/system:/opt/mastodon/public/system
streaming:
image: ghcr.io/mastodon/mastodon-streaming:v4.7.3
container_name: mastodon-4.7.3-streaming
restart: always
env_file: .env.production
command: node ./streaming/index.js
networks:
- external_network
- internal_network
healthcheck:
test: ['CMD-SHELL', "curl -s --noproxy localhost localhost:4000/api/v1/streaming/health | grep -q 'OK' || exit 1"]
ports:
- '127.0.0.1:8133:4000'
depends_on:
- db
- redis
sidekiq:
image: ghcr.io/mastodon/mastodon:v4.7.3
container_name: mastodon-4.7.3-sidekiq
restart: always
env_file: .env.production
command: bundle exec sidekiq
depends_on:
- db
- redis
networks:
- external_network
- internal_network
volumes:
- ./public/system:/opt/mastodon/public/system
healthcheck:
test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\\ 8' || false"]
networks:
external_network:
name: mastodon-4.7.3-ext
driver: bridge
ipam:
config:
- subnet: 10.89.63.0/24
internal_network:
name: mastodon-4.7.3-int
driver: bridge
internal: true
ipam:
config:
- subnet: 10.89.64.0/24
⚠️ 注意:官方编排的 web 服务卷挂载路径为
/mastodon/public/system,本实测统一改为/opt/mastodon/public/system(与 4.x 官方镜像内工作目录一致),db/redis/streaming/sidekiq 四服务的挂载路径与官方保持一致,两种写法均可,但不可与已产生的数据目录混用。
4.2 生成三组密钥
Mastodon 启动前必须在 .env.production 写入三组密钥,全部由官方提供的一次性命令生成。三个命令都用 docker compose run --rm web 执行(在部署目录下运行,db/redis 尚未启动也可执行,这三种生成均不依赖数据库)。
第一组:SECRET_KEY_BASE 与 OTP_SECRET(会话加密与两步验证加密,各生成一条 128 位随机串):
docker compose run --rm web bin/rails secret
docker compose run --rm web bin/rails secret
实测回显(两条不同输出,分别填入 SECRET_KEY_BASE 与 OTP_SECRET):
abfce8add58cba2281e3c590df4853146cb947666ee8bc094f18e665d803fd184707e95b69430aeb932a0a1e76fdf1f0e398d5d2347f1081b8349518e2098e00
8db2aca5607f496404125b33074edd1812cbcf117161325dae137fed642462503931b7a8e9c9d39d9f430ee58c0e3c6bf4894b650d5be8be49578cfdb6a01a4e
第二组:Web Push 的 VAPID 密钥对(浏览器推送通知用,公私钥成对):
docker compose run --rm web bin/rails mastodon:webpush:generate_vapid_key
实测回显(一次生成一对,原样分别填入 VAPID_PRIVATE_KEY 与 VAPID_PUBLIC_KEY):
VAPID_PRIVATE_KEY=Kuhr06N6jnu_KobOPoBfifhklQCGmgsTXeU9Mfbfrdk=
VAPID_PUBLIC_KEY=BBYPr1fvk8porSD-3eA6JC2rNcw-iMYlX9Dns5A8uGKubtd6XfLCEvKBCNMPZojEj2eI_lkofaDzthPHoI8QFZs=
第三组:ActiveRecord 加密密钥三件套(v4.7.x 强制要求,用于数据库敏感字段加密):
docker compose run --rm web bin/rails db:encryption:init
实测回显:
Add the following secret environment variables to your Mastodon environment (e.g. .env.production), ensure they are shared across all your nodes and do not change them after they are set:
# Do NOT change these variables once they are set
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=eh5iYqMLw0xH8eONJCyQnwvZsabhX6Cl
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=vogIZwbNStEKaRW3ZgbESYZBvlfCYxax
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=inengYmJ33nKsip0iB2xbjGad57LB8xE
⚠️ 注意:三组密钥一旦投入运行不可再改——尤其 ACTIVE_RECORD_ENCRYPTION 三件套,更换后已加密的数据库字段将无法解密(官方提示原文「do not change them after they are set」)。多节点部署时所有节点必须共用同一套值。
4.3 编写 .env.production
将生成值与实例参数写入 /opt/mastodon/.env.production(本实测部署全文,逐项说明见第七章):
# 实例身份——一经联邦开放后不可安全更改
LOCAL_DOMAIN=mastodon.local
# 运行环境
RAILS_ENV=production
RAILS_LOG_LEVEL=warn
# Redis(Sidekiq 队列 + Streaming pub/sub + 缓存)
REDIS_HOST=redis
REDIS_PORT=6379
# PostgreSQL(主存储)
DB_HOST=db
DB_USER=postgres
DB_NAME=mastodon
DB_PASS=
DB_PORT=5432
# Elasticsearch 全文搜索(本次部署未启用)
ES_ENABLED=false
# 密钥(4.2 生成的实际值)
SECRET_KEY_BASE=87d1fb9528539e4672495c20db82989bc3293a7480eb9670541e24b7c60dd7d9c3ec071e39d1859e85cc9b375476773dfaf06cc882e2f2c1c807d14986dc13b9
OTP_SECRET=4d45b956ce8010fa743a9a06d7059941d7cff01c1342426c335db48743f41817dafb85ce15348f7dc399cb55b785a170ea8bbf22f1db8dc2b18319db971bd803
VAPID_PRIVATE_KEY=skHHXcLCGp9jeVuQ2n6g4s_U_Hoe4gqYlg6EhuXUz_w=
VAPID_PUBLIC_KEY=BDXou3TM0IGj36bVmREh3qJk9ZFIJpAU65CXPeMZxgxOXhTPf_QvWphXeAVpNXwHefPxjQYmQA26dTsLM8PTrdA=
# AR 加密密钥(bin/rails db:encryption:init 生成,4.7.x 强制要求)
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fM6Q8nL3fi13vsPyo0xI3Eau9rM16oby
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=ZFS361EPTn6BlgKem1RkFKyaWWO4U3lu
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=SbMso2GmMmCppYf52akqcGSzN3lDgOc1
# SMTP(注册确认与通知邮件;本次实测未接外部邮件服务)
SMTP_SERVER=localhost
SMTP_PORT=25
SMTP_AUTH_METHOD=none
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_ENABLE_STARTTLS_AUTO=false
SMTP_FROM_ADDRESS=Mastodon <notifications@mastodon.local>
# 单机资源控制(与 PostgreSQL/Redis 同机时收紧并发)
WEB_CONCURRENCY=1
MAX_THREADS=5
STREAMING_CLUSTER_NUM=1
# 媒体与对象存储(本次部署使用本地磁盘 public/system)
S3_ENABLED=false
# 本地演示实例邮箱域名白名单(绕过对 mastodon.local 的 MX 解析校验)
EMAIL_DOMAIN_ALLOWLIST=mastodon.local
说明:DB_PASS 留空与 compose 中 POSTGRES_HOST_AUTH_METHOD=trust 配套——db 服务仅挂在 internal_network(internal: true,无外网路由)上,信任网络内的认证;生产环境如需更强隔离,可改为 POSTGRES_PASSWORD + DB_PASS 的口令认证,两者必须一致。EMAIL_DOMAIN_ALLOWLIST 是因为 mastodon.local 没有真实 MX 记录,若不加入白名单,本域邮箱会被注册校验拒绝(实测正反对照见《Mastodon 4.7.3 配置参数详解》EMAIL_DOMAIN_ALLOWLIST 一节)。
4.4 获取镜像
cd /opt/mastodon && docker compose pull
⚠️ 网络说明:本环境直连 ghcr.io 实测超时,按第二章降级链经代理获取四个镜像后
docker load载入;可直连 ghcr.io 的环境直接docker compose pull即可。载入后确认:
docker images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}' | grep -E 'mastodon|postgres:14-alpine|redis:7-alpine'
实测回显:
postgres:14-alpine 414MB
redis:7-alpine 57.8MB
ghcr.io/mastodon/mastodon:v4.7.3 1.4GB
ghcr.io/mastodon/mastodon-streaming:v4.7.3 463MB
4.5 启动数据库与缓存
先拉起 db 与 redis(web/sidekiq/streaming 依赖二者健康):
cd /opt/mastodon && docker compose up -d db redis
实测回显:
Network mastodon-4.7.3-ext Creating
Network mastodon-4.7.3-int Creating
Network mastodon-4.7.3-ext Created
Network mastodon-4.7.3-int Created
Container mastodon-4.7.3-db Creating
Container mastodon-4.7.3-redis Creating
Container mastodon-4.7.3-db Created
Container mastodon-4.7.3-redis Created
Container mastodon-4.7.3-db Starting
Container mastodon-4.7.3-redis Starting
Container mastodon-4.7.3-db Started
Container mastodon-4.7.3-redis Started
等待健康检查就绪后确认状态:
docker compose ps
实测回显:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
mastodon-4.7.3-db postgres:14-alpine "docker-entrypoint.s…" db 6 hours ago Up 6 hours (healthy) 5432/tcp
mastodon-4.7.3-redis redis:7-alpine "docker-entrypoint.s…" redis 6 hours ago Up 6 hours (healthy) 6379/tcp
4.6 数据库创建与迁移
创建数据库:
docker compose run --rm web bin/rails db:create
实测回显(末行):
Created database 'mastodon'
执行迁移(建出全部表结构与索引,首次部署必须执行):
docker compose run --rm web bin/rails db:migrate
迁移完成后用 db:migrate:status 复核——全部迁移应为 up 状态:
docker compose run --rm web bin/rails db:migrate:status | tail -6
实测回显:
up 20260803140751 Add back languages index to account summaries
up 20260803172525 Add target account index to follow requests
up 20260804081821 Convert materialized views to tables
up 20260805130216 Fix generated annual reports foreign key
up 20260812154114 Remove index keypairs on account
💡 说明:Mastodon 4.7.3 共携带 500+ 条迁移记录(
db:migrate:status全量输出约 500 行,上为末尾节选),迁移在首次部署约需 1~3 分钟,此后升级版本时增量执行。
4.7 创建管理员账号
用 tootctl 创建本地管理员(--confirmed 跳过邮箱确认、--role Owner 绑定最高权限角色):
docker compose run --rm web bin/tootctl accounts create admin --email admin@mastodon.local --confirmed --role Owner
实测回显(随机密码每次不同,请记录):
OK
New password: 2f6027367aa3086a0a3d06a7559ae851
⚠️ 排障实录:
--role的值必须与内置角色名完全一致(内置三个:Owner / Admin / Moderator)。实测传入不存在或大小写不符的角色名时直接失败:Cannot find user role with that name
把密码固定为已知演示口令(随机密码不便留存,改为 q1w2e3r4):
docker compose run --rm web bin/rails runner "u=User.find_by(email:'admin@mastodon.local'); u.password='q1w2e3r4'; u.save!; puts 'saved'"
实测回显:
saved
校验密码已生效:
docker compose run --rm web bin/rails runner "puts User.find_by(email:'admin@mastodon.local').valid_password?('q1w2e3r4')"
实测回显:
true
4.8 启动三个应用服务
cd /opt/mastodon && docker compose up -d
web(Puma)启动日志实测回显(docker compose logs web | head):
[7] Puma starting in cluster mode...
[7] * Puma version: 8.0.2 ("Into the Arena")
[7] * Ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [x86_64-linux]
[7] * Min threads: 5
[7] * Max threads: 5
[7] * Environment: production
[7] * Master PID: 7
[7] * Workers: 1
[7] * Restarts: (✔) hot (✖) phased (✖) refork
[7] * Preloading application
[7] * Listening on http://0.0.0.0:3000
[7] Use Ctrl-C to stop
[7] ! WARNING: Detected running cluster mode with 1 worker.
[7] ! Running Puma in cluster mode with a single worker is often a misconfiguration.
[7] ! Consider running Puma in single-mode (workers = 0) in order to reduce memory overhead.
[7] ! Set the `silence_single_worker_warning` option to silence this warning message.
[7] - Worker 0 (PID: 14) booted in 0.0s, phase: 0
💡 说明:日志里的 WARNING 是 Puma 对「cluster 模式却只配 1 个 worker」的提醒——
WEB_CONCURRENCY=1是本单机实测的刻意选择(内存换进程数),生产多核机器可调高;若维持 1 worker 可在config/puma.rb加silence_single_worker_warning消除告警。线程数 5 即MAX_THREADS=5的生效结果。
streaming 启动日志实测回显(JSON 行格式,docker compose logs streaming | head -1):
{"level":"info","time":1793059604723,"pid":1,"hostname":"7c401c36220e","name":"streaming","msg":"Streaming API now listening on 0.0.0.0:4000"}
4.9 部署验证
五服务健康状态:
docker compose ps
实测回显:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
mastodon-4.7.3-db postgres:14-alpine "docker-entrypoint.s…" db 6 hours ago Up 6 hours (healthy) 5432/tcp
mastodon-4.7.3-redis redis:7-alpine "docker-entrypoint.s…" redis 6 hours ago Up 6 hours (healthy) 6379/tcp
mastodon-4.7.3-sidekiq ghcr.io/mastodon/mastodon:v4.7.3 "/usr/bin/tini -- bu…" sidekiq 20 minutes ago Up 20 minutes (healthy) 3000/tcp
mastodon-4.7.3-streaming ghcr.io/mastodon/mastodon-streaming:v4.7.3 "/usr/bin/tini -- bu…" streaming 27 minutes ago Up 27 minutes (healthy) 127.0.0.1:8133->4000/tcp
mastodon-4.7.3-web ghcr.io/mastodon/mastodon:v4.7.3 "/usr/bin/tini -- bu…" web 20 minutes ago Up 20 minutes (healthy) 127.0.0.1:8135->3000/tcp
健康检查端点(两个应用进程各自内置 /health):
curl -s http://127.0.0.1:8135/health
curl -s http://127.0.0.1:8133/api/v1/streaming/health
实测回显(各一行):
OK
OK
实例元数据 API(需按 12.1 说明携带 Host 头):
curl -s -H 'Host: mastodon.local' -H 'X-Forwarded-Proto: https' http://127.0.0.1:8135/api/v1/instance
实测回显(节选):
{"uri":"mastodon.local","title":"Mastodon","short_description":"","description":"","email":"","version":"4.7.3","urls":{"streaming_api":"wss://mastodon.local"},"stats":{"user_count":1,"status_count":5,"domain_count":0},"thumbnail":"https://mastodon.local/packs/assets/preview-vSUsFXid.png","languages
五服务全部 healthy、双 /health 返回 OK、instance API 返回 version: 4.7.3——部署完成。
4.10 注册、发嘟与时间线实测
本节从「实例管理员」视角完整走一遍账号 → 令牌 → 发帖 → 时间线 → 媒体 → 实时推送链路,全部经 REST API 实测(生产客户端同走此 API)。
第一步:注册 OAuth 应用(API 调用的前提是有一个应用凭据):
curl -s -H 'Host: mastodon.local' -H 'X-Forwarded-Proto: https' \
-X POST http://127.0.0.1:8135/api/v1/apps \
-d "client_name=tootctl-test&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=read write follow push"
实测回显:
{"id":"2","name":"tootctl-test","website":null,"scopes":["read","write","follow","push"],"redirect_uris":["urn:ietf:wg:oauth:2.0:oob"],"vapid_key":"BDXou3TM0IGj36bVmREh3qJk9ZFIJpAU65CXPeMZxgxOXhTPf_QvWphXeAVpNXwHefPxjQYmQA26dTsLM8PTrdA=","redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"HQsZP9EMCswMgp5Hk4mAF_kpjvOt5LBqxjLNuTqrCV0","client_secret":"qQN9BJzz-tec8_cPtoVYzRnCyWAEhc1zhOlW-xT1Cr0","client_secret_expires_at":0}
⚠️ 排障实录:不要尝试密码授权(
grant_type=password)直接换 token——4.7.3 默认关闭该授权流,实测返回:{"error":"unsupported_grant_type","error_description":"The authorization grant type is not supported by the authorization server."}官方 Doorkeeper 配置
config/initializers/doorkeeper.rb中写明grant_flows %w(authorization_code client_credentials),即仅保留授权码与客户端凭据两种流。自托管管理员为自己签发测试令牌,最直接的方式是用 Rails 控制台为应用创建令牌(下一步)。
第二步:为管理员签发访问令牌(client_id/client_secret 即第一步回显值):
docker compose run --rm web bin/rails runner "app=Doorkeeper::Application.find_by(name:'tootctl-test'); u=User.find_by(email:'admin@mastodon.local'); t=Doorkeeper::AccessToken.create_for(application: app, resource_owner: u, scopes: 'read write'); puts t.token"
实测回显(令牌示例):
DN7osteeiaOPz8IIpB32yNVpc-n7Pez8fqN5CW0Rw_A
第三步:核验令牌身份:
curl -s -H 'Host: mastodon.local' -H 'X-Forwarded-Proto: https' \
-H "Authorization: Bearer DN7osteeiaOPz8IIpB32yNVpc-n7Pez8fqN5CW0Rw_A" \
http://127.0.0.1:8135/api/v1/accounts/verify_credentials
实测回显(节选):
admin Owner statuses: 5
第四步:发嘟(创建帖子):
curl -s -H 'Host: mastodon.local' -H 'X-Forwarded-Proto: https' \
-X POST http://127.0.0.1:8135/api/v1/statuses \
-H "Authorization: Bearer DN7osteeiaOPz8IIpB32yNVpc-n7Pez8fqN5CW0Rw_A" \
--data-urlencode "status=Mastodon 4.7.3 部署实测:单机全栈拉起,web/streaming/sidekiq 三进程全部健康。"
实测回显(节选):
{"id":"117509977540512195","created_at":"2026-10-27T00:12:40.043Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"https://mastodon.local/ap/users/117509968321287485/statuses/117509977540512195","url":"https://mastodon.local/@admin/117509977540512195",...,"content":"<p>Mastodon 4.7.3 部署实测:单机全栈拉起,web/streaming/sidekiq 三进程全部健康。</p>",...,"account":{"id":"117509968321287485","username":"admin","acct":"admin",...},"media_attachments":[],...}
第五步:媒体上传 + 带图发嘟(先传媒体拿 id,再随帖发布):
curl -s -H 'Host: mastodon.local' -H 'X-Forwarded-Proto: https' \
-X POST http://127.0.0.1:8135/api/v1/media \
-H "Authorization: Bearer DN7osteeiaOPz8IIpB32yNVpc-n7Pez8fqN5CW0Rw_A" \
-F "file=@/opt/sample.png"
实测回显(节选):
{"id":"117509984859987084","type":"image","url":"https://mastodon.local/system/media_attachments/files/117/509/984/859/987/084/original/35546cc09d618a6d.png","preview_url":"https://mastodon.local/system/media_attachments/files/117/509/984/859/987/084/small/35546cc09d618a6d.png",...,"meta":{"original":{"width":800,"height":450,"size":"800x450","aspect":1.7777777777777777},"small":{"width":640,"height":360,"size":"640x360","aspect":1.7777777777777777}},"description":null,"blurhash":"U;J*v6j]IYj]ogfQazfQ0BfkxrayWGj[j[az"}
媒体文件落在本机 ./public/system/media_attachments/files/...(挂载与持久化见第九章)。随后随帖发布(响应结构与第四步相同,media_attachments 数组含上图的完整信息,statuses_count 从 2 变为 3)。
第六步:时间线读取(首页时间线需令牌;本地公开时间线匿名可读):
curl -s -H 'Host: mastodon.local' -H 'X-Forwarded-Proto: https' \
-H "Authorization: Bearer DN7osteeiaOPz8IIpB32yNVpc-n7Pez8fqN5CW0Rw_A" \
http://127.0.0.1:8135/api/v1/timelines/home
实测回显(节选,倒序时间线):
117509993031574482 2026-10-27T00:16:36.404Z <p>流式 API 实测:本帖应实时推送到 user 时间线流。</p>
117509991539012238 2026-10-27T00:16:13.630Z <p>流式 API 实测:本帖应实时推送到 user 时间线流。</p>
117509985713779868 2026-10-27T00:14:44.766Z <p>实测媒体上传链路:sample.png 上传成功后随帖发布。</p>
117509977950350169 2026-10-27T00:12:46.284Z <p>实测媒体上传:sample.png 经 /api/v1/media 上传后
117509977540512195 2026-10-27T00:12:40.043Z <p>Mastodon 4.7.3 部署实测:单机全栈拉起,web/stream
第七步:Streaming 实时推送(对 user 流发起 SSE 长连接,随后发布的新帖应实时到达):
curl -s -N -H 'Host: mastodon.local' \
-H "Authorization: Bearer DN7osteeiaOPz8IIpB32yNVpc-n7Pez8fqN5CW0Rw_A" \
http://127.0.0.1:8133/api/v1/streaming/user
实测回显(响应头与首帧,节选):
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/event-stream
Cache-Control: private, no-store
Transfer-Encoding: chunked
:)
event: update
data: {"id":"117509993031574482","created_at":"2026-10-27T00:16:36.404Z",...,"content":"<p>流式 API 实测:本帖应实时推送到 user 时间线流。</p>",...}
连接建立后先收到心跳帧 :),随后 event: update 帧携带新帖全文——发布、订阅、推送全链路实测打通。
五、web 服务排障实录(Created 未启动)
本章沉淀本次部署中 web 服务「处于 Created 状态不启动」的完整排障过程。这是 Compose 多服务栈最常见的启动类故障之一,定位链与解决方法可直接套用。
现象:执行 docker compose up -d 后 db/redis 正常运行,web 服务的容器停留在 Created 状态,端口未监听、服务不可访问:
docker ps -a --format '{{.Names}}\t{{.Status}}' | grep web
实测回显:
mastodon-4.7.3-web Created
定位链(三步锁定原因):
-
看日志——Created 状态的容器从未启动过,日志为空(无任何业务输出):
docker logs mastodon-4.7.3-web实测回显(空输出,说明进程从未运行):
-
看启动元数据——
StartedAt为零值,进一步确认从未启动成功:docker inspect mastodon-4.7.3-web --format 'created={{.Created}} started={{.State.StartedAt}} exit={{.State.ExitCode}}'实测回显:
created=2026-10-27T00:13:30.952080394Z started=0001-01-01T00:00:00Z exit=0 -
查端口占用——web 服务声明了宿主端口绑定(
127.0.0.1:8135:3000),Created 未启动的最常见成因就是宿主端口在启动时被其他进程占用:ss -ltn | grep 8135实测回显(8135 已被另一进程监听,占用成立):
LISTEN 0 511 127.0.0.1:8135 0.0.0.0:*
受控复现(用占位进程完整还原该故障的产生与恢复路径,以下命令与回显为实测原文):
第一步,用一个临时进程占用 8135 端口,模拟端口被抢:
docker run -d --name m473-port-holder -p 127.0.0.1:8135:9999 python:3.12-alpine python3 -c "import time; time.sleep(900)"
第二步,创建 web 服务容器(此时不启动,容器即处于 Created):
docker compose create web
实测回显:
Container mastodon-4.7.3-db Running
Container mastodon-4.7.3-redis Running
Container mastodon-4.7.3-web Creating
Container mastodon-4.7.3-web Created
第三步,尝试启动,因端口被占失败,容器停留在 Created:
docker start mastodon-4.7.3-web
实测回显(完整错误):
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint mastodon-4.7.3-web (9d011dfbeb8bc4ba1aab933e0dd1039c7bc4c9cc1d9e7eddd891eb7d495cb7ac): Bind for 127.0.0.1:8135 failed: port is already allocated
failed to start containers: mastodon-4.7.3-web
docker ps -a --format '{{.Names}}\t{{.Status}}' | grep 'mastodon-4.7.3-web'
实测回显(失败后状态仍为 Created):
mastodon-4.7.3-web Created
解决:释放被占端口(停掉占用方),重新启动:
docker rm -f m473-port-holder
docker start mastodon-4.7.3-web
实测回显:
mastodon-4.7.3-web
连带踩坑(重要):经历过端口绑定失败的容器,即使随后 docker start 成功,端口映射仍可能未真正生效——本实测中该容器启动后 docker ps 的 PORTS 列只显示 3000/tcp 而无 127.0.0.1:8135->3000/tcp,宿主机访问连接被拒(curl 报 Connection refused)。此时需强制重建该服务:
docker compose up -d --force-recreate web
实测回显:
Container mastodon-4.7.3-db Running
Container mastodon-4.7.3-redis Running
Container mastodon-4.7.3-web Recreate
Container mastodon-4.7.3-web Recreated
Container mastodon-4.7.3-web Starting
Container mastodon-4.7.3-web Started
重建后验证,端口映射与健康检查恢复正常:
docker ps --format '{{.Names}}\t{{.Status}}\t{{.Ports}}' | grep 'mastodon-4.7.3-web'
curl -s http://127.0.0.1:8135/health
实测回显:
mastodon-4.7.3-web Up 25 seconds (health: starting) 127.0.0.1:8135->3000/tcp
OK
💡 说明:排查「服务起不来」类问题时,牢记状态机——Created(从未启动)/ Exited(启动后退出)/ Up(运行中)三种状态对应完全不同的定位路径;Created 几乎总是「启动前置条件不满足」(端口、网络、挂载路径),从
docker start的完整报错文本入手最快。
六、数据持久化说明
三个数据目录全部以 bind mount 落在部署目录下,服务重启/重建后数据不丢:
| 编排内挂载 | 宿主目录 | 内容 | 备份要点 |
|---|---|---|---|
./postgres14:/var/lib/postgresql/data | /opt/mastodon/postgres14 | 全部结构化数据(账号/帖子/关系/设置) | 用 pg_dump 逻辑备份,勿直接拷目录热备 |
./redis:/data | /opt/mastodon/redis | 队列、缓存、订阅通道(可重建) | 丢失可自动重建,无需备份 |
./public/system:/opt/mastodon/public/system | /opt/mastodon/public/system | 本地媒体(图片/头像/表情原始文件) | 与数据库一致性备份(媒体路径记录在库中) |
数据库逻辑备份命令(实测可用):
docker compose exec -T db pg_dump -U postgres mastodon > /opt/mastodon/backup/mastodon-$(date +%F).sql
⚠️ 注意:
postgres14目录属 PostgreSQL 14 数据格式,升级大版本(如 14→15/16)不能直接复用该目录,需走pg_dumpall导出导入或pg_upgrade;这也是官方 compose 固定postgres:14-alpine、不轻易动大版本的原因。
七、环境变量表
.env.production 是全部服务的唯一配置注入点(三个应用服务共用 env_file)。本实测启用的变量全表:
| 变量 | 实测值 | 作用 |
|---|---|---|
LOCAL_DOMAIN | mastodon.local | 实例联邦身份,@user@domain 的域名部分,开放联邦后不可安全更改 |
RAILS_ENV | production | Rails 运行环境,生产固定 production |
RAILS_LOG_LEVEL | warn | Rails 日志级别,排障时可临时改 debug |
REDIS_HOST / REDIS_PORT | redis / 6379 | Redis 地址(compose 网络内服务名直连) |
DB_HOST / DB_PORT | db / 5432 | PostgreSQL 地址与端口 |
DB_USER / DB_NAME / DB_PASS | postgres / mastodon / 空 | 数据库凭据,空密码配套 trust 认证(见 4.3 说明) |
ES_ENABLED | false | Elasticsearch 全文搜索开关,未启用时为 false |
SECRET_KEY_BASE | (4.2 生成) | Rails 会话等加密基钥,泄露=全部会话可伪造 |
OTP_SECRET | (4.2 生成) | 两步验证(OTP)加密密钥 |
VAPID_PRIVATE_KEY / VAPID_PUBLIC_KEY | (4.2 生成) | Web Push 推送签名密钥对 |
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY 等 3 项 | (4.2 生成) | 数据库敏感字段加密三件套,投入运行后不可改 |
SMTP_SERVER / SMTP_PORT | localhost / 25 | 出站邮件服务器(本次实测未接真实服务,影响见 12.3) |
SMTP_AUTH_METHOD / SMTP_OPENSSL_VERIFY_MODE / SMTP_ENABLE_STARTTLS_AUTO | none / none / false | SMTP 认证与 TLS 策略(本机投递场景关闭) |
SMTP_FROM_ADDRESS | Mastodon <notifications@mastodon.local> | 发件人地址与显示名 |
WEB_CONCURRENCY | 1 | Puma worker 进程数(单机收紧) |
MAX_THREADS | 5 | Puma 每 worker 线程数(实测改 10 后 Puma 日志同步变为 10) |
STREAMING_CLUSTER_NUM | 1 | streaming 进程集群数 |
S3_ENABLED | false | 媒体对象存储开关,false 时落本地 public/system |
EMAIL_DOMAIN_ALLOWLIST | mastodon.local | 邮箱域白名单,命中则跳过 MX 解析校验 |
配置项的逐项修改-验证闭环(含 MAX_THREADS、EMAIL_DOMAIN_ALLOWLIST、SMTP、Elasticsearch 四组实测)在《Mastodon 4.7.3 配置参数详解》中专门展开,本文不重复。
八、配置参数说明
编排文件中影响生产行为的关键参数解读:
restart: always(五个服务全部启用):进程崩溃或宿主机重启后自动拉起,是长期运行服务的基础保障;运维中若需「故意停服」,必须用docker compose stop(stop 后 restart 策略不会拉起),不要用kill进程的方式。shm_size: 256mb(db 服务):PostgreSQL 并行查询与临时缓冲依赖共享内存,默认 64MB 对社交类负载偏小,官方编排固定 256MB。internal: true(internal_network):该网络无外网路由,db/redis 只在内部网络暴露;web/streaming/sidekiq 同时挂两个网络——业务流量走 external,数据库与缓存访问走 internal。这是官方编排的核心安全设计:数据库与缓存永远不直接暴露。depends_on:只保证启动顺序,不等待「健康」;db/redis 自带 healthcheck 后,应用服务崩溃重启(如数据库短暂不可达引发的)会由restart: always兜底重拉。healthcheck:web 与 streaming 用各自的/health端点判活(grep -q 'OK'),sidekiq 用进程特征(ps aux | grep '[s]idekiq\ 8',匹配 8 线程的 sidekiq 进程),db 用pg_isready,redis 用redis-cli ping。docker compose ps的(healthy)即来自这些探针。command::web 与 sidekiq 共用同一 Rails 镜像,仅以启动命令区分进程角色(puma -C config/puma.rb/sidekiq),升级镜像时两服务同步获得新版本。
九、配置文件挂载
.env.production:以env_file: .env.production注入 web/streaming/sidekiq 三个服务。修改后必须重建服务才生效(docker compose up -d会因环境变化自动重建对应服务;实测验证见配置篇 MAX_THREADS 一节)。./public/system:web 与 sidekiq 都挂载该目录——web 负责写入与读取媒体,sidekiq 负责后台转码缩略图,两者必须指向同一宿主目录,否则会出现「上传成功但图片 404」。./postgres14与./redis:数据目录挂载,见第六章。- 官方编排中的 Elasticsearch 注释段(
docker.elastic.co/elasticsearch/elasticsearch:8.19.15)是启用全文搜索的现成模板,取消注释、在.env.production打开ES_ENABLED=true并执行tootctl search deploy建索引即可启用(ES 未启用时执行该命令的实测报错见 12.4)。
十、等价 docker run
不使用 Compose 时,等价的逐服务启动命令如下(先创建两个网络;每条命令可独立执行):
docker network create --driver bridge --subnet 10.89.63.0/24 mastodon-4.7.3-ext
docker network create --driver bridge --internal --subnet 10.89.64.0/24 mastodon-4.7.3-int
docker run -d --name mastodon-4.7.3-db --restart always --shm-size=256mb \
--network mastodon-4.7.3-int \
-e POSTGRES_HOST_AUTH_METHOD=trust \
-v /opt/mastodon/postgres14:/var/lib/postgresql/data \
postgres:14-alpine
docker run -d --name mastodon-4.7.3-redis --restart always \
--network mastodon-4.7.3-int \
-v /opt/mastodon/redis:/data \
redis:7-alpine
docker run -d --name mastodon-4.7.3-web --restart always \
--env-file /opt/mastodon/.env.production \
--network mastodon-4.7.3-ext \
-p 127.0.0.1:8135:3000 \
-v /opt/mastodon/public/system:/opt/mastodon/public/system \
ghcr.io/mastodon/mastodon:v4.7.3 \
bundle exec puma -C config/puma.rb
docker run -d --name mastodon-4.7.3-streaming --restart always \
--env-file /opt/mastodon/.env.production \
--network mastodon-4.7.3-ext \
-p 127.0.0.1:8133:4000 \
ghcr.io/mastodon/mastodon-streaming:v4.7.3 \
node ./streaming/index.js
docker run -d --name mastodon-4.7.3-sidekiq --restart always \
--env-file /opt/mastodon/.env.production \
--network mastodon-4.7.3-ext \
-v /opt/mastodon/public/system:/opt/mastodon/public/system \
ghcr.io/mastodon/mastodon:v4.7.3 \
bundle exec sidekiq
⚠️ 注意:web/streaming/sidekiq 还需同时接入 internal 网络才能访问 db/redis(
docker network connect mastodon-4.7.3-int <服务名>补挂即可);Compose 场景下这些由编排文件自动完成。逐条手拉仅用于理解结构与应急恢复,日常管理统一走 Compose。
十一、常用运维命令
以下命令均在部署目录 /opt/mastodon 下执行:
docker compose ps # 五服务状态与健康一览(回显见 4.9)
docker compose logs -f web # 跟随 web 日志(streaming/sidekiq/db/redis 同理)
docker compose restart web # 重启单服务(改编排后用 up -d 重建而非 restart)
docker compose up -d # 配置/镜像变化时重建对应服务(env_file 改动会触发重建)
docker compose pull && docker compose up -d # 升级流程:拉新镜像 + 滚动重建
docker compose run --rm web bin/tootctl version # tootctl 任意命令的统一入口(回显:4.7.3)
docker compose exec -T db pg_isready -U postgres # 数据库就绪探针(回显:/var/run/postgresql:5432 - accepting connections)
docker compose down # 停止并移除五服务(数据目录保留,up -d 可恢复)
docker compose run --rm web bin/tootctl <子命令> 是全部管理命令的统一入口,tootctl 的完整命令矩阵(accounts/cache/media/emoji/domains/settings 等 18 组)与逐条实测回显见《Mastodon 4.7.3 常用 CLI 命令详解》。
十二、常见问题
Q1:web 服务停留在 Created 状态不启动?
端口被占用是头号成因,完整定位链与受控复现见第五章。次要成因是挂载路径不存在或权限不足——docker start <容器> 的报错文本会直接说明原因。
Q2:服务 Up 且 healthy,但宿主机访问端口连接被拒(Connection refused)?
检查 docker ps 的 PORTS 列是否包含 127.0.0.1:8135->3000/tcp。若只显示 3000/tcp 说明端口映射未生效(多见于经历过启动失败的容器),执行 docker compose up -d --force-recreate web 重建即恢复(实测回显见第五章连带踩坑)。
Q3:直连 web 端口返回 403 或 301?
Mastodon 校验请求 Host 必须匹配 LOCAL_DOMAIN,且生产模式默认按 HTTPS 对外。本实测三种行为的对照(同一 URL,仅请求头不同):
不带 Host 头(Host 为 127.0.0.1:8135) → 403(域名不被信任)
Host: mastodon.local(无 X-Forwarded-Proto) → 301(重定向到 https)
Host: mastodon.local + X-Forwarded-Proto: https → 200(正常返回)
生产环境在反向代理上把 Host 原样传给后端、并设置 X-Forwarded-Proto: https 即可;/health 端点不受此限制(任意 Host 均返回 200 OK)。
Q4:发帖/注册后没有邮件发出,日志报 Connection refused? 未接通 SMTP 的典型表现。实测用管理命令直接触发投递:
docker compose run --rm web bin/rails runner "begin; UserMailer.confirmation_instructions(User.find_by(email:'admin@mastodon.local'), 'testtoken', {}).deliver_now; rescue StandardError => e; puts \"#{e.class}: #{e.message}\"; end"
实测回显:
Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25
即 SMTP_SERVER=localhost:25 不可达。生产环境把 SMTP_* 五项改为真实邮件服务商参数即可;纯内网测试实例可忽略(管理员账号可用 tootctl 直接创建并 --confirmed,不依赖邮件)。
Q5:tootctl search deploy 报 Connection refused(localhost:9200)?
ES_ENABLED=false 时全文搜索未启用,但该命令仍会尝试连接 ES(默认 localhost:9200)。实测报错首行:
Connection refused - Connection refused - connect(2) for "localhost" port 9200 (localhost:9200) (Elastic::Transport::Transport::Error)
未启用 ES 的实例忽略该命令即可;启用路径见第九章。
Q6:Puma 日志出现 WARNING single worker?
WEB_CONCURRENCY=1 触发的提醒,见 4.8 说明:单机小内存是合理选择;多核生产机器调大 WEB_CONCURRENCY,或按提示关闭该告警。
十三、参考链接
- 官方管理员文档(部署与配置):
https://docs.joinmastodon.org/admin/ - 官方 docker-compose.yml(v4.7.3 tag):
https://github.com/mastodon/mastodon/blob/v4.7.3/docker-compose.yml - 官方 .env.production.sample:
https://github.com/mastodon/mastodon/blob/v4.7.3/.env.production.sample - 官方扩容文档(三进程扩展矩阵):
https://docs.joinmastodon.org/admin/scaling/ - 本专栏认知篇:《Mastodon 4.7.3 介绍与选型指南》(https://www.quzuo.net/document/gzKZrJqqJRK5)