English
Get started
← All templates

Deploy Lemmy on Pethost in one click

A link aggregator and forum for the fediverse

What happens

  1. You sign in to Pethost or make an account, with a passkey, GitHub or Google.
  2. No machine yet? You pick a plan, and we prepare one for you. The link waits in your browser until it is ready.
  3. New project opens at the app. You fill in what it asks for and click Create project: Admin password.
  4. In a minute or two the app is live at its own address with HTTPS. Its secrets are made for it at random, and its data is in the nightly backup.
  5. It is a project like any other: its compose.yaml, its .env, its logs and a terminal are in the panel, and your agent can change it, roll it back or restore it.

First steps

Sign in as admin with the password you gave; the first start takes a minute or two. New accounts wait for your approval: Registration Mode is under Admin Settings. The password is changed in Lemmy’s own settings: the one in .env is read only once. The address is the instance’s name in the fediverse, so keep it.

What it runs

The template is this compose.yaml and the variables it writes into the project’s .env. Pethost runs the file as it is written.

services:
  # Lemmy's own front door: a fediverse server asks for a page as ActivityPub
  # JSON, and those requests, the API's and every non-GET go to the backend;
  # the rest goes to the UI.
  proxy:
    image: nginx:1-alpine
    restart: unless-stopped
    configs:
      - source: nginx
        target: /etc/nginx/nginx.conf
    depends_on:
      - lemmy
      - lemmy-ui

  lemmy:
    image: dessalines/lemmy:0.19.20
    restart: unless-stopped
    environment:
      RUST_LOG: warn
    configs:
      - source: lemmy
        target: /config/config.hjson
    depends_on:
      db:
        condition: service_healthy
      pictrs:
        condition: service_started
    # The first start runs the migrations and makes the admin before it listens.
    healthcheck:
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8536/api/v3/site"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 180s

  lemmy-ui:
    image: dessalines/lemmy-ui:0.19.20
    restart: unless-stopped
    environment:
      LEMMY_UI_LEMMY_INTERNAL_HOST: lemmy:8536
      LEMMY_UI_LEMMY_EXTERNAL_HOST: ${HOST}
      LEMMY_UI_HTTPS: "true"
    depends_on:
      lemmy:
        condition: service_healthy
    # The image's own check runs once a minute; this one answers in ten seconds.
    healthcheck:
      test: ["CMD-SHELL", "curl -ILfSs http://localhost:1234/ > /dev/null || exit 1"]
      interval: 10s
      timeout: 10s
      retries: 3
      start_period: 30s

  pictrs:
    image: asonix/pictrs:0.5
    restart: unless-stopped
    environment:
      PICTRS__SERVER__API_KEY: ${PICTRS_API_KEY}
      # Kept light: no video transcoding, and small animations, as Lemmy's docs set them.
      PICTRS__MEDIA__VIDEO__ENABLE: "false"
      PICTRS__MEDIA__ANIMATION__MAX_WIDTH: 256
      PICTRS__MEDIA__ANIMATION__MAX_HEIGHT: 256
      PICTRS__MEDIA__ANIMATION__MAX_AREA: 65536
      PICTRS__MEDIA__ANIMATION__MAX_FRAME_COUNT: 400
    volumes:
      - pictrs:/mnt

  db:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: lemmy
      POSTGRES_USER: lemmy
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    # Lemmy's docs raise Docker's 64 MB: big queries fail with "could not resize shared memory segment".
    shm_size: 1g
    volumes:
      - db:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U lemmy -d lemmy"]
      interval: 5s
      timeout: 5s
      retries: 12
      start_period: 30s

configs:
  # Lemmy reads its settings from this file alone. `setup` makes the admin at the
  # first start, so no setup page is left open; the password is a multiline
  # string, which Hjson takes as written, quotes and backslashes included.
  lemmy:
    content: |
      {
        hostname: "${HOST}"
        database: {
          host: db
          password: "${DB_PASSWORD}"
        }
        pictrs: {
          url: "http://pictrs:8080/"
          api_key: "${PICTRS_API_KEY}"
          # Thumbnails of linked pages stay where they are, not copied into pict-rs.
          image_mode: "None"
        }
        setup: {
          admin_username: admin
          admin_password:
            '''
            ${ADMIN_PASSWORD}
            '''
          site_name: Lemmy
        }
      }

  # Lemmy's nginx_internal.conf (lemmy-ansible); $$ is a $ to Compose.
  nginx:
    content: |
      worker_processes auto;

      events {
          worker_connections 1024;
      }

      http {
          # Docker's DNS, asked at each request, so that a recreated container is found.
          resolver 127.0.0.11 valid=5s;

          # The visitor's address, from the machine's proxy.
          set_real_ip_from 10.0.0.0/8;
          set_real_ip_from 172.16.0.0/12;
          set_real_ip_from 192.168.0.0/16;

          map "$$request_method:$$http_accept" $$proxpass {
              default "http://lemmy-ui:1234";
              "~^(?:GET|HEAD):.*?application\/(?:activity|ld)\+json" "http://lemmy:8536";
              "~^(?!(GET|HEAD)).*:" "http://lemmy:8536";
          }

          server {
              set $$lemmy_ui "lemmy-ui:1234";
              set $$lemmy "lemmy:8536";
              listen 8536;
              server_name localhost;
              server_tokens off;

              # Uploads go to pict-rs through the backend.
              client_max_body_size 20M;

              proxy_set_header Host $$http_host;
              proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;

              location / {
                  proxy_pass $$proxpass;
                  rewrite ^(.+)/+$$ $$1 permanent;
              }

              location = /.well-known/security.txt {
                  proxy_pass "http://$$lemmy_ui";
              }

              location ~ ^/(api|pictrs|feeds|nodeinfo|.well-known|version|sitemap.xml) {
                  proxy_pass "http://$$lemmy";
              }
          }
      }

volumes:
  pictrs:
  db: