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.
On this page
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
The database. Stores users, posts, drafts, account connections, settings and history data permanently.
app
christinloehner/fedisuite:latest
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
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
Additional worker: refreshes post numbers and generates tips. Scheduler and reminder are switched off here. You can add more workers by copying this block.
Service: db (PostgreSQL)
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
env_file: .env
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
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.
./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
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
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}
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
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
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
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
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"
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
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
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.
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 workersThe jobs run directly in the app container (minimal setup). That saves containers and memory.
Default
2 workersAs 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 workersAdditional 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.
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"
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:
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.
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.
./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.