Introduction

Misskey uses ActivityPub protocal, which is also the protocal for Mastodon. But Bluesky does use a newer protocal, so you won't be able to reach Bluesky unless you use Bridgy Fed. Offcial documents from Misskey doesn't mention about how to use Meilisearch.

Start the installation

mkdir /opt/misskey
cd /opt/misskey
git clone -b master https://github.com/misskey-dev/misskey.git
cd misskey
git checkout master
cp .config/docker_example.yml .config/default.yml
cp .config/docker_example.env .config/docker.env
cp ./compose_example.yml ./compose.yml

Assuming docker is installed on the host and docker compose is also installed. If not, for Fedora, it's this.

Since we are using Meilisearch in this case we will also need .config/meilisearch.env , so we will modify 4 files in total.

For .config/meilisearch.env we just need to set a key with 16 characters or more.

MEILI_MASTER_KEY='YOUR-KEY-HERE'

For .config/default.yml I think everything is pretty straight forward, just set Postgres, Redis and Meilisearch. Do notice I set the ssl to false, because I think there are some bugs with it right now, it will have stream error if you enable it(might be my problem tho).

And for .config/docker.env just use what you have set in the .config/default.yml for the db

compose.yml

Things should be pretty easy so far. But for a new comer to Fedora, compose.yml made me crazy because of SELinux.(I will not tell you I just set it to permissive mode at the end lol)

Here is my configuration, tailored to Fedora, remember to uncomment meilisearch under web.

services:
  web:
    user: "1001:1001"
    build: .
    restart: always
    links:
      - db
      - redis
#     - mcaptcha
      - meilisearch
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "127.0.0.1:3000:3000"
    networks:
      - internal_network
      - external_network
    env_file:
      - .config/docker.env
    environment:
      - XDG_CACHE_HOME=/tmp/.cache
      - COREPACK_HOME=/tmp/corepack
      - NPM_CONFIG_CACHE=/tmp/.npm
    volumes:
      - ./files:/misskey/files:Z
      - ./.config:/misskey/.config:Z:ro
      - misskey_tmp:/tmp

  redis:
    user: "1000:1000"
    restart: always
    image: redis:7-alpine
    networks:
      - internal_network
    volumes:
      - ./redis:/data:z
    healthcheck:
      test: "redis-cli ping"
      interval: 5s
      timeout: 3s
      retries: 20

  db:
    restart: always
    image: postgres:15-alpine
    user: "999:999"
    networks:
      - internal_network
    env_file:
      - .config/docker.env
    volumes:
      - ./db:/var/lib/postgresql/data:Z,copy
    healthcheck:
      test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
      interval: 5s
      timeout: 3s
      retries: 20

#  mcaptcha:
#    restart: always
#    image: mcaptcha/mcaptcha:latest
#    networks:
#      internal_network:
#      external_network:
#        aliases:
#          - localhost
#    ports:
#      - 7493:7493
#    env_file:
#      - .config/docker.env
#    environment:
#      PORT: 7493
#      MCAPTCHA_redis_URL: "redis://mcaptcha_redis/"
#    depends_on:
#      db:
#        condition: service_healthy
#      mcaptcha_redis:
#        condition: service_healthy
#
#  mcaptcha_redis:
#    image: mcaptcha/cache:latest
#    networks:
#      - internal_network
#    healthcheck:
#      test: "redis-cli ping"
#      interval: 5s
#      retries: 20

  meilisearch:
    restart: always
    image: getmeili/meilisearch:v1.3.4
    environment:
      - MEILI_NO_ANALYTICS=true
      - MEILI_ENV=production
    env_file:
      - .config/meilisearch.env
    networks:
      - internal_network
    volumes:
      - ./meili_data:/meili_data

networks:
  internal_network:
    internal: true
  external_network:

volumes:
  misskey_tmp:

This is a bit different than the official compose.yml file from the repo, we applied labeling here :Z for SELinux, used 127.0.0.1 before web's port to not let public network visit our misskey instance without using 443(will set this up later). Added a /tmp directory for cache and corepack. Added users for each service for permission management, no point of using docker if you are using root.

Set correct permissions

You must saw in the compose.yml, I also added user parameters for each service, this is for security control, also if you don't do that I think SELinux will deny access for docker container processes.

Therefore now we will set correct permission for each directory. We will use 700 here, but if that causes problems for you, 750 is also fine.

chown -R 999:999 ./db
chown -R 1000:1000 ./redis
chown -R 1001:1001 ./files
chmod -R 700 ./db
chmod -R 700 ./redis
chmod -R 700 ./files

Build

docker compose build --no-cache #This will take a while
docker compose run --rm web pnpm run init
docker compose up -d

I always build with --no-cache because it's so easy to get things wrong when there are multiple yml and env files.

If something went wrong and you didn't use --no-cache it's better to prune

docker system prune -a

After everything is up, let's double check the web

curl -I http://127.0.0.1:3000 #ofc you should get 200 OK

Set up reverse proxy and SSL

Now we pretty much just need to install nginx and set up ssl.

dnf install nginx
dnf install snapd #I used snap to install certbot
systemctl enable --now snapd.socket
ln -s /var/lib/snapd/snap /snap
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbot

We will configure reverse proxy now. At /etc/nginx/conf.d we add a config

vim misskey.conf

# My config is as shown below:
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name your.domain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name your.domain.com;

   # Security headers
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline';";
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    client_max_body_size 100m;

    # Proxy configuration
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_http_version 1.1;
        proxy_redirect off;

        # If it's behind another reverse proxy or CDN, remove the following.
        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;

        # For WebSocket
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # Cache settings
        proxy_cache cache1;
        proxy_cache_lock on;
        proxy_cache_use_stale updating;
        proxy_force_ranges on;
        add_header X-Cache $upstream_cache_status;
    }
}

Now we add SSL using certbot, before that use nginx -t to test your config. I don't think an instruction for certbot is needed, it's a straight forward process and it's self explanatory.

I think that's the end?

During the time I am writing the blog, I realized a labeling (that's used to be) in the compose.yml is not needed and will actually cause SELinux denying access for misskey-db-1 . The parameter is:

    security_opt:
      - label=type:container_file_t

With this we are forcing the container process run with the file context(container_file_t) instead of process context(container_t) or unconfined context. We already had the :Z labeling so our container process could access the file volume(in container_file_t context). label=type:container_file_t means that even critical binaries (like bash and its libraries such as libreadline.so.8) are run in a context that isn’t allowed to read them. We can know this by running ps -Z and we will know now docker assigned unconfined_t or container_t context for docker processes.

And yes, I have SELinux enabled at the end.