1
头图

This article is the first content of using Mastodon to build a personal information platform. I will talk about some details of building Mastodon in a container environment.

At the same time, this article is probably one of the few things you can find on how to set up and optimize Mastodon services in a container environment.

write in front

With more and more systems tossing around, I began to look forward to a place where the messages in these systems can be presented in a centralized manner, so that I can quickly and clearly understand what interesting, new and important things have happened, and Allows me to perform quick queries on data in existing systems in an easier way, as well as record ideas that pop up.

I think that the information display in the form of feed stream with timeline as the clue, and the dialogue with various "virtual applications" and Bots may be able to solve my demands at this stage. The interaction is simple and direct, and the level of interactive operation is also shallow. In most query and recording scenarios, I only need to enter the content and press Enter to get the data I want, without having to open the page of the specific application, and then take another step. Step by step operation.

简单的交互示意图

In the past work and life, I actually used some tools that included interactions or functions that intersected with my demands, such as: TeamToy used in Sina Cloud work, Redmine and Ali Portal used in Taobao, Meituan Elephant used at the time, Slack used later, enterprise WeChat, Xuecheng, etc.

十年前在新浪云使用的 TeamToy

However, most of these solutions are internal or SaaS solutions. In personal use scenarios, especially in combination with various HomeLab systems, I prefer it to be a privatized service.

I am more restrained when it comes to adding "entities", so in the process of exploring before this, I have conducted some investigations and simple systems on Phabricator, Confluence, WordPress, Dokuwiki, Outline and other systems that I am familiar with before. In the secondary development, I found that although some problems can be solved, the interaction and experience are not so comfortable. Because these tools are more or less based on collaboration, or based on content organization, rather than information aggregation and presentation . I need a solution that is cool even when used by one person.

So, I began to completely try to switch my thinking, looking for a clue mentioned above, using the timeline as the information to display clues, and being able to interact with the Bot in the tool to record my thoughts and gather various events that I care about into the tool in real time. , the existing data in various systems can be queried with simple commands and methods. In the end, I settled on Mastodon, a "Twitter/Weibo Like" product I've been tossing around for a while two years ago.

已经成长到两万颗星星的 Mastodon

Before starting to toss, let's talk about its technical architecture.

Technology Architecture

Mastodon's technical architecture is a relatively classic Web architecture. The main functional components are: front-end application (React SPA), application interface (Ruby Rails6), push service (Node Express + WS), background task (Ruby Sidekiq), cache and queue (Redis), database (Postgres), and optional full-text indexing (Elasticsearch 7).

Mastodon 应用架构中的主要构成

In addition, it supports the use of anonymous network communication to communicate with other different community instances on the Internet, and exchanges the published content of the community to complete its concept of a distributed community. However, this function is not within the scope of this article, and it is very simple, so I will not expand it.

Basic service preparation

Before tossing the application, we first complete the construction of the application's dependency on the basic service. Let's talk about network planning first.

Build an application gateway for network planning

As with previous applications, we use Traefik as a service application gateway, allowing applications to dynamically access Traefik using service registration. And use Traefik to provide SSL loading, basic SSO authentication, etc.

If you do not know Traefik, you can read content before learning and understanding.

Mastodon 所在主机网络规划

I hope that each component of Mastodon can be isolated from other container services on the host at the network level under the premise that the components of Mastodon can communicate, the necessary services can be registered with Traefik, and provide Web access.

For the above considerations, we can execute the command to create an additional virtual network card for communication between components:

docker network create mastodon_networks

Building the database: Postgres

In the official configuration file , the definition of the database is as follows:

version: '3'
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"

Although it can be used, but after the database is running, we will receive some running warnings from the program.

********************************************************************************
WARNING: POSTGRES_HOST_AUTH_METHOD has been set to "trust". This will allow
         anyone with access to the Postgres port to access your database without
         a password, even if POSTGRES_PASSWORD is set. See PostgreSQL
         documentation about "trust":
         https://www.postgresql.org/docs/current/auth-trust.html
         In Docker's default configuration, this is effectively any other
         container on the same system.

         It is not recommended to use POSTGRES_HOST_AUTH_METHOD=trust. Replace
         it with "-e POSTGRES_PASSWORD=password" instead to set a password in
         "docker run".
********************************************************************************

During the running of the application, the database terminal will continuously accumulate some request logs and background task execution result log output, which will eventually generate a very large application log file. In extreme cases, it may even fill up the disk, affecting the normal operation of other applications on the entire server.

So, based on the actual situation, I made some simple adjustments to the above configuration:

version: '3'
services:

  db:
    restart: always
    image: postgres:14-alpine
    shm_size: 256mb
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./data:/var/lib/postgresql/data
    environment:
      - "POSTGRES_DB=mastodon"
      - "POSTGRES_USER=mastodon"
      - "POSTGRES_PASSWORD=mastodon"
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

networks:
  mastodon_networks:
    external: true

After saving the above content to the docker-compose.yml file in the postgres docker-compose up -d start the service, wait a moment, use docker-compose ps view the application, and we can see that the service is running normally.

# docker-compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
postgres-db-1       "docker-entrypoint.s…"   db                  running (healthy)   5432/tcp

The configuration and code of this part have been uploaded to GitHub, you can it up if you need it: 161eff45de2ba2 https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/postgres

Building cache and queue services: Redis

The default Redis startup will serve after 30 seconds, which is a bit long for us. In order to make Redis start providing responses faster, I also made simple adjustments to the content in the official configuration:

version: '3'
services:

  redis:
    restart: always
    image: redis:6-alpine
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./data:/data
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

networks:
  mastodon_networks:
    external: true

Save Configuration to redis directory docker-compose.yml , we use docker-compose up -d start the service, wait a moment, use docker-compose ps viewing application, you can see the service run normally.

# docker-compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
redis-redis-1       "docker-entrypoint.s…"   redis               running (healthy)   6379/tcp

The configuration and code of this part have also been uploaded to GitHub, you can get it yourself if you need it: https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/redis

Building full-text search: Elasticsearch

This component is optional for Mastodon, there are several cases where you might not need to use ES:

  • Your machine resources are very tight, enabling ES will take up an additional 500MB~1GB of memory
  • Your site has little content and users
  • Your searches are very limited
  • You expect to use a more resourceful and performant retrieval scheme

In the 2018 PG CONF EU, Oleg Bartunov have done a share, on the use of Postgres using full-text search of the scene of interest can themselves about .

Of course, out of respect for the official choice, we still briefly expand on the construction and use of ES. Also simple adjustments based on the official configuration can complete a new basic arrangement file:

version: '3'
services:

  es:
    restart: always
    container_name: es-mastodon
    image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "cluster.name=es-mastodon"
      - "discovery.type=single-node"
      - "bootstrap.memory_lock=true"
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./data:/usr/share/elasticsearch/data:rw
    ulimits:
      memlock:
        soft: -1
        hard: -1
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

networks:
  mastodon_networks:
    external: true

However, if we save the above orchestration file and try to start the service, we will encounter a classic problem, the directory permissions are incorrect, and the service cannot be started:

"stacktrace": ["org.elasticsearch.bootstrap.StartupException: ElasticsearchException[failed to bind service]; nested: AccessDeniedException[/usr/share/elasticsearch/data/nodes];",
"at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:174) ~[elasticsearch-7.10.2.jar:7.10.2]",
...

ElasticsearchException[failed to bind service]; nested: AccessDeniedException[/usr/share/elasticsearch/data/nodes];
Likely root cause: java.nio.file.AccessDeniedException: /usr/share/elasticsearch/data/nodes
    at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:90)
...

The solution to the problem is very simple. We can set the permissions of the data directory to be operable by the ES process in the container: (The simple and rude chmod 777 strongly not recommended)

mkdir -p data
chown -R 1000:1000 data
docker-compose down && docker-compose up -d

After executing the above command and restarting the container process, use the docker-compose ps command again to check the application status. We can see that the program is running normally.

# docker-compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
es-mastodon         "/tini -- /usr/local…"   es                  running (healthy)   9300/tcp

The configuration and code of this part have also been uploaded to GitHub, you can it up if you need it: 161eff45de2d96 https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/elasticsearch

Application construction

After the basic service is built, let's complete the construction and deployment of the application.

Application initialization

To facilitate application initialization, I wrote a simple orchestration configuration:

version: "3"
services:
  web:
    image: tootsuite/mastodon:v3.4.4
    restart: always
    environment:
      - "RAILS_ENV=production"
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; tail -f /etc/hosts"
    networks:
      - mastodon_networks

networks:
  mastodon_networks:
    external: true

Save the above as docker-compose.init.yml , and then use docker-compose up -d start a Mastodon-ready container for later use.

After the container is started, we execute the following command to start the Mastodon installation bootstrap:

docker-compose -f docker-compose.init.yml exec web bundle exec rake mastodon:setup

After executing the above command, it will enter the interactive command line. We ignore all warning messages and get a log similar to the following (example, you can adjust it according to your own situation)

Your instance is identified by its domain name. Changing it afterward will break things.
Domain name: hub.lab.com

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

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? yes
E-mail address to send e-mails "from": "(Mastodon <notifications@hub.lab.com>)"
Send a test e-mail with this configuration right now? 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:

# Generated with mastodon:setup on 2022-01-24 08:49:51 UTC

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

LOCAL_DOMAIN=hub.lab.com
SINGLE_USER_MODE=true
SECRET_KEY_BASE=ce1111c9cd51305cd680aee4d9c2d6fe71e1ba003ea31cc27bd98792653535d72a13c386d8a7413c28d30d5561f7b18b0e56f0d0e8b107b694443390d4e9a888
OTP_SECRET=bcb50204394bdce54a0783f1ef2e72a998ad2f107a0ee4dc3b61557f5c12b5c76267c0512e3d08b85f668ec054d42cdbbe0a42ded70cbd0a70be70346e666d05
VAPID_PRIVATE_KEY=QzEMwqTatuKGLSI3x4gmFkFsxi2Vqd4taExqQtZMfNM=
VAPID_PUBLIC_KEY=BFBQg5vnT3AOW2TBi7OSSxkr28Zz2VZg7Jv203APIS5rPBOveXxCx34Okur-8Rti_sD07P4-rAgu3iBSsSrsqBE=
DB_HOST=db
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=mastodon
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
SMTP_SERVER=localhost
SMTP_PORT=25
SMTP_AUTH_METHOD=none
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_FROM_ADDRESS="Mastodon <notifications@hub.lab.com>"

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


Database 'postgres' already exists
[strong_migrations] DANGER: No lock timeout set
Done!

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

Do you want to create an admin user straight away? Yes
Username: soulteary
E-mail: soulteary@gmail.com
You can login with the password: 76a17e7e1d52056fdd0fcada9080f474
You can change your password once you login.

In the above interactive program, in order to save time, I chose not to use external services to store files, and not to use external services to send emails. You can adjust it according to your own needs.

During the execution of the command, we may see some error messages related to Redis: Error connecting to Redis on localhost:6379 (Errno::ECONNREFUSED) . This is because we did not correctly configure the Redis server in advance when we started the configuration program and initialized the application. This does not mean that our configuration is wrong, but it has not yet taken effect, so don't panic.

The configuration and code of this part have also been uploaded to GitHub, you can it up if you need it: 161eff45de2e96 https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/app

Update app configuration

Next, we need to save the configuration-related information from the above log output to a configuration file .env.production .

# Generated with mastodon:setup on 2022-01-24 08:49:51 UTC

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

LOCAL_DOMAIN=hub.lab.com
SINGLE_USER_MODE=true
SECRET_KEY_BASE=ce1111c9cd51305cd680aee4d9c2d6fe71e1ba003ea31cc27bd98792653535d72a13c386d8a7413c28d30d5561f7b18b0e56f0d0e8b107b694443390d4e9a888
OTP_SECRET=bcb50204394bdce54a0783f1ef2e72a998ad2f107a0ee4dc3b61557f5c12b5c76267c0512e3d08b85f668ec054d42cdbbe0a42ded70cbd0a70be70346e666d05
VAPID_PRIVATE_KEY=QzEMwqTatuKGLSI3x4gmFkFsxi2Vqd4taExqQtZMfNM=
VAPID_PUBLIC_KEY=BFBQg5vnT3AOW2TBi7OSSxkr28Zz2VZg7Jv203APIS5rPBOveXxCx34Okur-8Rti_sD07P4-rAgu3iBSsSrsqBE=
DB_HOST=db
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=mastodon
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
SMTP_SERVER=localhost
SMTP_PORT=25
SMTP_AUTH_METHOD=none
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_FROM_ADDRESS="Mastodon <notifications@hub.lab.com>"

One thing to note here is that SMTP_FROM_ADDRESS in the sending email notification configuration needs to be wrapped in double quotation marks. If in the above interactive terminal configuration process, we use the carriage return "all the way Next", the generated configuration content may be missing quotation marks The problem.

If this problem occurs, you can manually add quotation marks when saving the file, and you do not need to re-execute the command.

Adjust application web service configuration

As with building the infrastructure and adjusting the configuration before, we can make a simple adjustment to the official configuration template to get the smallest container orchestration configuration that allows the service to run:

version: '3'
services:

  web:
    image: tootsuite/mastodon:v3.4.4
    restart: always
    env_file: .env.production
    environment:
      - "RAILS_ENV=production"
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
    networks:
      - mastodon_networks
    healthcheck:
     test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:3000/health || exit 1"]
     interval: 15s
     retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

  streaming:
    image: tootsuite/mastodon:v3.4.4
    env_file: .env.production
    restart: always
    command: node ./streaming
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    environment:
      - "STREAMING_CLUSTER_NUM=1"
      - "NODE_ENV=production"

  sidekiq:
    image: tootsuite/mastodon:v3.4.4
    environment:
      - "RAILS_ENV=production"
    env_file: .env.production
    restart: always
    command: bundle exec sidekiq
    networks:
      - mastodon_networks
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro


networks:
  mastodon_networks:
    external: true

After saving the above, we will start the service. Because we haven't mapped any ports to the server "local" at this point, we won't be able to access these services yet.

In order to solve this problem, we need to configure the front-end proxy of the Mastodon application.

Configure the service front-end proxy

The service defaults to using Ruby Puma as the web server, and Node Express to provide push and real-time updates. In order to solve the cross-domain problem of front-end resources and further improve service performance, we can use Nginx to provide reverse proxy for these services, aggregate the services together, and cache the static resources in them.

Officially there is a default template here, https://github.com/mastodon/mastodon/blob/main/dist/nginx.conf , but this configuration is suitable for not using containers, or applications running in containers, Nginx does not Scenarios running with containers.

Considering that we use Traefik to provide dynamic service registration and SSL certificate mounting, this configuration needs to be tweaked slightly to work (only major changes are shown).

location / {
  try_files $uri @proxy;
}

location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
  add_header Cache-Control "public, max-age=31536000, immutable";
  try_files $uri @proxy;
}

location /sw.js {
  add_header Cache-Control "public, max-age=0";
  try_files $uri @proxy;
}

location @proxy {
  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 "https";
  proxy_set_header Proxy "";
  proxy_pass_header Server;

  proxy_pass http://web:3000;
  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;
}

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 "https";
  proxy_set_header Proxy "";

  proxy_pass http://streaming:4000;
  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;
}

In the above configuration, Nginx "integrates" the two sets of web services we mentioned from Ruby and Node, and performs basic caching operations for static resources, and does a week-long LRU cache for cacheable content.

Seeing this, our service seems to be running normally. But is there really no problem?

Application bug fixes and architecture tuning

When we got the service up and running, even though the application seemed to be working fine, at this point we ran into the first problem. The warning message "X-Accel-Mapping header missing" frequently appears in the log.

The reason for triggering this problem https://github.com/mastodon/mastodon/issues/3221 , but the community has not given a good solution. The solution to this problem is actually very simple. It is enough to completely move the static resources out of the Ruby Web service: firstly, it can solve this problem, and secondly, it can improve the overall performance of the service, and make the service easier to scale horizontally in the future.

At the same time, when we try to upload pictures or videos, you will find that we always get an error return due to the permissions of the container mount directory. Some people will use the chmod 777 Dafa to solve the problem, but this is not a best practice: there are potential security problems, and the ability of your application to scale horizontally becomes very poor.

Of course, there are still some details, which we will deal with later, and deal with the above two issues first.

Split static resource services

When it comes to splitting application dynamic and static resources, we can't help thinking of CDN in the cloud service environment. In Mastodon, the app supports setting CDN_HOST to split static resources to CDN servers. However, most service maintainers will implement the solution of letting the CDN dynamically return to the source. Under the premise of ignoring a certain degree of data consistency, such maintenance costs are very low, and no adjustments and application changes are required.

But just doing this will not solve the problem we mentioned in the previous article (the CDN time limit is up, and the above problem will still be triggered back to the source). And it is also not conducive to privatized deployment and use (there are additional costs, and it has to rely on public network services).

A better solution here is for our static resources as a separate service running .

Referring to the multi-stage build and optimization of containers in previous articles, it is easy to write a Dockerfile similar to the following:

FROM tootsuite/mastodon:v3.4.4 AS Builder

FROM nginx:1.21.4-alpine
COPY --from=Builder /opt/mastodon/public /usr/share/nginx/html

After using docker build -t mastodon-assets . to package Mastodon's static resources and Nginx into a new image, let's write the container orchestration configuration for this service:

version: '3'
services:

  mastodon-assets:
    image: mastodon-assets
    restart: always
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"

      - "traefik.http.middlewares.cors.headers.accessControlAllowMethods=GET,OPTIONS"
      - "traefik.http.middlewares.cors.headers.accessControlAllowHeaders=*"      
      - "traefik.http.middlewares.cors.headers.accessControlAllowOriginList=*"
      - "traefik.http.middlewares.cors.headers.accesscontrolmaxage=100"
      - "traefik.http.middlewares.cors.headers.addvaryheader=true"

      - "traefik.http.routers.mastodon-assets-http.middlewares=cors@docker"
      - "traefik.http.routers.mastodon-assets-http.entrypoints=http"
      - "traefik.http.routers.mastodon-assets-http.rule=Host(`hub-assets.lab.com`)"
 
      - "traefik.http.routers.mastodon-assets-https.middlewares=cors@docker"
      - "traefik.http.routers.mastodon-assets-https.entrypoints=https"
      - "traefik.http.routers.mastodon-assets-https.tls=true"
      - "traefik.http.routers.mastodon-assets-https.rule=Host(`hub-assets.lab.com`)"

      - "traefik.http.services.mastodon-assets-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.mastodon-assets-backend.loadbalancer.server.port=80"

networks:
  traefik:
    external: true

After saving the above content as docker-compose.yml , and using docker-compose up -d start the service, the static resources that were originally handled by the Ruby service were switched to the independent Nginx service to complete the purpose of static resource handling.

Of course, in order for this operation to take effect, we also need to add the following configuration to .env.production

CDN_HOST=https://hub-assets.lab.com

Independent maintenance of uploaded resources

As mentioned earlier, in the default container application, the program logic is to let the Ruby application maintain and process the media files (pictures, videos) we upload. This solution is also not conducive to the future horizontal expansion of the service and splitting it to run on suitable machines. A relatively better solution is to use the S3 service to manage the files uploaded by the user, so that the application is close to stateless operation.

In "Private Cloud Environment Installed in a Notebook: Network Storage ( )" 161eff45de3181 and "Private Cloud Environment Installed in a Notebook: Network Storage ( 2)" 161eff45de3183, I have introduced how to Use MinIO as a general storage gateway. Therefore, how to build and monitor a private S3 service will not be repeated here, and only some differences will be discussed here.

Here I use the same machine deployment, so the access between services is solved through virtual network cards. Because the services are all "behind" Traefik, the interactive protocol can also be free of HTTPS (just let Mastodon access it directly using the container service name).

Here is a small detail. For the normal operation of the service, our S3 Entrypoint needs to use common ports, such as HTTP (80), HTTPS (443), so the running command in the MinIO service needs to be adjusted to:

command: minio server /data --address 0.0.0.0:80 --listeners 1  --console-address 0.0.0.0:9001

But if we use HTTP, another problem will arise, that is, when Mastodon displays static resources, it will use the HTTP protocol instead of the HTTPS we expect, which will cause the problem that the media resources in the web interface cannot be displayed. (It does not affect the client, how to solve it is limited by space, we will mention it in the next content)

Also use S3 service as file storage backend in Mastodon, because the default URL path provided by S3 service is S3_DOMAIN_NAME/S3_BUCKET_NAME , so we need to do the same S3_ALIAS_HOST However, considering the access performance and efficiency of resources, we can also start a Nginx as the static resource cache of MinIO, and further simplify this configuration, let us directly set S3_DOMAIN_NAME , which will also facilitate our subsequent program customization.

Let's write the orchestration configuration for this service first:

version: "3"
services:

  nginx-minio:
    image: nginx:1.21.4-alpine
    restart: always
    networks:
      - traefik
      - mastodon_networks
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 15s
      retries: 12
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.mastodon-s3-http.entrypoints=http"
      - "traefik.http.routers.mastodon-s3-http.rule=Host(`hub-res.lab.com`)"
      - "traefik.http.routers.mastodon-s3-https.entrypoints=https"
      - "traefik.http.routers.mastodon-s3-https.tls=true"
      - "traefik.http.routers.mastodon-s3-https.rule=Host(`hub-res.lab.com`)"
      - "traefik.http.services.mastodon-s3-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.mastodon-s3-backend.loadbalancer.server.port=80"

  minio:
    image: ${DOCKER_MINIO_IMAGE_NAME}
    container_name: ${DOCKER_MINIO_HOSTNAME}
    volumes:
      - ./data/minio/data:/data:z
    command: minio server /data --address 0.0.0.0:80 --listeners 1  --console-address 0.0.0.0:9001
    environment:
      - MINIO_ROOT_USER=${MINIO_ROOT_USER}
      - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
      - MINIO_REGION_NAME=${MINIO_REGION_NAME}
      - MINIO_BROWSER=${MINIO_BROWSER}
      - MINIO_BROWSER_REDIRECT_URL=${MINIO_BROWSER_REDIRECT_URL}
      - MINIO_PROMETHEUS_AUTH_TYPE=public
    restart: always
    networks:
      - traefik
      - mastodon_networks
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.middlewares.minio-gzip.compress=true"
      - "traefik.http.routers.minio-admin.middlewares=minio-gzip"
      - "traefik.http.routers.minio-admin.entrypoints=https"
      - "traefik.http.routers.minio-admin.tls=true"
      - "traefik.http.routers.minio-admin.rule=Host(`${DOCKER_MINIO_ADMIN_DOMAIN}`)"
      - "traefik.http.routers.minio-admin.service=minio-admin-backend"
      - "traefik.http.services.minio-admin-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.minio-admin-backend.loadbalancer.server.port=9001"
    extra_hosts:
      - "${DOCKER_MINIO_HOSTNAME}:0.0.0.0"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/minio/health/live"]
      interval: 3s
      retries: 12
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

networks:
  mastodon_networks:
    external: true
  traefik:
    external: true

Next, write the configuration of Nginx:

server {
    listen 80;
    server_name localhost;

    keepalive_timeout 70;
    sendfile on;
    client_max_body_size 80m;

    location / {
        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 Host $http_host;

        proxy_connect_timeout 300;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        chunked_transfer_encoding off;

        proxy_pass http://minio/mastodon/;
    }

    location /health {
        access_log off;
        return 200 "ok";
    }
}

Then there is the writing of the MinIO initialization script docker-compose.init.yml

version: "3"
services:

  minio-client:
    image: ${DOCKER_MINIO_CLIENT_IMAGE_NAME}
    entrypoint: >
      /bin/sh -c "
      /usr/bin/mc config host rm local;
      /usr/bin/mc config host add --quiet --api s3v4 local http://minio ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD};
      /usr/bin/mc mb --quiet local/${DEFAULT_S3_UPLOAD_BUCKET_NAME}/;
      /usr/bin/mc policy set public local/${DEFAULT_S3_UPLOAD_BUCKET_NAME};
      "
    networks:
      - traefik

networks:
  traefik:
    external: true

Finally, the basic configuration information required for MinIO to run .env :

# == MinIO
# optional: Set a publicly accessible domain name to manage the content stored in Outline

DOCKER_MINIO_IMAGE_NAME=minio/minio:RELEASE.2022-01-08T03-11-54Z
DOCKER_MINIO_HOSTNAME=mastodon-s3-api.lab.com
DOCKER_MINIO_ADMIN_DOMAIN=mastodon-s3.lab.com
MINIO_BROWSER=on
MINIO_BROWSER_REDIRECT_URL=https://${DOCKER_MINIO_ADMIN_DOMAIN}
# Select `Lowercase a-z and numbers` and 16-bit string length https://onlinerandomtools.com/generate-random-string
MINIO_ROOT_USER=6m2lx2ffmbr9ikod
# Select `Lowercase a-z and numbers` and 64-bit string length https://onlinerandomtools.com/generate-random-string
MINIO_ROOT_PASSWORD=2k78fpraq7rs5xlrti5p6cvb767a691h3jqi47ihbu75cx23twkzpok86sf1aw1e
MINIO_REGION_NAME=cn-homelab-1

# == MinIO Client
DOCKER_MINIO_CLIENT_IMAGE_NAME=minio/mc:RELEASE.2022-01-07T06-01-38Z

DEFAULT_S3_UPLOAD_BUCKET_NAME=mastodon

How to start and use MinIO has been introduced in the previous article. Due to the word limit of the space, I will not expand it. . The relevant code has been uploaded to GitHub 161eff45de32bc https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/minio

Final application configuration

Well, after a little integration of the above content and some simple adjustments, we can get a configuration similar to the following:

version: '3'
services:

  mastodon-gateway:
    image: nginx:1.21.4-alpine
    restart: always
    networks:
      - traefik
      - mastodon_networks
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - ./nginx.conf:/etc/nginx/nginx.conf
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 15s
      retries: 12
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.mastodon-nginx-http.entrypoints=http"
      - "traefik.http.routers.mastodon-nginx-http.rule=Host(`hub.lab.com`)"
      - "traefik.http.routers.mastodon-nginx-https.entrypoints=https"
      - "traefik.http.routers.mastodon-nginx-https.tls=true"
      - "traefik.http.routers.mastodon-nginx-https.rule=Host(`hub.lab.com`)"
      - "traefik.http.services.mastodon-nginx-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.mastodon-nginx-backend.loadbalancer.server.port=80"

  web:
    image: tootsuite/mastodon:v3.4.4
    restart: always
    env_file: .env.production
    environment:
      - "RAILS_ENV=production"
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:3000/health || exit 1"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

  streaming:
    image: tootsuite/mastodon:v3.4.4
    env_file: .env.production
    restart: always
    command: node ./streaming
    networks:
      - mastodon_networks
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1"]
      interval: 15s
      retries: 12
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    environment:
      - "STREAMING_CLUSTER_NUM=1"

  sidekiq:
    image: tootsuite/mastodon:v3.4.4
    env_file: .env.production
    restart: always
    command: bundle exec sidekiq
    networks:
      - mastodon_networks
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

networks:
  mastodon_networks:
    external: true
  traefik:
    external: true

In addition, because we configured independent static resource services and file storage services, we need to add some additional configuration .env.production

CDN_HOST=https://hub-assets.lab.com
S3_ENABLED=true
S3_PROTOCOL=http
S3_REGION=cn-homelab-1
S3_ENDPOINT=http://mastodon-s3-api.lab.com
S3_BUCKET=mastodon
AWS_ACCESS_KEY_ID=6m2lx2ffmbr9ikod
AWS_SECRET_ACCESS_KEY=2k78fpraq7rs5xlrti5p6cvb767a691h3jqi47ihbu75cx23twkzpok86sf1aw1e
S3_ALIAS_HOST=hub-res.lab.com

Use the familiar docker-compose up -d start the service, and after a while, we can see the application that starts normally.

This part of the relevant code has been uploaded to GitHub https://github.com/soulteary/Home-Network-Note/tree/master/example/mastodon/app , you can pick it up if you need it.

Mastodon 应用启动后的第一个界面

Click to log in, use the account email and initialization password we just created the application configuration to complete the application login, and start exploring Mastodon.

登录 Mastodon 后的界面

At last

Even if the content is repeatedly shortened, the word count of this article exceeds the length limit of most platforms, so if you find that there is a part missing during the reading process, you can try to read the original text or the complete example file on GitHub to solve the problem.

In the next article, I will talk about how to do some further tuning operations for performance, and solve some problems that this article has not solved.

In the future, I will sort out and share some small experiences in the process of knowledge management and knowledge base construction, hoping to help you who are also interested in this field and full of curiosity.

--EOF


If you think the content is still useful, please like and share it with your friends, thank you here.

If you want to see the updates of subsequent content faster, please don't hesitate to "like" or "forward and share", these free encouragements will affect the update speed of subsequent related content.


This article uses the "Signature 4.0 International (CC BY 4.0)" license agreement, welcome to reprint, or re-modify for use, but you need to indicate the source. Attribution 4.0 International (CC BY 4.0)

Author of this article: Su Yang

Original link: https://soulteary.com/2022/01/24/building-a-personal-information-platform-with-mastodon-part-1.html


soulteary
191 声望7 粉丝

折腾硬核技术,分享实用内容。