Deploy Lemmy on Pethost in one click
A link aggregator and forum for the fediverse
What happens
- You sign in to Pethost or make an account, with a passkey, GitHub or Google.
- No machine yet? You pick a plan, and we prepare one for you. The link waits in your browser until it is ready.
- New project opens at the app. You fill in what it asks for and click Create project: Admin password.
- 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.
- 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: