Problem Statement
The official docker-compose and Docker documentation for self-hosting Lemmy is not suitable for my use-case. It:
- Spins up its own single-use containers for
pictrs
,postgres
andnginx
. - Makes a bunch of assumptions about the deployment network topology that doesn't always work in a more managed setting.
I'm not a pro nor an expert in sysadmin, Docker or web technologies, so it took many hours of deciphering the (very) sparse documentation to figure out how to make Lemmy fit my deployment scenario. Here, I'd like to just share my own docker-compose
, lemmy.hjson
and my NGINX reverse proxy configuration, and hope it helps someone out there.
How I Host My Services
- Each service is single-instance, multiple-use. For example, my
postgres
container serves not just Lemmy, but other containers that require a DB service as well. - I have an existing reverse proxy with nginx, already provided by the awesome swag image.
- Each container gets an assigned internal LAN hostname, assigned internal LAN IP, and specified MAC address for house-keeping and manageability purposes.
- I have full control and authorship of all my services, and only services are exposed to the public internet through either CloudFlare or
nginx
. Hence, storing key values in my docker-compose is not a major security risk. If my LAN is breached, then I have bigger things to worry about besides a few passkeys being compromised. If you are operating in a multi-user LAN environment where security is paramount, then please use Docker Secrets instead of storing your secrets in plaintext.
Some Parameters You'll Need
My template values are assumed as such. For API keys and passwords, use your own generator or some UUID generation service. If you're using Linux and have the uuidgen
or pwgen
package, just generate keys on your terminal with
uuidgen -r | sed 's/-//g';
The second command just removes the -
character from stdout. All provided values below are dummy values! Please generate your own whenever applicable..
Alternatively, if you prefer to have upper and lower cases,
pwgen -1 --num-passwords=1 --secure 64
As an aside, in my keys, I avoid having symbols in them, as I find that troubleshooting errors due to an improperly-escaped password is just not worthwhile. I prefer generating longer alphanumeric passwords since there will be no need for escape characters.
To generate MAC addresses, use any MAC address generator tool, or an online service.
Needless to say, change the following parameters to suit your own deployment.
General Networking
- Your internal DNS IP:
192.168.0.1
- Your LAN subnet mask:
192.168.0.0/16
- Your Docker container subnet mask:
192.168.1.0/24
- Your Docker host IP:
192.168.1.1
- Your localhost domain name:
.local
- Your Docker bridge name (this must be an existing bridge):
custom_docker_bridge
Your SMTP settings
Here, I am assuming you have a gmail account that you want to use as your mailbox to send admin emails. Follow this guide to generate an app password for Google to authenticate you.
- smtp_server:
smtp.gmail.com:587
- smtp_login:
your@gmail.com
- smtp_password:
abcdefghijklmnop
- smtp_from_address:
no-reply@yourdomainname.yourtld
Your Lemmy Site Name & Admin Account
- admin_username:
admin
- admin_password:
c97f337aaa374d8a9c47fce0e197fd29
- site_name:
lemmy.yourowndomainname.yourtld
For pictrs
- PICTRS__SERVER__API_KEY:
e7160a506a9241abb1e623d4180d6908
- Container IP:
192.168.1.2
- Container MAC:
30:b1:fb:dd:af:ee
- Container Hostname:
PICTRS.local
- Persistent Volume:
/some/host/directory/pictrs
For postgres
- POSTGRES_USER:
postgres_admin
- POSTGRES_PASSWORD:
eefb3bce7ea54b8497307d0e0234b6c8
- POSTGRES_DB:
postgres_db
- Container IP:
192.168.1.3
- Container MAC:
a9:95:c4:a3:e5:4f
- Container Hostname:
POSTGRES.local
- Persistent Volume:
/some/host/directory/postgres
For lemmy
- Container IP:
192.168.1.4
- Container MAC:
77:26:eb:bf:c9:f7
- Container Hostname:
LEMMY.local
- Persistent Volume (for your
lemmy.hsjon
):/some/host/directory/lemmy/lemmy.hjson
- DB name:
lemmy_db
- DB user:
lemmy_admin
- DB password:
ebd3526474cf4cc6af752971f268d0f3
For lemmy-ui
- Container IP:
192.168.1.5
- Container MAC:
bd:77:70:e6:ca:d8
- Container Hostname:
LEMMYUI.local
- LEMMY_UI_LEMMY_EXTERNAL_HOST (this is your public-facing domain name that points to your Lemmy UI):
lemmy.yourowndomainname.yourtld
- LEMMY_UI_LEMMY_INTERNAL_HOST (match with above):
LEMMY.local:8536
Templates for docker-compose
For pictrs
version: "3.7"
services:
pictrs:
container_name: pictrs
image: asonix/pictrs:0.4
environment:
- PICTRS__SERVER__API_KEY=e7160a506a9241abb1e623d4180d6908
ports:
- 8080:8080
restart: unless-stopped
hostname: PICTRS.local
dns: 192.168.0.1
mac_address: 30:b1:fb:dd:af:ee
networks:
custom_docker_bridge:
ipv4_address: 192.168.1.2
volumes:
- /some/host/directory/pictrs:/mnt
networks:
custom_docker_bridge:
external: true
name: custom_docker_bridge
For postgres
This assumes you don't already have a postgres instance.
version: "3.7"
services:
postgres:
container_name: postgres
image: postgres:latest
# This is the default postgres db that is created when you spin up a new postgres container. This will not be used by Lemmy, but the credentials here are important in case you ever lose your password to `lemmy_admin`.
environment:
- POSTGRES_USER=postgres_admin
- POSTGRES_PASSWORD=eefb3bce7ea54b8497307d0e0234b6c8
- POSTGRES_DB=postgres_db
ports:
- 5432:5432
restart: unless-stopped
hostname: POSTGRES.local
dns: 192.168.0.1
mac_address: a9:95:c4:a3:e5:4f
networks:
custom_docker_bridge:
ipv4_address: 192.168.1.3
command:
[
"postgres",
"-c",
"session_preload_libraries=auto_explain",
"-c",
"auto_explain.log_min_duration=5ms",
"-c",
"auto_explain.log_analyze=true",
"-c",
"track_activity_query_size=1048576",
]
volumes:
- /some/host/directory/postgres:/var/lib/postgresql/data
networks:
custom_docker_bridge:
external: true
name: custom_docker_bridge
For lemmy
Backend
version: "3.7"
services:
lemmy:
container_name: lemmy
image: dessalines/lemmy:latest
hostname: LEMMY.local
dns: 192.168.0.1
mac_address: 77:26:eb:bf:c9:f7
ports:
- 8536:8536
networks:
custom_docker_bridge:
ipv4_address: 192.168.1.4
restart: unless-stopped
volumes:
- /some/host/directory/lemmy/lemmy.hjson:/config/config.hjson:Z
networks:
custom_docker_bridge:
external: true
name: custom_docker_bridge
For lemmy-ui
Frontend
version: "3.7"
services:
lemmy-ui:
container_name: lemmy-ui
image: dessalines/lemmy-ui:latest
environment:
- LEMMY_UI_LEMMY_INTERNAL_HOST=LEMMY.local:8536
- LEMMY_UI_LEMMY_EXTERNAL_HOST=lemmy.yourowndomainname.yourtld
- LEMMY_UI_HTTPS=false
- LEMMY_UI_DEBUG=true
hostname: LEMMYUI.local
dns: 192.168.0.1
mac_address: bd:77:70:e6:ca:d8
networks:
custom_docker_bridge:
ipv4_address: 192.168.1.5
restart: unless-stopped
networks:
custom_docker_bridge:
external: true
name: custom_docker_bridge
Template for lemmy.hjson
{
database: {
uri: "postgres://lemmy_admin:ebd3526474cf4cc6af752971f268d0f3@POSTGRES.local:5432/lemmy_db"
}
pictrs: {
url: "http://PICTRS.local:8080/"
api_key: "e7160a506a9241abb1e623d4180d6908"
}
email: {
smtp_server: "smtp.gmail.com:587"
smtp_login: "your@gmail.com"
# Password to login to the smtp server
smtp_password: "abcdefghijklmnop"
smtp_from_address: "no-reply@yourdomainname.yourtld"
tls_type: "tls"
}
# These will be used for the first-ever time the container is created. This is the admin account used to login to https://lemmy.yourowndomainname.yourtld and manage your Lemmy instance.
setup: {
# Username for the admin user
admin_username: "admin"
# Password for the admin user. It must be at least 10 characters.
admin_password: "c97f337aaa374d8a9c47fce0e197fd29"
# Name of the site (can be changed later)
site_name: "lemmy.yourowndomainname.yourtld"
# Email for the admin user (optional, can be omitted and set later through the website)
admin_email: "admin@yourowndomainname.yourtld"
}
hostname: "lemmy.yourowndomainname.yourtld"
# Address where lemmy should listen for incoming requests
bind: "0.0.0.0"
# Port where lemmy should listen for incoming requests
port: 8536
# Whether the site is available over TLS. Needs to be true for federation to work.
tls_enabled: true
}
Template for nginx
This assumes your NGINX's http
directive is pre-configured and exists elsewhere.
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name lemmy.*;
# Assuming your ssl settings are elsewhere
include /config/nginx/ssl.conf;
set $lemmy_frontend_hostname lemmyui.local;
set $lemmy_frontend_port 1234;
set $lemmy_backend_hostname lemmy.local;
set $lemmy_backend_port 8536;
set $upstream_proto http;
location ~ ^/(api|pictrs|feeds|nodeinfo)/ {
set $prox_pass $upstream_proto://$lemmy_backend_hostname:$lemmy_backend_port;
proxy_pass $prox_pass;
}
location / {
# Default to lemmyui.local
set $prox_pass $upstream_proto://$lemmy_frontend_hostname:$lemmy_frontend_port;
# Specific routes to lemmy.local
if ($http_accept ~ "^application/.*$") {
set $prox_pass $upstream_proto://$lemmy_backend_hostname:$lemmy_backend_port;
}
if ($request_method = POST) {
set $prox_pass $upstream_proto://$lemmy_backend_hostname:$lemmy_backend_port;
}
proxy_pass $prox_pass;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Conclusion
That's it! If you're looking for more in-depth tutorials for how each of these work, it is unfortunately out of scope for this post. Hope this helps someone in their journey to self-host Lemmy. Cheers.
Edit #1 - (2023-07-29) nginx.conf
needed some additional parameters for proxy_http_version
and proxy_set_header
, otherwise Lemmy's root_span_builder
will start to throw Incoming activity has invalid signature
errors. I believe the important line is proxy_set_header Host $host;
Edit #2 - (2023-07-31) Added another handy password generation CLI tool, pwgen
, and a short note on not having symbols within API keys/passwords.
I will not be happy until cats can look at us wherever and whenever they want to.