Lemmy Schedule

June 6, 2025 ยท View on GitHub

An app that makes it possible to schedule posts on Lemmy.

Am I the only one who thinks it's funny how the name "Lemmy Schedule" sounds like "let me schedule"?

Migrating to 2.x

If you're migrating to 2.x, please read the migration guide.

Interesting parts

  • AppAuthenticator - the authentication process, here you can make sure that I'm not a villain trying to steal your precious credentials and then use the power of impersonating you to take over the world.
  • CreatePostJob - the job that gets saved to schedule your post, as you can see I store your JWT to impersonate you, but sadly it's impossible to do without.

How it works?

It uses the excellent Symfony framework and even more excellent Symfony Messenger.

Basically, it creates a Messenger job and configures a delay for the job that is the same as the time of the post. The job is then received, processed and deleted (depends on your particular messenger backend).

Building the app locally

Note that this is a tutorial for a fully local deployment, depending on the method you'll be using for deployment (like Docker), you might not need to set up everything mentioned below, feel free to skip to the specific guides below and use this only as a reference if you want a deeper understanding of how the app works.

Here's a brief tutorial on how to build a production version of the app.

Prerequisites

  • php (8.4, intl and json extensions), composer etc.
  • yarn (if you use a different node package manager, you can adapt the commands to it)

Configuring

All configuration is made using environment variables, those can either be real environment variables or ones set in .env.local file.

If an option is marked as required, but also has a default value, you don't have to provide your own value if you're ok with the default

Environment variableDescriptionDefaultRequired
APP_ENVcan be either prod or devdevyes
APP_SECRETa random string used for signing cookies etc., should be unique and, well, as the name implies, secretdo not use the default value, always provide oneyes
DYNAMODB_CACHE_TABLEthe name of the AWS DynamoDB table to use for cache, ignore this option if you don't use DynamoDB for cachecacheno
AWS_REGIONthe name of the AWS region to use, you can ignore this if you're not using DynamoDB for cache and AWS EventBridge as a job schedulereu-central-1no
MESSENGER_TRANSPORT_DSNthe DSN for job transport, more belowredis://localhost:6379/messagesyes
DEFAULT_INSTANCEthe default instance for logging inlemmings.wolrdyes
FILE_UPLOADER_CLASSthe class that handles uploading of files, note that this needs to be set prior to compiling the service containerApp\FileUploader\LocalFileUploaderyes
LOCAL_FILE_UPLOADER_PATHthe filesystem path to store files in temporarily - the default uses a volatile location that might get deleted often, so it's advised to change it%kernel.project_dir%/var/imagesno
S3_FILE_UPLOADER_BUCKETthe bucket to use for storing files temporarily, only used if file uploader is set to AWS S3no
APP_CACHE_DIRthe directory for storing cache and database, it should be some permanent directoryno
APP_LOG_DIRthe directory for storing logsno
SINGLE_INSTANCE_MODEset to either 1 or 0, 1 means that only users from the instance specified in DEFAULT_INSTANCE can log in0yes
IMGUR_ACCESS_TOKENset to Imgur access token if you want to enable Imgurno
UNREAD_POSTS_BOT_JWTthe JWT token for the bot user that will send reports with unread posts - if it's not provided, the whole unread posts functionality will be disabled
UNREAD_POSTS_BOT_INSTANCEthe instance the bot for unread post reports is on - if it's not provided, the whole unread posts functionality will be disabled
DEFAULT_POST_LANGUAGEthe numerical ID of the language that will be preselected when scheduling a post. For list of IDs see this enum0yes
FLAG_COMMUNITY_GROUPSwhether community groups, an experimental feature, should be enabled0no
SOURCE_URLthe URL of the source code, used for new version checks and for a footer link - if you plan on maintaining a fork, you can change it to your fork URLhttps://github.com/RikudouSage/LemmyScheduleyes
NEW_VERSION_CHECKset to 1 or 0 to enable or disable checks for a new version1yes
DEFAULT_COMMUNITIESa comma separated list of communities that will be preselected when going to the schedule pageno

Job transports

Job transport is what manages where the scheduled jobs are stored and where does the messenger component consume them from. You can see the full list of officially supported transports in the official Symfony documentation.

If you're not familiar with the Messenger component, I advise you to go with Redis which is the simplest to configure. The Redis DSN looks like this: redis://localhost:6379/messages.

This configures the server (localhost), port (6379) and key (messages) where the jobs will be stored. For more complex examples visit the official documentation.

File uploading

Currently, there are two options for storing uploaded files โ€” locally or in AWS S3. In either case, the files are deleted once the job that references them is triggered.

These correspond to these FILE_UPLOADER_CLASS values:

Note that this environment variable needs to be set prior to compiling the service container

  • local - App\FileUploader\LocalFileUploader
  • S3 - App\FileUploader\S3FileUploader

For local, you need to also specify LOCAL_FILE_UPLOADER_PATH which is a directory in which the file uploads will be stored. You can use a special variable %kernel.project_dir% that gets replaced with the path to where the project is stored.

If you go with S3, you also need to specify S3_FILE_UPLOADER_BUCKET which is the name of the bucket where the files will be stored.

File providers

File providers are handlers for uploading images. Some of them might need configuration before they show up in the UI.

Currently supported ones:

  • instance upload - the default, always supported
  • Imgur - an Imgur access token is required
  • Catbox - you must either provide a user hash or allow anonymous usage

Building

  • Install the production php dependencies - composer install --no-dev --no-scripts
  • Install node dev dependencies - yarn install
  • Build static JS assets - yarn build
  • Compile the production service container - ./bin/console cache:warmup --env=prod

Done!

Running

If you want to test it out, run the development php server using php -S 127.0.0.1:8000, otherwise follow some guide to set up a webserver, Apache being the easiest solution.

Self-hosting - docker

Configuration

You need to configure these variables:

  • APP_SECRET - a random string around 32 characters length
    • you can use, for example, this command to generate one: hexdump -vn16 -e'4/4 "%08x" 1 "\n"' /dev/urandom

Other variables which might need changing:

  • DEFAULT_INSTANCE - set this to your preferred instance, otherwise the default lemmings.world will be used
  • MESSENGER_TRANSPORT_DSN - the transport name is by default set to expect a container with hostname redis, you might need to change this if your setup is different
  • APP_ENV - you might want to change this to dev if you're debugging the app, otherwise leave it out (or set to prod)
  • FILE_UPLOADER_CLASS - you may want to change how uploaded files are handled
  • SINGLE_INSTANCE_MODE - set to 1 if you want to run in single instance mode where only people from instance set in DEFAULT_INSTANCE can log in
  • IMGUR_ACCESS_TOKEN - the access token to use with Imgur, leave empty to not support Imgur
  • UNREAD_POSTS_BOT_JWT - JWT token of the bot user that will send unread post reports
  • UNREAD_POSTS_BOT_INSTANCE - the instance of the aforementioned bot user
  • FLAG_COMMUNITY_GROUPS - whether to enable community groups, an experimental feature

Volumes

Some permanent files are accessible at these locations:

  • /opt/runtime-cache - legacy, used to contain runtime data, now needs to be bound only during the migration
  • /opt/database - this is where the main SQLite database lives
  • /opt/uploaded-files - directory for uploaded images, MUST be bound to a volume

Ports

The app runs on port 80 inside the container, bind the port to any port you want.

Running

Taking the above into account, here is a docker command used for the production build (replace the APP_SECRET):

  • docker run -d -it -p 8000:80 -v files:/opt/uploaded-files -v data:/opt/database -e DEFAULT_INSTANCE=my.instance.tld -e APP_SECRET=5d640026bb762a0e874c1e1f2656e079 ghcr.io/rikudousage/lemmy-schedule:latest

If you have a Redis instance under different URL (replace redis.example.com with your hostname and lemmy_schedule with the Redis key you want to use):

  • docker run -d -it -p 8000:80 -v files:/opt/uploaded-files -v data:/opt/database -e DEFAULT_INSTANCE=my.instance.tld -e APP_SECRET=5d640026bb762a0e874c1e1f2656e079 -e MESSENGER_TRANSPORT_DSN=redis://redis.example.com/lemmy_schedule ghcr.io/rikudousage/lemmy-schedule:latest

Docker compose

You can easily run the app using docker compose like so:

version: "3.7"

services:
  redis:
    image: valkey/valkey
    hostname: redis
    command: valkey-server --save 60 1 --loglevel warning # make Redis dump the contents to disk and restore them on start
    volumes:
      - redis_data:/data
  lemmy_schedule:
    image: ghcr.io/rikudousage/lemmy-schedule:latest
    ports:
      - "8000:80" # replace 8000 with the port you want your app to run on
    environment:
      APP_SECRET: $APP_SECRET # actually create the secret, don't just use this value
      DEFAULT_INSTANCE: my.instance.tld
    volumes:
      - ./volumes/lemmy-schedule-cache:/opt/runtime-cache # This is only needed for if migrating from old format
      - ./volumes/lemmy-schedule-database:/opt/database
      - ./volumes/lemmy-schedule-uploads:/opt/uploaded-files
    depends_on:
      - redis

volumes:
  redis_data: