Mastodon 4.7.3 Docker Compose 部署指南

用官方镜像与官方 compose 编排(web/streaming/sidekiq/db/redis 五服务)从零部署 Mastodon 联邦社交实例:密钥生成、数据库迁移、tootctl 建管理员、注册发嘟时间线实测,并完整沉淀 web 服务 Created 未启动的排障实录。

MastodonMastodonFediverse2026/10/27

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,streaming 127.0.0.1:8133 → 4000(均绑回环,前置反向代理后对外)
  • 五服务常驻内存实测(启动并完成发帖/媒体上传后采样):
服务内存占用说明
web489.4 MiBPuma,WEB_CONCURRENCY=1 / MAX_THREADS=5
sidekiq354.7 MiB后台任务
streaming66.1 MiB实时推送,STREAMING_CLUSTER_NUM=1
db48.6 MiBPostgreSQL 14,空库+少量测试数据
redis4.2 MiB队列+缓存
合计约 963 MiB2GB 内存虚拟机即可起步,建议 ≥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/mastodonv4.7.31.4GBweb(Puma)与 sidekiq 共用,命令行参数区分进程
ghcr.io/mastodon/mastodon-streamingv4.7.3463MBstreaming(Node.js 实时推送)
postgres:14-alpine14.24414MB主存储,官方 compose 固定 14 系
redis:7-alpine7.4.1157.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 即可;代理为当前实测可用渠道。

三、前置条件

  1. Docker 与 Compose 插件:宿主机安装 Docker 24+ 与 docker compose v2 插件。验证:

    docker compose version
    

    实测回显:

    Docker Compose version v5.6.1
    
  2. 端口规划:web 8135、streaming 8133 绑定在 127.0.0.1(建议前置反向代理对外)。确认空闲:

    ss -ltn | grep -E ':8135|:8133'
    

    实测回显(无输出 = 端口空闲):

  3. 资源:五服务常驻合计约 963MiB 内存(见第一章表),加上系统与其他服务,建议 ≥4GB 内存、预留 10GB 磁盘(镜像 2.3GB + 数据目录)。

  4. 域名与身份规划:LOCAL_DOMAIN 是实例的联邦身份(@用户名@域名 的后半段),一旦开放联邦后不可安全更改,必须一次定型。本文用 mastodon.local 演示。

  5. SMTP 规划:生产实例的注册确认、通知邮件依赖 SMTP;本文实测环境未接入外部邮件服务,相关影响与验证见第十二章 FAQ 12.3。

  6. 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

定位链(三步锁定原因):

  1. 看日志——Created 状态的容器从未启动过,日志为空(无任何业务输出):

    docker logs mastodon-4.7.3-web
    

    实测回显(空输出,说明进程从未运行):

  2. 看启动元数据——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
    
  3. 查端口占用——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_DOMAINmastodon.local实例联邦身份,@user@domain 的域名部分,开放联邦后不可安全更改
RAILS_ENVproductionRails 运行环境,生产固定 production
RAILS_LOG_LEVELwarnRails 日志级别,排障时可临时改 debug
REDIS_HOST / REDIS_PORTredis / 6379Redis 地址(compose 网络内服务名直连)
DB_HOST / DB_PORTdb / 5432PostgreSQL 地址与端口
DB_USER / DB_NAME / DB_PASSpostgres / mastodon / 空数据库凭据,空密码配套 trust 认证(见 4.3 说明)
ES_ENABLEDfalseElasticsearch 全文搜索开关,未启用时为 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_PORTlocalhost / 25出站邮件服务器(本次实测未接真实服务,影响见 12.3)
SMTP_AUTH_METHOD / SMTP_OPENSSL_VERIFY_MODE / SMTP_ENABLE_STARTTLS_AUTOnone / none / falseSMTP 认证与 TLS 策略(本机投递场景关闭)
SMTP_FROM_ADDRESSMastodon <notifications@mastodon.local>发件人地址与显示名
WEB_CONCURRENCY1Puma worker 进程数(单机收紧)
MAX_THREADS5Puma 每 worker 线程数(实测改 10 后 Puma 日志同步变为 10)
STREAMING_CLUSTER_NUM1streaming 进程集群数
S3_ENABLEDfalse媒体对象存储开关,false 时落本地 public/system
EMAIL_DOMAIN_ALLOWLISTmastodon.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)