Mementomori.social
Try BirdUI in containers

Running mastodon with BirdUI in containers

Mementomori.social runs BirdUI theme for mastodon. If you want to try out what it looks like in your environment, or develop or perhaps switch using it, there is containerized way to easily (?) do so. It is described in this doc.

Just out of interest I thought it would be nice to try and play around with mastodon on my home server. Better yet, could I run it easily in containers. Answer is yes I can, and I write instructions how you can too!

This doc describes you how you can experiment BirdUI or mastodon in general. We build simple mastodon setup with containers for different roles. Containers are running in one pod in podman. There is traefik container doing load balancing as reverse proxy.

There are plenty of docs how to run mastodon in docker, you can use those to test BirdUI just by exchanging the container image url to point to birdUI ones instead of the originals.

Quick experiment with podman kube play

Let's start by creating working directory for the tests:

mkdir ~/mastodon-birdui
cd !$

Create a file defining the pods the kubernetes way. This is just one way, but let's start with it. The file contains mastodon configuration options. I just setup something that should not federate and keeps it minimal. Go ahead and enhance it by following mastodon configuration guide.

cat > mastodon-pod.yaml <<EOF
---
apiVersion: v1
# This configmap is used to define the env variables that Mastodon uses.
kind: ConfigMap
metadata:
  name: mastodon-env
data:
  # DB Config
  POSTGRES_USER: mastodon
  POSTGRES_PASSWORD: mysecretdbpasswd # XXX Generate this
  POSTGRES_DB: mastodon_production
  DB_USER: mastodon
  DB_NAME: mastodon_production
  DB_PASS: mysecretdbpasswd # XXX Generate this
  DB_PORT: "5432"
  DB_HOST: localhost
  # Site Config
  ALLOWED_PRIVATE_ADDRESSES: 192.168.117.0/24 # CHANGE to your home network
  LOCAL_HTTPS: false
  LIMITED_FEDERATION_MODE: true
  SECRET_KEY_BASE: "e59f561d592a9" # XXX Generate this
  OTP_SECRET: "2a3b41e7d69d66bf0b" # XXX Generate this
  LOCAL_DOMAIN: mastodon.homeserver.localnet # XXX change to your servers name in you network
  IP_RETENTION_PERIOD: "31556952"
  SESSION_RETENTION_PERIOD: "31556952"
  ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: okj1Ad # XXX Generate this
  ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: Lo6Q # XXX Generate this
  ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: WEUBzsSwMKeV # XXX Generate this
  VAPID_PRIVATE_KEY: Qd1KNmM # XXX Generate this
  VAPID_PUBLIC_KEY: BGFXvIU # XXX Generate this
  AUTHORIZED_FETCH: false
  # Valkey Config
  REDIS_HOST: "127.0.0.1"
  REDIS_PORT: "6379"
  # XXX Mail Config, SMTP settings explnations here: https://docs.joinmastodon.org/admin/config/#email
  SMTP_FROM_ADDRESS: "me@example.com"
  SMTP_LOGIN: "me@example.com"
  SMTP_PASSWORD: "my_smtp_passwd"
  SMTP_PORT: "587"
  SMTP_SERVER: "mail.example.com"
  SMTP_DELIVERY_METHOD: "none"
  SMTP_TLS: "true"
---
apiVersion: v1
kind: Pod
metadata:
  name: mastodon
  annotations:
    # the following annotations are instructions for podman
    io.kubernetes.cri-o.ContainerType/db: container
    io.kubernetes.cri-o.ContainerType/valkey: container
    io.kubernetes.cri-o.ContainerType/sidekiq: container
    io.kubernetes.cri-o.ContainerType/streaming: container
    io.kubernetes.cri-o.ContainerType/web: container
    io.kubernetes.cri-o.SandboxID/db: mastodon
    io.kubernetes.cri-o.SandboxID/valkey: mastodon
    io.kubernetes.cri-o.SandboxID/sidekiq: mastodon
    io.kubernetes.cri-o.SandboxID/streaming: mastodon
    io.kubernetes.cri-o.SandboxID/web: mastodon
    io.kubernetes.cri-o.TTY/db: "false"
    io.kubernetes.cri-o.TTY/valkey: "false"
    io.kubernetes.cri-o.TTY/sidekiq: "false"
    io.kubernetes.cri-o.TTY/streaming: "false"
    io.kubernetes.cri-o.TTY/web: "false"
    io.podman.annotations.autoremove/db: "FALSE"
    io.podman.annotations.autoremove/valkey: "FALSE"
    io.podman.annotations.autoremove/sidekiq: "FALSE"
    io.podman.annotations.autoremove/streaming: "FALSE"
    io.podman.annotations.autoremove/web: "FALSE"
    io.podman.annotations.init/db: "FALSE"
    io.podman.annotations.init/valkey: "FALSE"
    io.podman.annotations.init/sidekiq: "FALSE"
    io.podman.annotations.init/streaming: "FALSE"
    io.podman.annotations.init/web: "FALSE"
    io.podman.annotations.privileged/db: "FALSE"
    io.podman.annotations.privileged/valkey: "FALSE"
    io.podman.annotations.privileged/sidekiq: "FALSE"
    io.podman.annotations.privileged/streaming: "FALSE"
    io.podman.annotations.privileged/web: "FALSE"
    io.podman.annotations.publish-all/db: "FALSE"
    io.podman.annotations.publish-all/valkey: "FALSE"
    io.podman.annotations.publish-all/sidekiq: "FALSE"
    io.podman.annotations.publish-all/streaming: "FALSE"
    io.podman.annotations.publish-all/web: "FALSE"
  labels:
    app: mastodon
    # Here we configure Traefik reverse proxy
    traefik.enable: "true"
    # LB for web port
    traefik.http.services.mastodon-web.loadbalancer.server.port: "3000"
    traefik.http.routers.mastodon-web.rule: "Host(`mastodon.homeserver.localnet`)"
    traefik.http.routers.mastodon-web.entrypoints: "websecure"
    traefik.http.routers.mastodon-web.service: "mastodon-web"
    traefik.http.routers.mastodon-web.tls: "true"
    # The following would setup redirect from http to https. No need here, but for an example.
    # traefik.http.middlewares.mastodon-web-https-redirect.redirectscheme.scheme: https
    # traefik.http.routers.mastodon-web.middlewares: "mastodon-web-https-redirect"
    # traefik.http.routers.mastodon-web-secure.rule: Host(`mastodon.homeserver.localnet`)
    # traefik.http.routers.mastodon-web-secure.tls: "true"
    # LB for streaming port
    traefik.http.services.mastodon-streaming.loadbalancer.server.port: "4000"
    traefik.http.routers.mastodon-streaming.rule: "(Host(`mastodon.homeserver.localnet`) && PathPrefix(`/api/v1/streaming`))"
    traefik.http.routers.mastodon-streaming.entrypoints: "websecure"
    traefik.http.routers.mastodon-streaming.service: "mastodon-streaming"
    traefik.http.routers.mastodon-streaming.tls: "true"
    # traefik.http.middlewares.mastodon-streaming-https-redirect.redirectscheme.scheme: https
    # traefik.http.routers.mastodon-streaming.middlewares: "mastodon-streaming-https-redirect"
    # traefik.http.routers.mastodon-streaming-secure.rule: (Host(`mastodon.homeserver.localnet`) && PathPrefix(`/api/v1/streaming`))
spec:
  containers:
    - args:
        - postgres
      envFrom:
        - configMapRef:
            name: mastodon-env
            optional: false
      image: docker.io/library/postgres:14-alpine
      name: db
      ports:
        - containerPort: 3000
          hostPort: 3000
        - containerPort: 4000
          hostPort: 4000
        - containerPort: 9200
          hostPort: 9200
      livenessProbe:
        exec:
          command:
            - pg_isready
            - -U
            - postgres
        initialDelaySeconds: 30
        periodSeconds: 10
        timeoutSeconds: 5
      resources: {}
      securityContext:
        capabilities:
          drop:
            - CAP_MKNOD
            - CAP_AUDIT_WRITE
      volumeMounts:
        - mountPath: /var/lib/postgresql/data
          name: mastodon-vol-db-pvc
    - args:
        - valkey-server
      image: docker.io/valkey/valkey:9-alpine
      name: valkey
      livenessProbe:
        exec:
          command:
            - valkey-cli
            - ping
        initialDelaySeconds: 10
        periodSeconds: 10
        timeoutSeconds: 5
      resources: {}
      securityContext:
        capabilities:
          drop:
            - CAP_MKNOD
            - CAP_AUDIT_WRITE
      volumeMounts:
        - mountPath: /data
          name: mastodon-vol-valkey-pvc
    - command: ["node"]
      args: ["./streaming"]
      envFrom:
        - configMapRef:
            name: mastodon-env
            optional: false

      image: ghcr.io/mementomori-social/mastodon-streaming:mementomods-2026-06-19
      # image: docker.io/tootsuite/mastodon-streaming:latest
      name: streaming
      livenessProbe:
        exec:
          command:
            - /bin/sh
            - -c
            - "curl -s --noproxy localhost localhost:4000/api/v1/streaming/health | grep -q 'OK' || exit 1"
        initialDelaySeconds: 30
        periodSeconds: 15
        timeoutSeconds: 5
      resources: {}
      securityContext:
        capabilities:
          drop:
            - CAP_MKNOD
            - CAP_AUDIT_WRITE
    - command: ["bundle"]
      args: ["exec", "sidekiq"]
      envFrom:
        - configMapRef:
            name: mastodon-env
            optional: false

      image: ghcr.io/mementomori-social/mastodon:mementomods-2026-06-19
      # image: docker.io/tootsuite/mastodon:latest
      name: sidekiq
      livenessProbe:
        exec:
          command:
            - /bin/sh
            - -c
            - "ps aux | grep '[s]idekiq' || false"
        initialDelaySeconds: 30
        periodSeconds: 15
        timeoutSeconds: 5
      resources: {}
      securityContext:
        capabilities:
          drop:
            - CAP_MKNOD
            - CAP_AUDIT_WRITE
      volumeMounts:
        - mountPath: /mastodon/public/system
          name: mastodon-vol-pubsys-pvc

    - command: ["/bin/bash"]
      args:
        [
          "-c",
          # "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000",
          "while true; do foo; sleep 10; done"
        ]
      envFrom:
        - configMapRef:
            name: mastodon-env
            optional: false
      image: ghcr.io/mementomori-social/mastodon:mementomods-2026-06-19
      # image: docker.io/tootsuite/mastodon:latest
      name: web
      livenessProbe:
        exec:
          command:
            - /bin/sh
            - -c
            - "curl -s --noproxy localhost localhost:3000/health | grep -q 'OK' || exit 1"
        initialDelaySeconds: 60
        periodSeconds: 15
        timeoutSeconds: 5
      resources: {}
      securityContext:
        capabilities:
          drop:
            - CAP_MKNOD
            - CAP_AUDIT_WRITE
      volumeMounts:
        - mountPath: /mastodon/public/system
          name: mastodon-vol-pubsys-pvc
  restartPolicy: Never
  # Volumes for the various data you need persistent.
  volumes:
    - name: mastodon-vol-db-pvc
      persistentVolumeClaim:
        claimName: mastodon-db
    - name: mastodon-vol-valkey-pvc
      persistentVolumeClaim:
        claimName: mastodon-valkey
    - name: mastodon-vol-pubsys-pvc
      persistentVolumeClaim:
        claimName: mastodon-pubsys
EOF

That's quite bit of config file. Not everything is absolutely needed, but let's start with this. All the configs you better tune to your liking, like post lenght, vote item amounts etc. stuff people do change.

I'll explain here things you need to change, marked with XXX.

Tunables you need to change

These values are the ones you need to change in mastodon-pod.yaml file.

  • This is your hosts name wher you run podman.
    LOCAL_DOMAIN
  • Just put any password here, it's for database connection
    POSTGRES_PASSWORD for postgresql database
    DB_PASS for mastodon to connect to database
  • These are keys needed to be generated.
    I'll describe later how to do these with rails secret.
    SECRET_KEY_BASE
    OTP_SECRET
  • These secrets we later generate with: bundle exec rake db:encryption:init
    ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
    ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
    ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
  • These secrets we later generate with: rake mastodon:webpush:generate_vapid_key.
    VAPID_PRIVATE_KEY
    VAPID_PUBLIC_KEY
  • If you wish to try mail sending, fill these with your mail providers info.
    SMTP Mail sending config

Running Traefik for reverse proxy

Traefic routers view with mastodon entrypoints

Traefik was new to me, so I wanted to learn it. I configured it to listen to 8443 and 8081 ports on my host for web/https traffic. It then listens to podman socket to find mastodon containers to forward the traffic to.

It is fully capable of maintaining you letsencrypt or static certs for https. I left it out here to simplify the setup. But you'll find tons of instructions how to get that done if you wish to make this public to internet.

Traefik listens to podman socket for finding the ports to be served. You need to start podman service to enable traefik to get the info:

systemctl --user enable --now podman.socket

Start traefik with this command:

podman run -d \
  --replace \
  --name=traefik \
  --net podman \
  --security-opt label=type:container_runtime_t \
  -v /run/user/${UID}/podman/podman.sock:/var/run/docker.sock:z \
  -p 8081:80 \
  -p 8443:443 \
  -p 8080:8080 \
  -l=traefik.http.routers.traefik.entrypoints=websecure \
  -l=traefik.enable=true \
  -l='traefik.http.routers.traefik.rule=(Host(`homeserver.localnet`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`)))' \
  -l="traefik.http.routers.traefik.service=api@internal" \
  -l="traefik.http.routers.traefik.entrypoints=websecure" \
  -l="traefik.http.routers.traefik.middlewares=dashboardauth" \
  -l="traefik.http.middlewares.dashboardauth.basicauth.users=admin:XXX_admin_passwd" \
  -l='traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)' \
  -l="traefik.http.routers.http-catchall.entrypoints=web" \
  -l="traefik.http.routers.http-catchall.middlewares=redirect-to-https" \
  -l="traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" \
  docker.io/library/traefik:latest \
  --api.dashboard=true \
  --api.insecure=true \
  --entrypoints.web.address=":80" \
  --entrypoints.websecure.address=":443" \
  --providers.docker=true \
  --providers.docker.exposedbydefault=false

This does the following:

  • Listen to 8081 (http) and 8443 (https) for incoming traffic
  • Listen to 8080 for admin dashboard
  • Allows anyone login to admin dashboard with no credentials asked. Never run it like this while connected to internet.
  • Listens to podman for open ports

Start mastodon containers for initialization

Mastodon and traefik pods in fedora web console (cockpit)

Remember there was mention we configure secrets later. Now its the time.

  1. Start the containers in pod:
podman kube play mastodon-pod.yaml
  1. Generate the secrets
    Check from the above tunables section where to use rails secret, this is how you run it:
podman exec -ti -e "RAILS_ENV=production" mastodon-web bundle exec rails secret

The similar with encryption init:

podman exec -ti -e "RAILS_ENV=production" mastodon-web bundle exec rake db:encryption:init

Similarly for the ones

  1. Initialize the database
podman exec -ti -e "RAILS_ENV=production" mastodon-web bundle exec rails db:setup
podman exec -ti -e "RAILS_ENV=production" mastodon-web bundle exec rails db:migrate
  1. Create the admin user
podman exec -ti -e "RAILS_ENV=production" \\n  mastodon-web bin/tootctl accounts create \
  admin --email admin@example.com --confirmed --role Owner
podman exec -ti -e "RAILS_ENV=production" mastodon-web bin/tootctl accounts modify admin --approve
  1. Initialization done. Stop and remove the containers. Data will remain in the volumes.
podman pod rm mastodon -f 

Start the mastodon

Now we are done with all the configs. It's time to start the mastodon processes for real. First we comment out the mastodon web sleep loop, and replace it with a real process. We just used this container in sleep state to run the initialization commands.

Change the hastag in mastodon-pod.yaml file from beginning of the higher line to the beginning of the lower line:

# "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000",
"while true; do foo; sleep 10; done"

So it looks like this:

"rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000",
# "while true; do foo; sleep 10; done"

And start the mastodon:

podman kube play mastodon-pod.yaml

It should be running now! If not, patches to guide welcome 😁

Accessing the services

Mastodon with BirdUI should be available at: https://mastodon.homeserver.localnet:8443 and traefik proxy dashboard should be at: https://mastodon.homeserver.localnet:8080

For experimenting

If you want to modify the configurations, change versions or otherwise stop and start the services, edit the podman-mastodon.yaml and do:

podman pod rm mastodon -f && podman kube play mastodon-pod.yaml

If you want to wipe it all and start from scratch, do:

podman pod rm mastodon -f && podman volume rm mastodon-db mastodon-redis mastodon-pubsys

Final note

Remember this is not for production. It is for fun nerding, evaluating and perhaps developing.

My setup is not reachable from internet. However podman is excellent tool to run it in production. For such case I'd convert the mastodon-pod.yaml into quadlet setup with systemd. Podman 6.0 has a command to do that. Also you'd need to secure the traefik and enable certs. Also mastodon configs should be revaluated and secured. And mastodon container amounts tuned to handle the traffic and user load.

For any further questions about BirdUI, contact Rolle, and for mastodon, well the community. The podman guide I can update if you find errors.

Thanks

I used the kube format so I can try it also one day in OpenShift kube if there are enough rainy days. Also as there was nice example in internet which I just copied and started with. Also credits to this traefik blog. Also thanks for Michael for sending the PR for building containers of BirdUI. And Naturally Rolle who created the BirdUI and the whole mementomori thingie.

Happy nerding and good luck,
ikkeT

Last updated 3 hours ago by IkkeT

On this page