The official docker-compose and Docker documentation for self-hosting Lemmy is not suitable for my use-case. It:
pictrs
, postgres
and nginx
.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.
postgres
container serves not just Lemmy, but other containers that require a DB service as well.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.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
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.
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.
192.168.0.1
192.168.0.0/16
192.168.1.0/24
192.168.1.1
.local
custom_docker_bridge
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.gmail.com:587
your@gmail.com
abcdefghijklmnop
no-reply@yourdomainname.yourtld
admin
c97f337aaa374d8a9c47fce0e197fd29
lemmy.yourowndomainname.yourtld
pictrs
e7160a506a9241abb1e623d4180d6908
192.168.1.2
30:b1:fb:dd:af:ee
PICTRS.local
/some/host/directory/pictrs
postgres
postgres_admin
eefb3bce7ea54b8497307d0e0234b6c8
postgres_db
192.168.1.3
a9:95:c4:a3:e5:4f
POSTGRES.local
/some/host/directory/postgres
lemmy
192.168.1.4
77:26:eb:bf:c9:f7
LEMMY.local
lemmy.hsjon
): /some/host/directory/lemmy/lemmy.hjson
lemmy_db
lemmy_admin
ebd3526474cf4cc6af752971f268d0f3
lemmy-ui
192.168.1.5
bd:77:70:e6:ca:d8
LEMMYUI.local
lemmy.yourowndomainname.yourtld
LEMMY.local:8536
docker-compose
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
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
lemmy
Backendversion: "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
lemmy-ui
Frontendversion: "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
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
}
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;
}
}
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;
A place to share alternatives to popular online services that can be self-hosted without giving up privacy or locking you into a service you don’t control.
Rules:
Be civil: we’re here to support and learn from one another. Insults won’t be tolerated. Flame wars are frowned upon.
No spam posting.
Posts have to be centered around self-hosting. There are other communities for discussing hardware or home computing. If it’s not obvious why your post topic revolves around selfhosting, please include details to make it clear.
Don’t duplicate the full text of your blog or github here. Just post the link for folks to click.
Submission headline should match the article title (don’t cherry-pick information from the title to fit your agenda).
No trolling.
Resources:
Any issues on the community? Report it using the report flag.
Questions? DM the mods!
Anyone running lemmy on k8s?
Yup
I haven’t had the deployment use case to get into k8s, but it is always something on my bucket list to pick up. I’d try my hand at it when the opportunity arises. There’s not much to go on but some google-fu turned out this guide: https://codeberg.org/jlh/lemmy-k8s, seems to be fairly digestable on first look.
I’m trying to get Lemmy running on k3s. Slow going learning ansible, kubernetes and lemmy all in one go
This is great, thanks for this. I’ll give this a try later!
Cheers, took me a few attempts (gave up a couple of times early on, but came back determined to finish this) to get it up and running. Let me know how it goes and if this works.
Thank you for this post. I was hoping I could ask you a couple of questions because I also had to make some modifications to get Lemmy up and running. I don’t have email yet because my isp blocks post 25 and I haven’t gotten around to requesting an unblock yet.
I see you’ve modified your hjson file to use gmail. The default config syntax for email is:
Hey buddy, happy to help! You certainly can, you’ll just have to generate an app password for your Google account, it’s pretty simple and guide here (https://support.google.com/accounts/answer/185833?hl=en) is easy to follow.
A quick note if you haven’t done it before, the app password is generated in place of your actual Google account password. It is intended for single-app use, and bypasses multi-factor authentication on your Google account.
Questions:
I think, the default
docker-compose.yml
andlemmy.hjson
state that the PostgreSQL password and the pictrs API key have to match? If I remember correctly, they both have something like{{ postgres_password }}
as default. I found that weird, but I also didn’t question it.What do you do if one service requires PostgreSQL 15 and another service requires an older version or something like that? Again, if I remember correctly, Lemmy devs recently downgraded PostgreSQL in the default setup for some reason.
I don’t want to fact check what I said right now, because I’m in the bathtub. I’m just talking from the top of my head.
Don’t get me wrong, I use a similar setup for my homelab, because I hate spinning up several instances of entire database servers just to get a service running. But I’d be lying if I claimed that I never ran into issues with that setup.
The docker compose file in the lemmy-ansibe mainline still has postgres 15, so I’m not seeing any evidence of a downgrade.
I probably had this in my head, so nothing major.
I wasn’t calling you out, just contributing my best knowledge to the conversation 😅
Interesting! I didn’t quite see that line about the postgres password and pictrs API key having to match. So far, I haven’t had issues with my instance with them being different values.
If Lemmy really assume by convention that the postgres password and the pictrs key must be the same, it sounds like a huge architectural WTF and massive security risk, so I assume it shouldn’t be.
For postgres versions, my solution would be to host different postgres versions in their own containers if there’s no other elegant way to avoid it. Then the URI should point to the respective postgres containers as necessary.
I’ve only recently started diving into the code and working on standing up my own setup, but so far, as someone who has a bit of devops and architecture experience, the architectural decisions of the project seem less than ideal.
Hoping I’ll be able to contribute some improvements before too long.
Thanks for that! Reading through the Lemmy docs gave me some head-scratching moments too. However, I’m more than grateful to the creators and its a monumental undertaking, so I give them a huge deal of credit.
If you have any tips or suggestions on how to host it better on Docker, let me know too, always happy to tweak and improve my setup and learn as I go along.
All credit where credit is due, it’s an impressive project. Just some things where I’m like… “this isn’t going to stand up to significant traffic as-is”. I’ve legit considered starting a clone - not least because I’m just not as familiar with rust, yet - but that would be counterproductive to my goal of improving things.
As far as improvements, honestly, if you’re just hosting a small instance with a small user count, you’ll probably be fine. If you start getting significant amounts of traffic, that’s where I see problems starting to arise.
Personally, the instance I’m working on, I’m trying to build to support scaling to multiple geolocated servers (and multiple processes on each server to support traffic) with centralized database and image hosting among them. The docker setup is… not suitable for such 😅 I’d love to see how some of the bigger instances have their architectures set up, to see how much they deviate from the standard.
This is extremely valuable, thanks for this!
As a general question, why did you decide to use a single postgres container for multiple services instead of multiple, stack specific containers? When I first started working with containers I considered your scheme for the sake of minimalism, but didn’t want a single container to bring down multiple unrelated services. I also had the resources to accomodate the redundancy.
Thanks for asking! I think this is more or less an architectural choice, and I was vaguely adhering to the microservice design philosophy. While spinning up duplicate services for each container that requires it has its advantages in terms of isolation and what not, I wanted to:
Hence, all of my docker containers are deduped and reused whenever possible, and follow my own notations and conventions, as well as static and opiniated networking. It has been a really fun journey so far, but I’m also a glutton for punishment and sleep-less nights ;p
What makes your guide any different than the official guide?
https://join-lemmy.org/docs/administration/install_docker.html
The official guide is better written, far less complicated, and generally works perfectly fine for someone who knows what they are doing. I had my instance setup on a new digital ocean droplet in a few hours, with lets encrypt and cloudflare.
What does your guide do different other than pass off docker compose files that you didn’t write?
See the Problem Statement section.
But you’re not addressing any of that. You’re just blasting your configurations that you pilfered from the official documents. The official guide is far easier, far more flexible, and far more resilient as you’re not relying on shared resources.
no admin should follow your guide.