Self-hosting a Lemmy server with Podman

Preamble

So… I don’t doubt many readers have heard of the huge fiasco between Reddit and its users. Actually, there have been many recent fiascos between large companies and their users. (Like just recently, YouTube and Invidious. Invidious was the only way for me to watch videos on YouTube…) As a result, many people have started to consider the Fediverse more and more: a decentralized way of social networking where users are in charge. (And usually, through using FOSS!)

I’ve been eying the Fediverse myself for some time, but have been putting it off for a while, telling myself it wasn’t a priority because I only use two social media services (Reddit included). Well, Reddit’s gone, so I guess it’s time to federate!

I also am in a bit of a pickle for another reason: I don’t really trust others with my data anymore (no offense to anyone running a server). Plus, ideally with the Fediverse, everyone would be self-hosting their account so that they don’t overload existing servers (like lemmy.ml has seen) and are in more control of their data.

So I decided to host my own private Lemmy instance! Unfortunately, the current documentation is only for Docker/Ansible on amd64 systems, and since I have a Raspberry Pi with Podman, that doesn’t entirely work out. So after some tinkering, I’ve found a way to self-host Lemmy on Podman, with just a few console commands!

Lastly, I should state two things you’ll need: (1) Podman 4.0 or higher, which meant for me unofficially upgrading my Raspbian install to Debian 12, and (2) a bit of technical expertise with Linux. (I mean, you are self-hosting, after all, so you probably know more than me.)

I should also state that I initially this while very hyper off the fact that I finally did this, so I might not explain things or go on tangents. I’m really sorry if I do do that! If you notice/encounter any errors or have suggestions, list them in the comments below and I’ll update this article! I also recommend you have a look at a guide that explains what you’re doing in more detail first, like this one. Just keep in mind that it’s not updated for the latest version of Lemmy/using Podman so it might not work.

Ready?

LET’S DO THIS

First off, this tutorial is based off the Docker files from version 0.17 of Lemmy. I recommend you have a look at it here: https://github.com/LemmyNet/lemmy/tree/release/v0.17/docker/prod. If you want to run later versions, you just need to change the images of the containers you will be creating (more on that later). Make sure to look to the release branch of the version you want (make sure it says “release” in the URL just like the link I provided). Look for where it says “image” for each container, and then just copy the release tag for each container to the docker.io links in each command.

You could create a Podman network for this, but those haven’t actually worked in testing. We’re going to be using the podman pod commands, which also help to organize our files.

1. Prepare the files!

Make sure you’re in your home directory. You’ll need to create a working folder to put all your Lemmy data in, so we’ll make a folder in the home directory where we store configuration files and volumes. Let’s do that now:

mkdir -p ~/Lemmy
cd ~/Lemmy
# Create folders for pictrs (which will store our picture data) and postgres
mkdir -p volumes/pictrs
mkdir -p volumes/postgres

We also need to assign special permissions to one of our volumes, so that pictrs can use it.

sudo chown -R 991:991 volumes/pictrs
sudo chmod a+rw volumes/pictrs

Next, we’ll need to copy over the Nginx config from Lemmy’s Docker tutorial:

worker_processes 1;
events {
    worker_connections 1024;
}
http {
    upstream lemmy {
        # this needs to map to the lemmy (server) docker service hostname
        server "lemmy:8536";
    }
    upstream lemmy-ui {
        # this needs to map to the lemmy-ui docker service hostname
        server "lemmy-ui:1234";
    }

    server {
        # this is the port inside docker, not the public one yet
        listen 80;
        # change if needed, this is facing the public web
        server_name localhost;
        server_tokens off;

        gzip on;
        gzip_types text/css application/javascript image/svg+xml;
        gzip_vary on;

        # Upload limit, relevant for pictrs
        client_max_body_size 20M;

        add_header X-Frame-Options SAMEORIGIN;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";

        # frontend general requests
        location / {
            # distinguish between ui requests and backend
            # don't change lemmy-ui or lemmy here, they refer to the upstream definitions on top
            set $proxpass "http://lemmy-ui";

            if ($http_accept ~ "^application/.*$") {
              set $proxpass "http://lemmy";
            }
            if ($request_method = POST) {
              set $proxpass "http://lemmy";
            }
            proxy_pass $proxpass;

            rewrite ^(.+)/+$ $1 permanent;
            # Send actual client IP upstream
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        # backend
        location ~ ^/(api|pictrs|feeds|nodeinfo|.well-known) {
            proxy_pass "http://lemmy";
            # proxy common stuff
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

            # Send actual client IP upstream
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

Save this file as ~/Lemmy/nginx.conf.

Now, we’ll need to come up with a few things.

We're generating passwords!
We’re generating passwords!

First, generate a password for your database and API key. I’ll henceforth refer to these as $DB_PASSWORD and $API_PASSWORD for the rest of this guide. Done? So here’s the next (and last) file you’ll have to make:

Download lemmy.hjson from the GitHub repository I linked earlier. Or just copy it from here. In either case, you’ll need to replace the lines I highlighted with the PASSWORD variables. Don’t worry about changing anything else since we will be able to do that with the UI once we set up Lemmy.

{
  # for more info about the config, check out the documentation
  # https://join-lemmy.org/docs/en/administration/configuration.html
  # only few config options are covered in this example config

  # the domain name of your instance (eg "lemmy.ml")
  hostname: "localhost"
  # 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

  # pictrs host
  pictrs: {
    url: "http://pictrs:8080/"
    api_key: "$API_PASSWORD"
  }

  # settings related to the postgresql database
  database: {
    # name of the postgres database for lemmy
    database: "lemmy"
    # username to connect to postgres -- not the same as admin_username!
    user: "lemmy"
    # password to connect to postgres
    password: "$DB_PASSWORD"
    # host where postgres is running
    host: "postgres"
    # port where postgres can be accessed
    port: 5432
    # maximum number of active sql connections
    pool_size: 5
  }
}

3. LET’S COPY SOME COMMANDS

Note:

If you’re using an ARM processor, you need to install qemu-user-static.

Starting with 0.18.0, Lemmy builds on Docker are no longer for arm64 devices, meaning you have to emulate it with QEMU. Luckily, Lemmy isn’t too taxing on hardware so you shouldn’t notice any dips in performance.

Step 1 is to create a Podman pod. This will allow our containers to talk with one another with only two ports facing the outside world. (You only need port 8537, but we’ll open 8536 in case we need it.)

podman pod create --publish 8536:8536 --publish 8537:80 lemmy-pod

Next, we need to create an Nginx container. This is ultimately how every other container is going to get its port forwarded to the outside world.

podman run \
  --detach \
  -v $HOME/Lemmy/nginx.conf:/etc/nginx/nginx.conf:Z \
  --restart on-failure \
  --name lemmy-proxy \
  --pod lemmy-pod \
  docker.io/library/nginx:1-alpine

Next step is to create a Lemmy container.

podman run \
  --detach \
  --restart on-failure \
  --name lemmy \
  --pod lemmy-pod \
  --env RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug" \
  --env RUST_BACKTRACE=full \
  -v $HOME/Lemmy/lemmy.hjson:/config/config.hjson:Z \
  --arch amd64 \
  docker.io/dessalines/lemmy:0.18.0

Then we’ll need to create another container for the UI. This is how you will be able to access Lemmy from your browser.

podman run \
  --detach \
  --restart on-failure \
  --name lemmy-ui \
  --pod lemmy-pod \
  --env LEMMY_UI_LEMMY_INTERNAL_HOST=lemmy:8536 \
  --env LEMMY_UI_LEMMY_EXTERNAL_HOST=localhost:1236 \
  --env LEMMY_HTTPS=true \
  --env RUST_BACKTRACE=full \
  --arch amd64 \
  docker.io/dessalines/lemmy-ui:0.18.0

Then, we need to create a pictrs server. This is how Lemmy will be able to store and retrieve pictures you want to upload to your server.

podman run \
  --detach \
  -v $HOME/Lemmy/volumes/pictrs:/mnt \
  --security-opt label=disable \
  --restart on-failure \
  --name pictrs \
  --env PICTRS_OPENTELEMETRY_URL=http://otel:4137 \
  --env PICTRS__API_KEY="$API_KEY" \
  --env RUST_LOG=debug \
  --env RUST_BACKTRACE=full \
  --user 991:991 \
  --pod lemmy-pod \
  docker.io/asonix/pictrs:0.4.0-beta.19

FINALLY. Our last server is Postgres, which stores a bunch of data.

podman run \
  --detach \
  --restart on-failure \
  --name postgres \
  -v $HOME/Lemmy/volumes/postgres:/var/lib/postgresql/data:Z \
  --env POSTGRES_USER=lemmy \
  --env POSTGRES_PASSWORD="$DB_PASSWORD" \
  --env POSTGRES_DB=lemmy \
  --pod lemmy-pod \
  docker.io/library/postgres:15-alpine \
  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

And WE’RE DONE!! Head over to localhost:8537 to enjoy the fruits of your labour.

But…

Now here’s where I try to do rapid-fire coverage of the things I didn’t cover, or all the errors you might encounter. It may seem like light coverage, but if I covered all these things in-depth, this article would be twice the length it is now!

Why is my homepage white/how do I start seeing some posts?

This is a huge one and a problem I’ve had and thought about for quite a while after I set up my server. Turns out, you first have to go to your admin settings. Refresh the page so that Lemmy can pull all previous settings (like which domains you’re federated with). Scroll to the near-bottom and start adding instances to your federation allowlist (like lemmy.ml or lemmy.world. Or just copy the list on https://join-lemmy.org). Then, go back to the homepage, and search up a community. For instance, to get all posts for https://lemmy.ml/c/programming, search up “[email protected]”. There’s also a test community at https://lemmy.ml/c/test/.

At this point, you still don’t see anything. Refresh the page after you hit the search button, and you should get a link to the community you want. Click on the link, and then hit the subscribe button. (Apparently, actions on other communities will only be recognized from your server when someone’s subscribed to it.)

Now that you’re done this, go back to your settings and remove everything from the allowlist. Here’s the reason why:

Note: 🤓 alert, you can skip past this if you want.

Lemmy works by not pulling content from servers, but actions. For instance, when someone makes a post about beans, that action is broadcast from the instance the user made the post on to any servers that (a) have the community listed on their server (which can happen when a user searches for a community) and (b) have that Lemmy instance in their allowlist or “recognized” list. When someone wants to comment on that post, or vote on it, it’s the same deal. This is also why you don’t see any posts when you search up for a community for the first time, and also why you can still see posts from communities on a long-gone instance (like vlemmy). All those posts are not just read from those other servers, but cached on your own!

So when both those lists are empty, we need to put a Lemmy community on the allowlist so we can search up for a community, and then once that community is on the server, it can start pulling actions from the Lemmy instance that community originated from (it’s on the “recognized” list too now, so we can take it off of the allowlist).

Then, we need to clear the allowlist. This tells Lemmy that if anybody comments on that post with beans, it can be anyone from any instance — but don’t worry if you get any negative comments, as the community’s moderators can always delete comments and ban users. This allows you to see posts made from people in personal instances, which are hard to all manually include with an allowlist. Every time someone comments on that post in the example, their domain is added to the recognized list. If their server has any communities you want to search up and start subscribing to, this can now be possible. So we have a bit of a snowball effect that allows us to interact with ALL THE PEOPLE, but we can still leave up a denylist for instances that are borderline illegal!

How do I back up data on my Lemmy instance?

Data isn’t stored on the containers themselves — at least, not important data. Everything you need to back up is actually in the ~/Lemmy folder we created a while back – that’s why it has all those volumes. That’s where Postgres and pictrs store their data.

I rebooted my system, but now I can’t access my instance!

The containers are automatically destroyed when you turn off your computer. You can read up here on how to make them systemd services so they’re automatically recreated when you turn on your computer. (Note: on my PC, with Podman 4.51, they aren’t automatically destroyed? Still, I recommend making the pod a systemd service just in case.)

I have a domain I use to access my server, how do I set it up with Lemmy?

Try installing something like Apache and then making a reverse proxy from the port 8357 to 80/443.

Update: If you use Apache, it should be good to note that you also need to proxy WebSockets. Here’s an approach I adapted from this StackOverflow post:

<IfModule mod_ssl.c>
<VirtualHost *:443>
  ProxyPreserveHost On
  ProxyRequests Off
  ServerName lemmy.example.com
  ServerAlias lemmy.example.com

  # HTML
  <Location />
    ProxyPass http://localhost:8537/
    ProxyPassReverse http://localhost:8537/
  </Location>

  # WebSockets (location is /api because all the WebSocket calls I have seen were to it)
  <Location /api>
    ProxyPass ws://localhost:8537/api
    ProxyPassReverse ws://localhost:8537/api
  </Location>

  # [...]
</VirtualHost>
</IfModule>

And that’s all!

That’s everything you need to get a Lemmy instance on your own computer! Hope you found this useful (and not too long), and thanks for reading!


Comments

One response to “Self-hosting a Lemmy server with Podman”

  1. Consider federating this WordPress blog.

Leave a Reply

Your email address will not be published. Required fields are marked *