docker compose 运行 mastodon

在 debian 服务器上尝试部署了下 mastodon,使用 docker compose 的方式。

前往 github 获取代码,地址:https://github.com/mastodon/mastodon

docker compose 配置文件

项目根目录已经包含了一个 docker-compose.ymlv4.5.2 版本内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# This file is designed for production server deployment, not local development work
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/docs/DEVELOPMENT.md#docker

services:
db:
restart: always
image: postgres:14-alpine
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
networks:
- internal_network
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
volumes:
- ./redis:/data

# es:
# restart: always
# image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4
# environment:
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true"
# - "xpack.license.self_generated.type=basic"
# - "xpack.security.enabled=false"
# - "xpack.watcher.enabled=false"
# - "xpack.graph.enabled=false"
# - "xpack.ml.enabled=false"
# - "bootstrap.memory_lock=true"
# - "cluster.name=es-mastodon"
# - "discovery.type=single-node"
# - "thread_pool.write.queue_size=1000"
# networks:
# - external_network
# - internal_network
# healthcheck:
# test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
# volumes:
# - ./elasticsearch:/usr/share/elasticsearch/data
# ulimits:
# memlock:
# soft: -1
# hard: -1
# nofile:
# soft: 65536
# hard: 65536
# ports:
# - '127.0.0.1:9200:9200'

web:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/mastodon/mastodon:v4.5.2
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
networks:
- external_network
- internal_network
healthcheck:
# prettier-ignore
test: ['CMD-SHELL',"curl -s --noproxy localhost localhost:3000/health | grep -q 'OK' || exit 1"]
ports:
- '127.0.0.1:3000:3000'
depends_on:
- db
- redis
# - es
volumes:
- ./public/system:/mastodon/public/system

streaming:
# You can uncomment the following lines if you want to not use the prebuilt image, for example if you have local code changes
# build:
# dockerfile: ./streaming/Dockerfile
# context: .
image: ghcr.io/mastodon/mastodon-streaming:v4.5.2
restart: always
env_file: .env.production
command: node ./streaming/index.js
networks:
- external_network
- internal_network
healthcheck:
# prettier-ignore
test: ['CMD-SHELL', "curl -s --noproxy localhost localhost:4000/api/v1/streaming/health | grep -q 'OK' || exit 1"]
ports:
- '127.0.0.1:4000:4000'
depends_on:
- db
- redis

sidekiq:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/mastodon/mastodon:v4.5.2
restart: always
env_file: .env.production
command: bundle exec sidekiq
depends_on:
- db
- redis
networks:
- external_network
- internal_network
volumes:
- ./public/system:/mastodon/public/system
healthcheck:
test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 8' || false"]

## Uncomment to enable federation with tor instances along with adding the following ENV variables
## http_hidden_proxy=http://privoxy:8118
## ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
# tor:
# image: sirboops/tor
# networks:
# - external_network
# - internal_network
#
# privoxy:
# image: sirboops/privoxy
# volumes:
# - ./priv-config:/opt/config
# networks:
# - external_network
# - internal_network

networks:
external_network:
internal_network:
internal: true

包含了以下几个部分:

  • db:PostgreSQL 数据库,版本是14。
  • redis:redis 内存型数据存储。
  • es: Elasticsearch 全文搜索,版本 7.17.4 比较旧了,默认未启用,注释掉了。
  • web: mastodon 主程序。
  • streaming
  • sidekiq
  • tor:注释掉了,无视之。
  • privoxy:注释掉了,无视之。

另外 volume 全部是目录的方式挂载,还有俩个网络: external_networkinternal_network

直接运行 docker compose up 是跑不起来的,还缺少 .env.production 配置文件,以及一些初始化配置。

直接复制 .env.production.sample 里的内容也不行,我们需要为 docker compose 的运行进行专门配置。

应用初始化

参考:https://docs.joinmastodon.org/admin/install/,我们需要执行初始化命令:

1
RAILS_ENV=production bin/rails mastodon:setup

对应的 docker compose 需要运行:

1
2
3
4
5
# 新建一个空白配置文件
touch .env.production

# 运行容器命令,做一些配置相关工作
docker compose run -e RAILS_ENV=production --rm -- web bundle exec bin/rails mastodon:setup

交互过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
➜  mastodon git:(main) docker compose run -e RAILS_ENV=production --rm -- web bundle exec bin/rails mastodon:setup 
[+] Creating 4/4
✔ Network mastodon_internal_network Created 0.0s
✔ Network mastodon_external_network Created 0.0s
✔ Container mastodon-redis-1 Created 0.1s
✔ Container mastodon-db-1 Created 0.1s
[+] Running 2/2
✔ Container mastodon-redis-1 Started 0.2s
✔ Container mastodon-db-1 Started 0.2s
Your instance is identified by its domain name. Changing it afterward will break things.
Domain name: example.com

Single user mode disables registrations and redirects the landing page to your public profile.
Do you want to enable single user mode? No

Are you using Docker to run Mastodon? Yes

PostgreSQL host: db
PostgreSQL port: 5432
Name of PostgreSQL database: postgres
Name of PostgreSQL user: postgres
Password of PostgreSQL user:
Database configuration works! 🎆

Redis host: redis
Redis port: 6379
Redis password:
Redis configuration works! 🎆

Do you want to store uploaded files on the cloud? No

Do you want to send e-mails from localhost? No

SMTP server: smtp.mailgun.org
SMTP port: 587
SMTP username: a
SMTP password:
SMTP authentication: plain
SMTP OpenSSL verify mode: none
Enable STARTTLS: auto
E-mail address to send e-mails "from": Mastodon <notifications@codegeass.cc>
Send a test e-mail with this configuration right now? no

Do you want Mastodon to periodically check for important updates and notify you? (Recommended) no

This configuration will be written to .env.production
Save configuration? Yes
Below is your configuration, save it to an .env.production file outside Docker:

生成的配置信息如下,需要手动复制到 .env.production 文件里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Generated with mastodon:setup on 2025-11-21 08:58:21 UTC

# Some variables in this file will be interpreted differently whether you are
# using docker-compose or not.

LOCAL_DOMAIN=example.com
SINGLE_USER_MODE=false
SECRET_KEY_BASE=560c129d7d8a730b49786790fc581de3a951311c83a6f39674069663660be956b27d17395c63b6f3b5797086f8090c7141034b14082fcf6313406c02a3ac086f
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=TynWLYp7WGbD7yUR3Om1YRRMgijQkANC
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=T8SmqUbK6XQ32FTr7xJXUMdl7V6c6eec
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=FdzfsRzyF5j6cJgvzRaJnMBHuOwfFHJL
VAPID_PRIVATE_KEY=MlmIYzlc0aXbcT5gLtW10ToqcQySEQWm_lL3bDW3L3I=
VAPID_PUBLIC_KEY=BMTDIscmyHiEaZVUXotXJvcOpWf1WB06Zdcspd5rAkRbZv5KGH8TvktH2WJWxHynmadlBi6RlLViYQMHEGm6DmY=
DB_HOST=db
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=[pgpass]
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587
SMTP_LOGIN=a
SMTP_PASSWORD=b
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_ENABLE_STARTTLS=auto
SMTP_FROM_ADDRESS=Mastodon <notifications@codegeass.cc>
UPDATE_CHECK_URL=

其中 LOCAL_DOMAIN 需要填网站域名,DB_PASS 设一个复杂点的密码,REDIS_PASSWORD 我直接回车跳过了,SMTP 相关的配置随便填的,后面需要再仔细配置。

然后是数据库初始化,管理账号初始化我是先跳过了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
It is also saved within this container so you can proceed with this wizard.

Now that configuration is saved, the database schema must be loaded.
If the database already exists, this will erase its contents.
Prepare the database now? Yes
Running `RAILS_ENV=production rails db:setup` ...


I, [2025-11-21T09:01:07.354171 #76] INFO -- : [dotenv] Loaded .env.production
Database 'postgres' already exists
Done!

All done! You can now power on the Mastodon server 🐘

Do you want to create an admin user straight away? no

再次运行 docker compose up ,就可以正常启动了。

到目前为止还不能在浏览器里打开网站,还需要托管一下前端文件,以及做一下接口的反向代理。

前端部分

我是使用了服务器上直接安装的 nginx 来处理 https 请求(而不是nginx 容器),已经有现成的 nginx 配置文件 dist/nginx.conf,需要稍微改动下。

在配置 nginx 之前需要准备下 ssl 证书,我是使用的 acme.sh 脚本生成的免费证书,具体过程不再赘述。

改动后的 nginx 配置文件 /etc/nginx/sites-enabled/mastodon.conf 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

upstream backend {
server 127.0.0.1:3000 fail_timeout=0;
}

upstream streaming {
# Instruct nginx to send connections to the server with the least number of connections
# to ensure load is distributed evenly.
least_conn;

server 127.0.0.1:4000 fail_timeout=0;
# Uncomment these lines for load-balancing multiple instances of streaming for scaling,
# this assumes your running the streaming server on ports 4000, 4001, and 4002:
# server 127.0.0.1:4001 fail_timeout=0;
# server 127.0.0.1:4002 fail_timeout=0;
}

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g;

server {
listen 80;
listen [::]:80;
server_name example.com;
root /mnt/none;
location /.well-known/acme-challenge/ { allow all; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;

ssl_protocols TLSv1.2 TLSv1.3;

# You can use https://ssl-config.mozilla.org/ to generate your cipher set.
# We recommend their "Intermediate" level.
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;

ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

# Uncomment these lines once you acquire a certificate:
ssl_certificate /path/to/cer/file;
ssl_certificate_key /path/to/key/file;
ssl_dhparam /path/to/dhparams.pem;

keepalive_timeout 70;
sendfile on;
client_max_body_size 99m;
proxy_read_timeout 120; ## Increase if you experience 504 errors uploading media

root /mnt/none;

gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/rss+xml text/javascript image/svg+xml image/x-icon;
gzip_static on;

location / {
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri @mastodon;
}

# If Docker is used for deployment and Rails serves static files,
# then needed uncomment line with `try_files $uri @mastodon;`.
location ^~ /assets/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri @mastodon;
}

location ^~ /avatars/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri @mastodon;
}

location ^~ /emoji/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri @mastodon;
}

location ^~ /headers/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri @mastodon;
}

location ^~ /ocr/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri @mastodon;
}

location ^~ /packs/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri @mastodon;
}

location ^~ /sounds/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri @mastodon;
}

location ^~ /system/ {
add_header Cache-Control "public, max-age=2419200, immutable";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
add_header X-Content-Type-Options nosniff;
add_header Content-Security-Policy "default-src 'none'; form-action 'none'";
try_files $uri @mastodon;
}

location ^~ /api/v1/streaming {
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_set_header Proxy "";

proxy_pass http://streaming;
proxy_buffering off;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

tcp_nodelay on;
}

location @mastodon {
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_set_header Proxy "";
proxy_pass_header Server;

proxy_pass http://backend;
proxy_buffering on;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_cache CACHE;
proxy_cache_valid 200 7d;
proxy_cache_valid 410 24h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header X-Cached $upstream_cache_status;

tcp_nodelay on;
}

error_page 404 500 501 502 503 504 /500.html;
}

主要的改动包括:

  • 使用自己的域名
  • 改正 http2 配置为 http2 on;
  • 和 /assets/ 路径类似地,给其他几个路径也加上了 try_files $uri @mastodon;
  • root 配置 root /mnt/none;(请求都反向代理到 docker 容器了,这个配置项不重要)。
1
2
sudo nginx -t
sudo systemctl restart nginx

此时浏览器访问网站,可以正常打开了。

配置管理用户

前面没有配管理账号,这里需要执行下命令:

1
2
3
4
5
6
7
8
9
10

# 创建 @admin 账号(也可以用别的名称)
docker compose run -e RAILS_ENV=production --rm -- web bundle exec bin/tootctl accounts create \
admin \
--email admin@example.com \
--confirmed \
--role Owner

# 批准 @admin 账号
docker compose run -e RAILS_ENV=production --rm -- web bundle exec bin/tootctl accounts modify admin --approve