As I presented days ago, I have a #Pixelfed instance that I maintain on my own. It has been running and federating for almost a month and I am proud to see how well now behaves with the current load.

In this article I go through the steps I did to bring it up into a #RaspberryPi under #Docker.

0. Overview

A pair months ago I explained how to spawn an Akkoma in a Raspberry Pi 4 under Docker. The high level process is similar: get a machine with docker, run the image and configure it. Here I split the article in the following sections:

  1. Install Docker & docker-compose

  2. Get a Pixelfed docker image

  3. Configure the instance

  4. Start the instance

  5. Setting up the instance

  6. Set up the instance in the Reverse Proxy

  7. Add a SSL certificate

  8. Spawn the first user

  9. Next steps for performance

  10. Wrapping up

  11. References

0.1 Assumptions

1. Install Docker & docker-compose

Docker will allow us to run all the needed infrastructure isolated from the machine itself. It allows us to spin up any infrastructure without having an amount of components installed in our system, having it relatively clean and isolated.

docker-compose is a wrapping tool that makes our live with Docker easier. We'll install both.

1.1. Install Docker

  1. ssh corellia

    ssh xavier@corellia
  2. Download and install Docker

    curl -sSL | sh
  3. Add the current user into the docker group

    sudo usermod -aG docker $USER
  4. Exit the session and ssh in again.

  5. Test that it works:

    docker run hello-world


  6. Get the container ID of this test run. In my case is 9373dee4491c

    docker ps -a


  7. Remove the test container

    docker rm 9373dee4491c 

 1.2. Install docker-compose

  1. First of all, check what is the latest version by navigating to their releases: Releases · docker/compose · GitHub. At the moment of this article, the latest stable release was v2.19.0

  2. Dowload the right binary from their releases into the binaries directory of our system:

    sudo curl -L "$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  3. Give executable permissions to the downloaded file

    sudo chmod +x /usr/local/bin/docker-compose
  4. Test the installation:

    docker-compose --version

    It should print something like Docker Compose version v2.19.0

2. Get a Pixelfed docker image

Here we want to get Pixelfed Docker image. It turns out not to be that straight forward...

  1. The "official" account for pixelfed in Docker Hub does not contain any image published.

  2. There is a thread in Pixelfed's Github discussing about the Docker support since March 2020 and the last post is from December 2022.

So at the end of the road what we have is a bunch of images that you can download from Docker Hub and some also got announced in the Github's thread. I've tried them and they appear to work. Actually, my first published version was based on one of these, but then Pixelfed's version got bumped up and I proceeded to build my own, to be able to include my own customizations.

Therefore, here's your option:

🅰️ You can make use of any of the images already published:

🅱️ You build your own image.

In the case of this article, the base architecture is ARMv8 as it is a Raspberry Pi 4. Sometimes the current images are not really working or are old enough. On top of it, I want to have my own set of logos and a pair of customized images for the public side, so I will go for 🅱️ and create my own image that will work smooth with my needs. If you choose 🅰️, then keep note of the image you'll use and jump to the section 3.2 Change the docker-compose.yml to point to your docker image.

2.1. Get Pixelfed's code

So we're building our image. Great. First of all go and get Pixelfed's code

  1. Move to the home folder. I will serve the code from there.

    cd ~
  2. Clone the Pixelfed's Github repository. It will create a ~/pixelfed folder.

    git clone
  3. Move to this folder.

    cd ~/pixelfed
  4. Now we want to be sure that we stick to a tagged version. Pixelfed is still in development mode, so the main branch is actually dev and contains lot of late merged commits. Sticking to a tagged version helps us to work with a kinda stable state. Check the Releases to find out what is the latest version. At the moment of the article it was the 0.11.8. We checkout to this tag:

    git checkout v0.11.8

So now we have the latest version code.

ℹ️ Optional: Customize your instance

Now is the moment to be curious with the codebase. While the majority of the texts are managed by the Admin panel, the statics are compiled. This means that if you want to change the logos, favicon and etc, you will notice no change once you're up and running.

If you want to change them I recommend to change the following binaries:

 2.2. Build a Docker image

Now you have the codebase ready to be packed in a Docker image. Let's build the image.

Images should be created already with a local tag, so that later on we can recall on them. I use project:date as a tag for my local images.

  1. Be sure you're in the project's directory

    cd ~/pixelfed
  2. Run the build command defining a tag and pulling from the oficial Dockerfile:

    docker build . -t pixelfed:20230621 -f contrib/docker/Dockerfile.apache

It will take a while. At the end of the process we'll have in our host a new image ready to be used. To see the available images in our host just run:

docker images


What would be next? You may want to store your image outside of your host, in case this machine burns down and you need quickly a Docker image to spin up another instance. If so, you can create a new user in Docker Hub and then check my previous article moving directly to the section 3. Push the image Build a new Docker image for a Pixelfed instance |

Still, you can simply work with the local image. In this article I will continue with it as it is the simpler.

3. Configure the instance

Before we spawn our instance, we need to prepare it with some configurations.

3.1. Create & populate an .env.docker file

Pixelfed will take a big portion of the parameters from the .env.docker dot-env file. You can easily start with coping from the .env.example that it's already distributed in the root of the repo, but I like to add the parameters as I need / understand them. So, I'll go through each section I split and explain them shortly. You can find all the parameters in the official documentation.

Not all the following sections are needed, and also not all the parameters in the sections are needed. Pixelfed can work with a smaller subset of them, but this is how I am running my instance.

For example, you'd want to discard the sections 3.1.5 Logging and 3.1.6 Mailing if you go really minimal.

3.1.1. General settings

## General Settings
INSTANCE_DESCRIPTION="LaDragonera és una instància PixelFed en català i privada."

Here we define the basics of the instance: Name, URL, Domain, decription... Still, there are a couple of parameters interesting:

  • ENABLE_CONFIG_CACHE: It comes as False by default. Once you have your instance up and running, you may want to have some parameters configurable from the Administration Panel. This fact needs to change this value to True. By now, you can simply ommit this parameter and ad it later.

  • APP_LOCALE: My instance is intended to be publishing in Catalan, so I set here the language.

  • APP_FALLBACK_LOCALE: No catalan? then english.

3.1.2. General infrastructure

## Databases (MySQL)
# pass the same values to the db itself

## Cache (Redis)

Just a typical set of parameters for the app to connect to the DB and Redis servers. Keep in mind to set the same password for DB_PASSWORD and MYSQL_PASSWORD

3.1.3. Activity Pub federation

## ActivityPub Federation

This parameters should allow our instance to be able to federate with the rest of the fediverse.

3.1.4. Other parameters

## Other

Here I just put together a bunch of parameters. I want my hashtags public, I don't want to support stories, my timezone, the registrations are closed, the Image driver that my Linux (the one in the Docker system) has, I want to play with custom emojis and that the instance has a public discover section with some of my posts, and the most important, the length of the descriptions should be 1000 characters.

3.1.5. Logging

## Logging

I found useful to have these in and be able to debug the instance.

3.1.6. Mailing

## Mail Configuration (Post-Installer)
MAIL_FROM_NAME="Pixelfed LaDragonera

I set up a personal email in google and use the Google mailing ability to let the Pixelfed send emails. To be honest, it sends very few.

3.2. Change the docker-compose.yml to point to your docker image

One last thing before starting our instance: to change the Docker image that is referenced in the docker-compose.yml. If we don't do this step, Docker will complain stating that he can't find any image.

What we do is to replace both references (in the services > app and services > worker), from image: pixelfed/pixelfed:latest to image: pixelfed:20230621, which is the tag that we created for out local image.

4. Start the instance

Now it's the moment to start our instance.

docker-compose up -d


5. Setting up the instance

At this point, we should have the docker containers running. There is a bunch of actions that we need to do. In the official documentation they're called one-time setup tasks. Just keep in mind that we run them inside the Docker containers, so the commands are a bit different:

docker-compose exec app php artisan key:generate
docker-compose exec app php artisan storage:link
docker-compose exec app php artisan migrate --force
docker-compose exec app php artisan import:cities
docker-compose exec app php artisan instance:actor
docker-compose exec app php artisan passport:keys
docker-compose exec app php artisan route:cache
docker-compose exec app php artisan view:cache
docker-compose exec app php artisan config:cache

You can learn more about each command in the official explanation, but still:

  • key:generate will generate an app key and automatically add it into the .env.docker file.

  • storage:link links the storage with the app.

  • migrate --force runs all migrations, these are the files that generates and maintains the DB structure and the direct relation of it with the app along the versions.

  • import:cities will populate the locations that will be available to choose when we select the location in a new post.

  • instance:actor starts the ActivityPub federation

  • passport:keys creates the encryption keys for the API authentication, so we can use external apps.

  • route:cache and view:cache cleans the cache related to routes and templates. The documentation mentions that they should be cleaned in every code change.

  • config:cache cleans the cache related to the config. According to the docs, it should run after any change in the .env.docker file.

After this, we still need to set up the Job Queuing. There is a service worker that is meant to run maintenance work. This runs through Laravel Horizon, and it even shows a proper panel within the Administration panel. To activate it we need to run the following commands:

docker-compose exec worker php artisan horizon:install
docker-compose exec worker php artisan horizon:publish
docker-compose restart worker

You surely have noted that here we're using the container worker and not app.

6. Set up the instance in the Reverse Proxy

At this point our instance should be ready to accept requests, but nothing comes. We need to route traffic to the instance, and as I mentioned at the beginning, I use another Raspberry Pi to run a Reverse Proxy under Apache2. Therefor, I only need to create a new virtual host for the Pixelfed:

  1. SSH into dagobah:

    ssh dagobah
  2. Move yourself into the available sites directory of Apache

    cd /etc/apache2/sites-available
  3. Create a new virtual host file:

  4. Edit the file:

  5. Fill the file with the following code:

    <VirtualHost *:80>
      ProxyPreserveHost On
      ProxyPass / http://corellia:8080/ nocanon
      ProxyPassReverse / http://corellia:8080/
      ProxyRequests Off
    <VirtualHost *:443>
      ProxyPreserveHost On
      ProxyPass / http://corellia:8080/ nocanon
      ProxyPassReverse / http://corellia:8080/
      ProxyRequests Off
      ProxyPassReverseCookiePath / /
      RequestHeader set X_FORWARDED_PROTO 'https'

    Some notes about this configuration:

    • We intend to generate an SSL certificate in the next section. This needs a virtual host over the port 80 and nothing else. But I know that the certification app (certbot) likes to create a separate file for the SSL virtual host, while I prefer all virtual hosts related to the same app to exist in the same file. That's why I actually create both virtual hosts but I still do not redirect the 80 to the 443 to please certbot afterwards.

    • You would notice that I am relying to my local DNS, so I use corellia and not the IP where the Raspberry allocates the Pixelfed

    • Also you'd notice that Pixelfed is listening the port 8080.

  6. Save (ctrl + o) and exit (ctrl + x)

  7. Restart the reverse proxy

    sudo service apache2 restart

At this point you should be able to view the instance without SSL.

7. Add a SSL certificate

Here I will assume that we already had the certbot installed and working previously, as explained in the previous article Set up a Reverse Proxy in a Raspberry Pi with Apache | Otherwise, just go there and follow the section 4. Set up the SSL handling in the reverse proxy host, at least the initial steps.

I also assume that we have already the domain pointing to this host and the DNSs propagated.

What we have to do here is to request a certificate for our new domain:

sudo certbot --agree-tos -d --apache

It should perform a challenge to ensure we're owning the domain, and afterwards it should update the virtual hosts that we created in the previous section:

<VirtualHost *:80>

    ProxyPreserveHost On
    ProxyPass / http://corellia:8080/ nocanon
    ProxyPassReverse / http://corellia:8080/
    ProxyRequests Off

    RewriteEngine on
    RewriteCond %{SERVER_NAME}
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
<VirtualHost *:443>

    ProxyPreserveHost On
    ProxyPass / http://corellia:8080/ nocanon
    ProxyPassReverse / http://corellia:8080/
    ProxyRequests Off

    ProxyPassReverseCookiePath / /
    RequestHeader set X_FORWARDED_PROTO 'https'

    SSLCertificateFile /etc/letsencrypt/live/
    SSLCertificateKeyFile /etc/letsencrypt/live/
    Include /etc/letsencrypt/options-ssl-apache.conf

8. Spawn the first user

Finally, we have the instance up and running, and the traffic directed to the host via the domain and the reverse proxy.

Now we want to have our first user, which most likely will be the admin user. So we do.

Assuming that we're ssh-ed into the machine and in the project's directory:

docker-compose exec app php artisan user:create

This will trigger a questions/answers process to create a new user. Just remember to set verification = yes to avoid sending verification mail, and if we want this user to be Admin, select the Make Admin option.

🚀At this point we should be able to visit our instance and log in!

9. Next steps for performance

You can call it a day and start with it. If you meant this instance for a single-user mode, the performance is ok-ish and shows peaks of heavy load when a post gets reblogged by a popular user.

If you tail -f the logs (which here is more a docker-compose logs -f app) you'll notice that when it starts to heavily federate or an user interacts with your instance, the network i/o goes high, the CPUs as well, and the fear of heavy swapping over the poor Micro-SD starts.

I mitigated this by doing two actions:

  1. Move the whole filesystem from the Micro-SD to a SSD SATA III with a cable adapter SATA III to USB3. I explained this very straight forward process in an article in catalan here: Moure Raspberry Pi OS de Micro-SD a USB |

  2. Serve all images from a CDN, set up in an Object Storage in Spaces from DigitalOcean. I also explained this process in catalan here, even done for a Mastodon instance, it's even easier here as Pixelfed supports Spaces directly: Spaces de DigitalOcean com a Object Storage per a Mastodon |

This both actions reduced the load dramatically from the Raspberry Pi, making it a pleasure to play with and now it is my main Pixelfed instance 😊

10. Wraping up

As we saw with Akkoma in a Raspberry Pi 4 under Docker |, there are 4 main actions in order to run a fediverse instance in a Raspberry Pi 4 with Docker:

1️⃣ Have a Raspberry Pi 4 up and running

2️⃣ Get the instance code and generate an image

3️⃣ Perform the post-install actions to settle the instance

4️⃣ Install / adjust a Reverse Proxy to bring traffic to the instance

With Pixelfed has been the same, and also was the same experience with a Mastodon instance (a post that I still need to publish).

I hope this will help anyone that is willing to build its own instance! 💪🏼

11. References

Previous Post Next Post