Running a Mastodon instance entirely free forever

My single-user Mastodon instance has been ticking away at phocks.eu.org for a while now, over a year at least. All up, I've paid zero dollars to keep it running. I've had a few people ask me to write up something about it, so here it is.

If you're comfortable logging into a Linux server via SSH and running commands you shouldn't have any major troubles setting it up, but it will take a few hours of work. Enjoy!

Firstly though, why would anyone even want to run their own Mastodon instance? Well, for the fun of mucking around with servers and software, perhaps? Maybe for the freedom to post whatever you want on your own platform and no dumb billionaire can suspend you or force you to take it down? Who knows? Anyway...

To run a Mastodon instance you need 2 things.

  1. A domain, or subdomain.
  2. A server, 2GB RAM minimum. 50GB disk space minimum.

Getting a proper domain name for free is more art than science these days, especially since Freenom shut down new registrations. Here are some other places to try.

  1. nic.eu.org - They make you jump through a few hoops, plus it can take up to a month or two (or forever) to get approved.
  2. nic.ua - pp.ua domain. 1-year registrations only, but you can renew manually for free each year.

(If you know of any other places to get free domains, please let me know.)

You can also use a subdomain. Subdomains are easier to get for free. Try some of these places or do a search online.

  1. afraid.org
  2. GitHub free-subdomain topic
  3. noip.com
  4. duckdns.org

Or else, if you already own a domain name, you can create a subdomain like mastodon.yourdomain.com or fedi.yourdomain.com or whatever you like.

Now you need a server to point your domain to.

You could use any old computer that you have lying around the house, or you could sign up for the free tier on the Oracle Cloud. This is what I'm using currently.

Google Cloud also offers a free server, but they only give you 30GB storage and 1GB of outbound traffic and Mastodon will chew through this pretty quick. But you could try GoToSocial, Pleroma, or Akkoma instead, which are a bit more lightweight.

(If anyone knows any other free-forever tier VPSs please let me know too.)

I wrote up this little guide a while ago about how to get 2x Oracle Cloud servers free forever. These are x86 servers with 1GB RAM each. 1GB is a bit small for a Mastodon instance, but I can confirm that it does run if you enable some swap space and don't enable Elastic Search. You could even try splitting the database and the web server between the two separate servers, if you're feeling adventurous.

Good news though. Oracle has now added ARM servers to their free tier. These are the Ampere A1 Compute servers. You can create from 1 up to 4 servers with 24GB of RAM spread between them, which is more than enough for a Mastodon instance. And they're free forever too. I am running mine on a 2-thread ARM CPU with 12GB RAM and 50GB block storage.

Oracle gives you up to 200GB of block storage, which you can spread across all your servers. I have had some issues with running low on disk space and having to set up scripts to compress media etc (see below), so I'd probably recommend using 100GB for your Mastodon instance as a minimum. Unfortunately, Mastodon uses a lot of space for storing accounts and media etc. You can hook up object storage later if you want, but it's not necessary for starters.

OK, so now you have your domain and you have it pointed to the IP address of your server (or your home IP address with ports 80 and 443 forwarded to your internal address). Now you need to install Mastodon. Simply follow the instructions over at the official guide to get started. There are quite a few steps involved, but it's not too hard if you take it one step at a time.

If everything went well, you should now have a working Mastodon instance. Log in and start posting!


ps.

Here are some scripts that I run on cron every few hours, mostly to help keep disk usage down.

This first one runs tootctl to prune old accounts and media etc. This may not be necessary these days as I believe Mastodon has some built-in pruning now. But I've been running this for a while and it seems to work.

#!/usr/bin/env bash

export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
export RAILS_ENV=production

# Added from https://ricard.dev/improving-mastodons-disk-usage/
/home/mastodon/live/bin/tootctl accounts prune
#/home/mastodon/live/bin/tootctl media remove --remove-headers --include-follows --days 1
/home/mastodon/live/bin/tootctl media remove --prune-profiles --days 1
/home/mastodon/live/bin/tootctl cache clear
/home/mastodon/live/bin/tootctl statuses remove --days=1
/home/mastodon/live/bin/tootctl media remove --days=1
/home/mastodon/live/bin/tootctl preview_cards remove --days=1
/home/mastodon/live/bin/tootctl media remove-orphans
/home/mastodon/live/bin/tootctl media usage
date

This compresses images in the cache. You'll need to install jpegoptim and pngquant for this to work.

#!/usr/bin/env bash
cd /home/mastodon/live/public/system/cache
find -name '*.jpg' -print0 | xargs -0 jpegoptim --preserve --threshold=1 --max=45
find -name '*.jpeg' -print0 | xargs -0 jpegoptim --preserve --threshold=1 --max=45

cd /home/mastodon/live/public/system/cache/accounts
find -name '*.png' -print0 | xargs -0 pngquant --ext=.png --force --speed 10 --quality 45-50 --skip-if-larger

cd /home/mastodon/live/public/system/cache/media_attachments
find -name '*.png' -print0 | xargs -0 pngquant --ext=.png --force --speed 10 --quality 45-50 --skip-if-larger

cd /home/mastodon/live/public/system/cache/preview_cards
find -name '*.png' -print0 | xargs -0 pngquant --ext=.png --force --speed 10 --quality 45-50 --skip-if-larger

# Can't really use gif compression as you can't ignore on bigger file size etc
#find -name '*.gif' -print0 | xargs -0 gifsicle -O3 --colors=64 --use-col=web --lossy=100 --batch

Aaaaaaaand... there you have it. Contact me on Mastodon at @josh if you have any questions or need any help, or if you just wanna say hi 👋


pps.

I forgot to mention, the only other thing that you kinda might need during the install is an SMTP service for sending emails for sign-ups. I'm pretty sure it's not 100% necessary for a single-user instance, because the installation will set up an admin account for you and you can also use the Mastodon cli tools to create accounts without email confirmation.

But if you want to be able to send emails, you can use Mailgun for free. You can also try SendGrid, but I believe they require you to verify your identity with a credit card.

You can also try setting up SMTP through Gmail, as suggested by KuJoe.

Happy days!


ppps.

Thanks to @futzle for pointing out that if you don't want literal Nazis and 4chan-like trolls posting the worst stuff on the Internet at you (I found out the hard way), you should probably import a blocklist to block the worst of the worst instances in the fediverse. Here's a good one here called FediNuke. You can import the text file in admin settings under Moderation -> Federation.


UPDATE 2024-03-03: Unfortunately Oracle's free-tier services locked me out of my account without warning, with no way to contact a human for support :(

The steps above will still work, but I would recommend you run your Mastodon instance on your own hardware in-house, or try Google's free tier (though it probably won't have enough storage — maybe try GoToSocial), otherwise (and yes this is not free forever but hey whatever) try to find a cheap VPS on LowEndBox.

Good luck!


If you found this useful, please consider buying me a coffee, as unfortunately coffee isn't free 🥲 ☕

Test early test often: writing tests in Rust is super easy and (kinda) fun!

It's extremely tempting to simply not write tests.

You could forever keep saying "I'll learn how to write tests later" or "Any errors will just show up in production and I'll just catch them there" ... aaaaand I've fallen into that trap many times.

This time let's tackle testing in Rust head-on, up front!

Tests are super simple. Tests make your code better, and they make you a better coder.

Simply put, tests in Rust are functions annotated with #[test] which are run using cargo test. If the function panics, the test fails. If it doesn't panic, the test passes. That's it.

We can use assert! to check if something is true (it will cause the function to panic if the expression is false).

Here's a test that passes.

#[test]
fn it_works() {
    assert!(1 == 1);
}

And here's a test that fails.

#[test]
fn it_works() {
    assert!(1 == 2);
}

We can also use assert_eq! to check if two things are equal.

#[test]
fn it_works() {
    assert_eq!(1, 1);
}

And we can use assert_ne! to check if two things are not equal.

#[test]
fn it_works() {
    assert_ne!(1, 2);
}

We can put these tests anywhere and cargo test will run them all. It's common however to put all your tests inside a tests module at the bottom of the file. Annotate this module with #[cfg(test)].

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(1, 1);
    }

    #[test]
    fn it_works2() {
        assert_eq!(2, 2);
    }
}

Why would we want to do this?

Well, putting tests in a module like this allows us to encapsulate code and any helper functions or libraries we might need for our tests and make sure they don't end up in our final binary.

For simple test functions that don't bring in other code, it's not strictly necessary. You could even put the tests inline right after the functions you're testing.

Here are some tests in action.

// src/main.rs

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn minus(a: i32, b: i32) -> i32 {
    a - b
}

fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

#[test]
fn it_multiplies() {
    assert_eq!(multiply(2, 2), 4);
}

fn main() {
    let add_result = add(1, 2);
    println!("result: {}", add_result);

    let minus_result = minus(2, 1);
    println!("result: {}", minus_result);

    let multiply_result = multiply(2, 2);
    println!("result: {}", multiply_result);
}

#[cfg(test)]
mod tests {
    // "super" brings parent functions into scope
    // eg. add and minus so we can test them
    use super::*;

    #[test]
    fn it_adds() {
        assert_eq!(add(1, 2), 3);
    }

    #[test]
    fn it_subtracts() {
        assert_eq!(minus(2, 1), 1);
    }
}

Well, there you go. Now you can write tests to make sure a function actually does what you want it to do. Super easy and ... kinda fun!

There's a little bit more to testing in Rust and you can read about it in the Rust Book. Here's a breakdown of some other stuff to think about.

  • #[should_panic] - put this under #[test] if you actually want the test to panic (useful for testing error handling).
  • #[ignore] - put this under #[test] to ignore the test unless you run cargo test -- --ignored (useful for tests that take a long time to run).
  • Integration tests - put tests as separate files in the tests/ directory if you're making a library and want to test how it integrates with other code.
  • Custom failure messages - use assert!(1 == 2, "1 does not equal 2") to provide a custom failure message. ie. additional arguments to assert! will be passed to format! and used as the failure message.
  • Result<(), String> - instead of panicking, you can use Result<(), String> as the return type for your test function and the test will pass if the result is Ok(()) and fail if the result is Err(String).
  • use cargo test -- --nocapture to see the output of println! if your tests pass.
  • choose which tests to run with cargo test it_works or cargo test it_works2.

That's it for now. Good luck out there you beautiful evolving Rustaceans! Go write some amazing tests!

🦀🦀🦀🦀🦀🦀🦀