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 explains every part of it — even if you've never worked with Docker before.

What is the docker-compose.yml?

Think of the docker-compose.yml as a blueprint. It defines which programs (containers) get started, how they can reach each other, where they store their data, and in which order they should start.

A single Docker container is like a program running in a completely isolated environment — it has its own file system, its own network interface, and runs independently from the rest of the system. Docker Compose orchestrates multiple such containers and lets them work together.

You do not need to touch the docker-compose.yml for basic FediSuite operation. All personal settings like domain, passwords, and email configuration belong in the .env file. Only those who want to customize the worker setup or integrate Traefik need to edit this file.

Overview: the four services

In full operation, FediSuite consists of four containers. Each has a clearly defined role:

db postgres:15-alpine
Required

The database. Permanently stores all users, posts, account links, settings, and history data.

app christinloehner/fedisuite:latest
Required

The core. Provides the web interface and API through which users operate FediSuite. Automatically initializes the database on first start.

worker1 christinloehner/fedisuite:latest
Optional

Handles all background tasks: publishes scheduled posts on time, fetches updated stats from platforms, sends reminders, and generates tips. You can run more than one worker.

worker2, worker3 … christinloehner/fedisuite:latest
Optional

Each additional worker shares the load so stats stay fresh as your instance grows. app.fedisuite.com, for example, runs 5 workers.

Important: None of the four containers use Redis. Background jobs are managed directly by the worker processes — no separate cache service is needed.

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 in version 15, based on Alpine Linux — a particularly lightweight Linux variant. FediSuite uses PostgreSQL as its database; MySQL or other systems are not supported.

env_file: .env

The database container reads POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD directly from your .env file. You don't need to enter these values twice.

restart: unless-stopped

If the container crashes or the server restarts, Docker automatically brings it back up — unless you deliberately stopped it with docker compose stop. This ensures FediSuite keeps running reliably after a reboot without any manual intervention.

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

Something important happens here: the ./postgres folder on your server is linked to the /var/lib/postgresql/data folder inside the container. This is called a Bind Mount.

Without this volume, all database data would be lost as soon as the container is deleted or recreated — for example, during an update. With the bind mount, the data remains on your server regardless of what happens to the container.

Remember: The ./postgres/ directory is created automatically next to your docker-compose.yml on first start. Never delete this folder — it contains all your data.

healthcheck

Docker checks every 5 seconds whether PostgreSQL is actually ready to accept connections — not just whether the container is running. Only once this check passes (after at most 5 attempts) does the app container start. This prevents the app from trying to access a database that hasn't finished starting up yet.

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
    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"

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

Uses the image you set in FEDISUITE_IMAGE in your .env. The part after :- is the default value if the variable is empty — in this case christinloehner/fedisuite:latest.

pull_policy: always

Docker checks on every start whether a newer version of the image is available on Docker Hub and downloads it if needed. This ensures that after a docker compose up -d you're always running the latest version — as long as you use the latest tag.

environment: ENABLE_*=false

In full operation, the app container has all background jobs disabled — the workers handle those tasks. The difference between env_file and environment:

  • env_file loads all variables from the .env file into the container.
  • environment sets individual variables directly in docker-compose.yml — and overrides values from the .env. This is intentional: the workers should handle the jobs, not the app.

depends_on: db: condition: service_healthy

The app waits to start until the db container passes its health check — meaning PostgreSQL is actually accepting connections. Without this dependency, the app could try to initialize the database before it's ready, causing startup errors.

ports: "3000:3000"

The FediSuite app runs internally on port 3000. The mapping 3000:3000 makes this port accessible on the host system — the server itself. If you use Traefik as a reverse proxy, Traefik can reach the app through this port, and you access FediSuite at your domain via HTTPS.

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

Three bind mounts:

  • ./uploads:/app/uploads — FediSuite stores files uploaded by users here (e.g. media for posts). These are preserved across updates.
  • ./plugins:/app/plugins — The plugins folder. Plugins are managed on the host and accessible to the app and workers.
  • ./logs:/app/logs — Application audit logs. FediSuite writes security-relevant events (login, registration, 2FA, etc.) as JSON Lines to monthly-rotating files (audit-YYYY-MM.log). Logs do not appear in container stdout.

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

When the container starts, two steps run in sequence: first, init-db.js initializes the database structure — creates tables, sets up the first admin account from ADMIN_EMAIL/ADMIN_PASSWORD. Then index.js starts the actual app. You don't need to trigger this initialization manually.

Workers: background tasks

Think of FediSuite like a small office: the app container is the front desk — it takes requests from users and responds immediately. Workers are the back-office staff who handle everything that doesn't need to happen instantly but needs to be done regularly.

Specifically, workers handle these tasks automatically in the background — without users or admins having to do anything:

📅

Publish scheduled posts

When a user schedules a post for 2:00 PM, the worker makes sure it goes out on Mastodon, Bluesky & co. right on time.

📊

Update statistics

Likes, reposts, comments — the worker regularly fetches fresh numbers from the platforms so the analytics dashboard stays current.

🔔

Send inactivity reminders

If a user has posting goals enabled and hasn't posted in a while, the worker automatically sends a friendly nudge.

💡

Generate tips

The tips engine analyses which posts performed particularly well and prepares helpful suggestions for users in the background.

What happens without workers? If no worker is running and the app container isn't handling the jobs itself, scheduled posts will not be published, statistics will stay frozen, and reminders will not be sent. Either the app container handles these tasks itself (minimal setup, see below) — or you run at least one worker.

How many workers do I need?

It depends on how many users your instance has. As a rule of thumb:

👤

Just you or 2–3 people

→ No worker needed

Run all background jobs directly inside the app container (minimal setup). Saves resources.

👥

Small group (up to ~20 users)

→ 1 worker

One worker reliably handles all tasks, leaving the app container free for the web interface.

🏘️

Medium community (up to ~100 users)

→ 2 workers

Worker1 handles scheduling and reminders; worker2 helps with statistics and tips.

🏙️

Large community (100+ users)

→ 3–5 workers

app.fedisuite.com runs 5 workers to serve thousands of accounts smoothly. More workers = faster stats updates.

Configuring workers

All workers use the same Docker image as the app container — but without a web interface. Task distribution is controlled via environment variables. Key rule: ENABLE_SCHEDULER=true must appear in exactly one worker — otherwise scheduled posts may be published multiple times.

docker-compose.yml — worker1 (the "main worker")
  worker1:
    image: ${FEDISUITE_IMAGE:-christinloehner/fedisuite:latest}
    pull_policy: always
    env_file:
      - .env
    environment:
      - ENABLE_SCHEDULER=true       # Only true here — publishes scheduled posts
      - ENABLE_POSTS_REFRESH=true   # Fetch stats from platforms
      - ENABLE_IDLE_REMINDER=true   # Send inactivity reminders
      - ENABLE_TIPS_ENGINE=true     # Generate tips for users
      - WORKER_ID=worker-1
      - REFRESH_BATCH_SIZE=3
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./uploads:/app/uploads
      - ./plugins:/app/plugins
      - ./logs:/app/logs
    restart: unless-stopped
    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 — scheduler only runs in worker1
      - ENABLE_POSTS_REFRESH=true   # Helps with stats updates
      - ENABLE_IDLE_REMINDER=false  # Reminders are fine in one worker
      - ENABLE_TIPS_ENGINE=true     # Process tips in parallel
      - WORKER_ID=worker-2
      - REFRESH_BATCH_SIZE=3
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./uploads:/app/uploads
      - ./plugins:/app/plugins
      - ./logs:/app/logs
    restart: unless-stopped
    command: sh -c "node server/index.js"

Adding more workers (worker3, worker4 …)

Simply copy the worker2 block and increment the number in WORKER_ID and the service name. All additional workers keep ENABLE_SCHEDULER=false.

  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=3
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./uploads:/app/uploads
      - ./plugins:/app/plugins
      - ./logs:/app/logs
    restart: unless-stopped
    command: sh -c "node server/index.js"

What do the variables mean in detail?

ENABLE_SCHEDULER

Controls whether this container publishes scheduled posts at the right time. Must be true in exactly one container — otherwise posts are either never published or sent multiple times. In normal operation with workers: true in worker1, false in all others.

ENABLE_POSTS_REFRESH

Regularly fetches updated numbers from Fediverse platforms: likes, reposts, comments, reach. Can run in multiple containers at the same time — the work is split between them and completes faster. How many posts are processed per cycle is set by REFRESH_BATCH_SIZE.

ENABLE_IDLE_REMINDER

Regularly checks whether users have posting goals enabled and haven't posted in a while — and then automatically sends a reminder. One container is enough for this.

ENABLE_TIPS_ENGINE

Analyses in the background which posts performed particularly well or poorly and turns that into tips for users. Can run in multiple containers.

WORKER_ID

A freely chosen name for this container, e.g. worker-1, worker-2. It appears in the audit logs and helps you trace which worker did what when troubleshooting. Every worker needs its own unique ID.

REFRESH_BATCH_SIZE

How many posts the worker updates from platforms in one cycle. Small value (e.g. 3): gentler on platform APIs but slower with many users. Larger value (e.g. 10): faster but more API requests. Recommendation: 3 is a solid starting point — increase if needed.

Minimal setup: only db + app

Workers are optional. If you run FediSuite for yourself or a very small group, you can skip separate worker containers entirely. This saves RAM and system resources since fewer containers are running.

In the default setup the app container has all background jobs disabled (ENABLE_*=false), because normally workers handle those tasks. In minimal setup you simply turn them back on in the app container:

docker-compose.yml — app in minimal setup
  app:
    image: ${FEDISUITE_IMAGE:-christinloehner/fedisuite:latest}
    ...
    environment:
      - ENABLE_SCHEDULER=true      # ← set to true
      - ENABLE_POSTS_REFRESH=true  # ← set to true
      - ENABLE_IDLE_REMINDER=true  # ← set to true
      - ENABLE_TIPS_ENGINE=true    # ← set to true

Then delete or comment out all worker blocks from your docker-compose.yml. You then simply start with docker compose up -d — only db and app will run.

You can switch at any time. Moving from minimal to worker-based operation is straightforward: add worker blocks to your docker-compose.yml, set the ENABLE_* flags in the app container back to false, and run docker compose up -d. No data is lost in the process.

Volumes: where data is stored

Containers are inherently ephemeral — when a container is removed, all data inside it is gone. Volumes solve this problem: they link a folder on the host system with a folder inside the container so that data persists permanently.

FediSuite uses exclusively Bind Mounts — meaning you can see the data directly as regular folders next to the docker-compose.yml. This makes backups straightforward: just back up these folders.

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

Used by: db

All database data: user accounts, posts, settings, Fediverse account links. The most important part of the entire setup.

Never delete this folder — it contains all the database data for your instance.
./uploads/ /app/uploads

Used by: app, worker1, worker2

Files uploaded by users, e.g. images for posts. Must be shared between the app and workers.

./plugins/ /app/plugins

Used by: app, worker1, worker2

Installed plugins. Plugins are managed on the host and accessible to the app and workers.

./logs/ /app/logs

Used by: app, worker1, worker2

Application audit logs. FediSuite writes security-relevant events as JSON Lines to monthly-rotating files (audit-YYYY-MM.log). Logs do not appear in container stdout and contain no personal data in plain text (GDPR-compliant).

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 app image can start as both a web server and a background processor. The role a container takes on is determined by the combination of ENABLE_* variables and the command entry.

What does depends_on with service_healthy mean?

Without this condition, Docker would start all containers simultaneously. The app could then try to access the database before PostgreSQL is even ready — leading to startup errors. With condition: service_healthy, each dependent container waits until the db health check has passed successfully.

Networking: how do containers communicate?

Docker Compose automatically creates an internal network in which all containers in the stack can reach each other by their service name. The app talks to the database simply using the hostname db — that's why the DATABASE_URL reads postgresql://...@db:5432/.... The database is not accessible from the outside.

Traefik labels in the docker-compose.yml

In the app service you'll find commented-out Traefik labels. These tell Traefik which domain the app should be accessible at and which cert resolver to use for TLS. How to set up Traefik and customize these labels is covered on the next pages.