Installation

Configuration

The docker-compose.yml file controls which containers FediSuite starts, how they communicate with each other, where data is stored, and what happens when a container crashes. This page walks through the bundled file service by service.

What is the docker-compose.yml?

The docker-compose.yml describes which containers are started, how they reach each other, where they store their data and in which order they start. Docker Compose reads the file and builds the stack from it. A container is a program running in an isolated environment with its own file system and its own network.

For a simple setup you do not need to touch the docker-compose.yml. Domain, passwords and email settings belong in the .env file. You only need to adapt the Compose file if you want to change the workers or add a reverse proxy.

Put your own changes into a docker-compose.override.yml where possible. Docker Compose loads it automatically in addition, and the self-hosting repository lists it in its .gitignore. That keeps the bundled file unchanged so it can be updated with git pull.

Overview: the four services

The bundled Compose file starts four containers. Each has a clearly defined job:

db postgres:15-alpine
Required

The database. Stores users, posts, drafts, account connections, settings and history data permanently.

app christinloehner/fedisuite:latest
Required

Provides the web interface and the API. On every start it first runs node server/init-db.js (schema, migrations, admin account) and then starts the server.

worker1 christinloehner/fedisuite:latest
Optional

Background jobs: publishes scheduled posts (scheduler), refreshes post numbers and reach, sends the reminder to users without a connected account and generates tips. It is the only container with the scheduler switched on.

worker2 christinloehner/fedisuite:latest
Optional

Additional worker: refreshes post numbers and generates tips. Scheduler and reminder are switched off here. You can add more workers by copying this block.

Important: FediSuite needs neither Redis nor any other queue or cache service. The background jobs run as timers inside the Node.js processes, and the queue for the reach calculation lives in the PostgreSQL database.

Service: db (PostgreSQL)

docker-compose.yml
  db:
    image: postgres:15-alpine
    env_file:
      - .env
    restart: unless-stopped
    volumes:
      - ./postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5

image: postgres:15-alpine

The official PostgreSQL image, version 15, based on Alpine Linux. FediSuite uses PostgreSQL, MySQL or other databases are not supported. You should not simply change the major version in the tag: the data directory is not compatible between PostgreSQL major versions, so a switch goes through dump and restore (see Backup & restore).

env_file: .env

The database container reads POSTGRES_DB, POSTGRES_USER and POSTGRES_PASSWORD from your .env. The user is also the superuser of the database. The values are only evaluated when the data directory is first created: changing POSTGRES_PASSWORD in the .env later does not change the password of an existing database.

restart: unless-stopped

If the container crashes or the server reboots, Docker brings it back up unless you stopped it on purpose with docker compose stop.

volumes: ./postgres:/var/lib/postgresql/data

The folder ./postgres on your server is linked to the data directory inside the container (bind mount). Without this volume all data would be gone as soon as the container is recreated, for example during an update.

Note: The directory ./postgres/ is created next to the docker-compose.yml on the first start. Never delete it, it holds all your data. The repository's .gitignore does not list it (it lists postgres_data/), so do not commit it by accident.

healthcheck

Docker runs pg_isready every 5 seconds. If the check fails five times in a row, the container counts as unhealthy. The app and the workers only start once the database reports healthy, so they do not hit a database that is still starting up.

Service: app

docker-compose.yml
  app:
    image: ${FEDISUITE_IMAGE:-christinloehner/fedisuite:latest}
    pull_policy: always
    env_file:
      - .env
    environment:
      - ENABLE_SCHEDULER=false
      - ENABLE_POSTS_REFRESH=false
      - ENABLE_IDLE_REMINDER=false
      - ENABLE_TIPS_ENGINE=false
      - PLUGINS_ENABLED=true
      - PLUGIN_SCAN_ON_START=true
      - PLUGIN_API_VERSION=1
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./uploads:/app/uploads
      - ./plugins:/app/plugins
      - ./logs:/app/logs
    ports:
      - "3000:3000"
    restart: unless-stopped
    command: sh -c "node server/init-db.js && node server/index.js"
    healthcheck:
      test: ["CMD", "node", "--input-type=module", "-e", "try { const response = await fetch('http://127.0.0.1:3000/api/health', { signal: AbortSignal.timeout(3000) }); if (!response.ok) process.exitCode = 1; } catch { process.exitCode = 1; }"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10m

image: ${FEDISUITE_IMAGE:-christinloehner/fedisuite:latest}

Uses the image from FEDISUITE_IMAGE in your .env. The part after :- is the default if the variable is missing or empty. The tags are listed on the Environment variables page.

pull_policy: always

On every docker compose up Docker checks whether Docker Hub has a newer image for the tag and downloads it if needed. With latest, even a plain docker compose up -d (for example after a configuration change) can therefore update to a new version. If you do not want that, pin a fixed tag in the .env.

environment: ENABLE_*=false

In the default configuration the app container has the scheduler, post refresh, reminder and tips switched off, because the workers take care of that. What is set under environment overrides values with the same name from the .env (which is loaded through env_file). Variables in the .env therefore only apply to the app container if they are not listed under environment. The reach worker (ENABLE_REACH_REFRESH) and the retry worker for historical imports run in every container of the default file, including app.

PLUGINS_ENABLED, PLUGIN_SCAN_ON_START, PLUGIN_API_VERSION

Control the plugin system. PLUGINS_ENABLED=true switches it on, PLUGIN_SCAN_ON_START=true scans the folder /app/plugins for plugins on start (folders containing a plugin.json), and PLUGIN_API_VERSION is the plugin API version of the instance (currently 1). Details are on the plugin pages.

depends_on: db: condition: service_healthy

The app waits until the db container passes its health check. Without this dependency it could try to initialise the database on the first start before PostgreSQL is ready.

ports: "3000:3000"

The app listens on port 3000 inside the container. The mapping 3000:3000 publishes it on the host, on all interfaces. Docker bypasses firewalls such as ufw when it does this. With a reverse proxy you should remove the mapping or restrict it to 127.0.0.1:3000:3000 (see Reverse proxy).

volumes: ./uploads, ./plugins & ./logs

Three bind mounts that the app and the workers share:

  • •./uploads:/app/uploads: attachments of posts and drafts. Since FediSuite 2.0 the files of a post are kept after it is published so it can be reused as a draft. They are removed when the post is deleted.
  • •./plugins:/app/plugins: the plugins folder. Plugins are placed on the host and are visible to the app and the workers.
  • •./logs:/app/logs: audit logs. FediSuite writes security-relevant events (login, registration, two-factor authentication and others) as JSON lines into monthly files (audit-YYYY-MM.log, month in UTC). Email addresses and Fediverse handles are partly masked in them. The logs do not appear in the container output.

healthcheck

Every 5 seconds Docker queries http://127.0.0.1:3000/api/health. The response contains the state of the database connection. start_period: 10m means that failed attempts do not count during the first ten minutes after the start, so long database migrations do not mark the container as broken. As soon as one check passes, the container is healthy. After that, five consecutive failures lead to unhealthy. The workers wait for this state (see below).

command: node server/init-db.js && node server/index.js

Two steps run one after the other on start: init-db.js creates the database schema or brings it up to date and creates the admin account from ADMIN_EMAIL and ADMIN_PASSWORD if it does not exist yet. If a step fails, the server does not start. Then index.js starts the actual app. You do not need to trigger anything manually.

Workers: background jobs

The app container receives requests from users and answers them right away. The workers take care of everything that does not have to happen immediately but has to run regularly. They use the same image as the app and start node server/index.js, but publish no port and have different environment variables.

These jobs run automatically in the background:

Publish scheduled posts

The scheduler checks every minute whether posts are due and publishes them on the respective platform. It also releases posts that got stuck in the middle of publishing (after 30 minutes without a change).

Refresh post numbers

Every minute, due accounts are worked through: new posts, likes, boosts, replies and notifications. In addition a rotation re-reads a few older posts every hour so their counters stay current.

Calculate reach

A queue in the database collects jobs for the reach calculation. Each process works through them in small batches and limits concurrent requests per remote instance. In the default file this worker runs in all three containers.

Send a reminder

Once per user: anyone who registered and verified more than 48 hours ago but has not connected a Fediverse account yet gets an email. The admin account from the .env is excluded.

Generate tips

Every six hours the stored tips of all connected accounts are recalculated, for example about posting times and formats.

Sync the instance list

If the instance is published in the directory at fedisuite.com/instances, the container with the scheduler switched on syncs its details (name, version, user count) every two hours.

What happens without workers? If no worker runs and the app container does not take over the jobs itself, scheduled posts are not published, post numbers stay frozen and reminders and tips are not generated. Either run at least one worker or switch the jobs on in the app container (minimal setup, see below).

How many workers?

There is no fixed formula. The Compose file starts two workers, which is the starting point:

Just you or a very small group

No workers

The jobs run directly in the app container (minimal setup). That saves containers and memory.

Default

2 workers

As shipped. worker1 has the scheduler and the reminder, worker2 helps with post numbers and tips. The app container stays free for the web interface.

Many accounts or a long queue

more workers

Additional workers share the refresh of accounts and the reach queue. The work is split through the database, so workers can safely run in parallel. The logs show whether more workers help or whether REFRESH_BATCH_SIZE and the REACH_* values are the better lever.

Configuring the workers

All workers use the same image as app, but without a published port. Environment variables control the distribution of jobs. The scheduler belongs in exactly one container (ENABLE_SCHEDULER=true only in worker1). Every post is claimed atomically before it is published, so a second scheduler would not post it twice, but it would be redundant.

docker-compose.yml: worker1 (the main worker)
  worker1:
    image: ${FEDISUITE_IMAGE:-christinloehner/fedisuite:latest}
    pull_policy: always
    env_file:
      - .env
    environment:
      - ENABLE_SCHEDULER=true   # true only here: publishes scheduled posts
      - ENABLE_POSTS_REFRESH=true   # refresh accounts
      - ENABLE_IDLE_REMINDER=true   # reminder emails
      - ENABLE_TIPS_ENGINE=true   # generate tips
      - WORKER_ID=worker-1
      - REFRESH_BATCH_SIZE=5
      - PLUGINS_ENABLED=true
      - PLUGIN_SCAN_ON_START=true
      - PLUGIN_API_VERSION=1
    depends_on:
      db:
        condition: service_healthy
      app:
        condition: service_healthy
    restart: unless-stopped
    volumes:
      - ./uploads:/app/uploads
      - ./plugins:/app/plugins
      - ./logs:/app/logs
    command: sh -c "node server/index.js"
docker-compose.yml: worker2 (additional worker)
  worker2:
    image: ${FEDISUITE_IMAGE:-christinloehner/fedisuite:latest}
    pull_policy: always
    env_file:
      - .env
    environment:
      - ENABLE_SCHEDULER=false   # false: the scheduler only runs in worker1
      - ENABLE_POSTS_REFRESH=true   # helps with refreshing
      - ENABLE_IDLE_REMINDER=false   # one container is enough
      - ENABLE_TIPS_ENGINE=true   # process tips in parallel
      - WORKER_ID=worker-2
      - REFRESH_BATCH_SIZE=5
      - PLUGINS_ENABLED=true
      - PLUGIN_SCAN_ON_START=true
      - PLUGIN_API_VERSION=1
    depends_on:
      db:
        condition: service_healthy
      app:
        condition: service_healthy
    restart: unless-stopped
    volumes:
      - ./uploads:/app/uploads
      - ./plugins:/app/plugins
      - ./logs:/app/logs
    command: sh -c "node server/index.js"

Adding more workers (worker3, worker4 …)

Copy the worker2 block and increase the number in the service name and in WORKER_ID. All further workers have ENABLE_SCHEDULER=false and ENABLE_IDLE_REMINDER=false. The WORKER_ID must be unique per worker.

  worker3:
    image: ${FEDISUITE_IMAGE:-christinloehner/fedisuite:latest}
    pull_policy: always
    env_file:
      - .env
    environment:
      - ENABLE_SCHEDULER=false
      - ENABLE_POSTS_REFRESH=true
      - ENABLE_IDLE_REMINDER=false
      - ENABLE_TIPS_ENGINE=true
      - WORKER_ID=worker-3
      - REFRESH_BATCH_SIZE=5
      - PLUGINS_ENABLED=true
      - PLUGIN_SCAN_ON_START=true
      - PLUGIN_API_VERSION=1
    depends_on:
      db:
        condition: service_healthy
      app:
        condition: service_healthy
    restart: unless-stopped
    volumes:
      - ./uploads:/app/uploads
      - ./plugins:/app/plugins
      - ./logs:/app/logs
    command: sh -c "node server/index.js"

What do the variables mean in detail?

ENABLE_SCHEDULER

Controls whether the container publishes due scheduled posts (every minute) and, if the instance is published in the directory, syncs the instance list. Default in the code: true; the Compose file sets it to true in worker1 and to false elsewhere.

ENABLE_POSTS_REFRESH

Regularly refreshes accounts and post numbers (one batch every minute). May run in several containers, the accounts are split with database locks. Default: true.

ENABLE_REACH_REFRESH

Switches the reach queue on. Not set in the Compose file, so active in every container. Default: true.

ENABLE_IDLE_REMINDER

Sends the one-time reminder to users without a connected Fediverse account (checked hourly, at most 50 per pass). One container is enough. Default: true.

ENABLE_TIPS_ENGINE

Generates the tips every six hours. May run in several containers. Default: true.

WORKER_ID

A freely chosen, unique name for the container, for example worker-1. It shows up in the logs and in locks and queues. Without a value FediSuite uses the hostname of the container.

REFRESH_BATCH_SIZE

How many accounts a worker refreshes per pass (every 60 seconds). Range 1 to 50, default in the code 10, the Compose file sets 5. Smaller values go easier on the servers of the connected instances, larger ones work through many accounts faster.

Further settings (reach, time limits, logging) are on the Environment variables page.

Minimal setup: just db + app

Workers are optional. If you run FediSuite just for yourself or a very small group, you can do without the worker containers. That saves memory because fewer containers run.

In the default file the app container has the background jobs switched off (ENABLE_*=false). In the minimal setup you switch them back on there:

docker-compose.yml: app in minimal setup
  app:
    image: ${FEDISUITE_IMAGE:-christinloehner/fedisuite:latest}
    ...
    environment:
      - ENABLE_SCHEDULER=true        # was false before
      - ENABLE_POSTS_REFRESH=true    # was false before
      - ENABLE_IDLE_REMINDER=true    # was false before
      - ENABLE_TIPS_ENGINE=true      # was false before
      - PLUGINS_ENABLED=true
      - PLUGIN_SCAN_ON_START=true
      - PLUGIN_API_VERSION=1

After that you can delete or comment out the worker1 and worker2 blocks. docker compose up -d then only starts db and app.

Switching later is possible. You can move to workers later: add the worker blocks, set the ENABLE_* variables in the app container back to false and run docker compose up -d. The data stays intact.

Volumes: where data is stored

Containers are ephemeral: when a container is removed, its data is gone. Volumes link a folder on the host to a folder in the container so data survives.

FediSuite uses bind mounts only. The data sits in ordinary folders next to the docker-compose.yml, so a backup covers these folders (see Backup & restore).

./postgres/ /var/lib/postgresql/data

Used by: db

All database data: users, posts, drafts, settings, links to Fediverse accounts. The most important part of the whole setup.

Never delete this folder. It contains all the database data of your instance.
./uploads/ /app/uploads

Used by: app, worker1, worker2

Attachments of posts and drafts. Must be shared between the app and the workers because the workers read them when publishing.

./plugins/ /app/plugins

Used by: app, worker1, worker2

Installed plugins (one subfolder each, containing a plugin.json).

./logs/ /app/logs

Used by: app, worker1, worker2

Audit logs as JSON lines in monthly files (audit-YYYY-MM.log). Parts of email addresses and handles are masked. The logs do not appear in the container output.

Further concepts explained

Why do worker1 and worker2 use the same image as app?

FediSuite is a single Node.js application that takes on different roles depending on environment variables. The ENABLE_* variables decide which role a container has. That only app is reachable from outside is down to the published port in the Compose file, not the image.

What does depends_on with service_healthy mean?

Without this condition Docker starts all containers at the same time. The app would then access the database before PostgreSQL is ready. With condition: service_healthy each dependent container waits for the health check to pass. The workers depend on db and app: they only start once the app has finished its migrations and responds, so the workers and the app do not work on the database schema at the same time.

Network: how do the containers talk to each other?

Docker Compose creates an internal network in which all containers reach each other by service name. The app talks to the database under the hostname db, which is why the DATABASE_URL reads postgresql://…@db:5432/…. The database publishes no port and cannot be reached from outside.

Traefik labels and network in the docker-compose.yml

The app service has Traefik labels and the network entries prepared as comments. The file docker-compose.traefik.example.yml shows the same entries as an example. How to adapt them is explained on the Reverse proxy page.