itshalfempty

content pending


I’ve spun up my own Mastodon instance over at social.itshalfempty.com. Mastodon is an open source, distributed social micro-blogging platform (or more simply - a decentralized Twitter alternative). Even without the social component - I’m looking at it as a way to more quickly and easily share quick updates about me or my life without putting in the effort write a longer blog post.

I really wanted my handle to be @alex@itshalfempty.com - which unfortunately means I needed to run my own instance. Because I already have content hosted at itshalfempty.com, there were only two options here - either using a hosted solution that allows for changing the WEB_DOMAIN of the instance (like masto.host), or hosting it on my own.

There was a also middle-of-the-road approach I tried but decided I didn’t like - which was to make my account discoverable by searching for @alex@itshalfempty.com. This was pretty easy to set up by serving or redirecting requests to itshalfempty.com/.well-known/webfinger to the mastodon instance where my account is located (like written about here). I was able to get that working pretty easily, but it only aids discoverability and doesn’t allow me to actually use my preferred handle.

So, because I am already familiar with AWS and want to have control over my data, I decided to spin up my own instance on infrastructure I manage.

The plan

Mastodon requires a Postgres database, a redis database, and some place to dump (and serve) files. On the whole, that’s pretty straightforward. Many people have walked this route before, but there were a few resources I used in particular:

  • This write-up from Micah Walter. I wanted to run everything on a single EC2 instance, and he confirmed it was possible and picked an instance size that would work. He also shared costs which sounded reasonable.
  • A terraform template from @josephgruber@josephgruber.space. I didn’t use too much of this, but it was a helpful reference.
  • This very old docker guide, though in the end I think this ended up hurting more than it helped.

My plan was to run a single EC2 instance with an attached EBS volume, and run everything on it using docker. All of the relevant persistent data (from Postgres, redis, etc.) could be stored on the EBS volume. I’d use S3 for file storage and serve assets using Cloudfront. I doubt I’ll ever need more than this for an instance that I only I use.

Webfinger

I use Cloudfront to serve itshalfempty.com. In order to host my instance at social.itshalfempty.com but have my handles as itshalfempty.com, I needed to configure Cloudfront to serve webfingers from my instance. Here’s the relevant Terraform configuration I used.

resource "aws_cloudfront_distribution" "site" {
   # This is only a partial config for a Cloudfront distribution for itshalfempty.com

   origin {
    domain_name = "social.itshalfempty.com"
    origin_id = "mastodon-webfinger"

    custom_origin_config {
      http_port = 80
      https_port = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols = [ "TLSv1.2" ]
    }
  }

  ordered_cache_behavior {
    path_pattern = "/.well-known/webfinger"
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "mastodon-webfinger"

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"
      }
    }

    min_ttl = 300
    default_ttl = 3600
    max_ttl = 86400
    compress = true
    viewer_protocol_policy = "redirect-to-https"
  }
}

The EC2 instance

This was the most annoying part of the setup. While it’s easy enough to install docker and spin up mastodon - configuring the initial install required me logging on to the machine to run some commands and edit configuration files.

I set up a user data script that runs when the instance is created. This script:

  1. Updates and upgrades packages
  2. Mounts the EBS volume
  3. Configures unattended upgrades
  4. Installs Docker and Docker compose
  5. Points /etc/letsencrypt to a directoy on the EBS volume
  6. Points /etc/nginx/sites-enabled/${instance_domain} to a file on the EBS volume. I had to hand roll this file (though certbot will modify it later)
  7. Installs nginx + certbot, and runs certbot
  8. Runs docker compose up -d

The initial nginx configuration for the site was pretty simple. This gets modified by certbot later to support (and redirect to) HTTPS.

server {
    server_name social.itshalfempty.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Steps 5-8 of the script are:

######
# Install & configure nginx
######

ln -s $MOUNT_POINT/letsencrypt /etc/letsencrypt

apt install nginx -y
snap install --classic certbot

sudo ln -s $MOUNT_POINT/nginx/${instance_domain} /etc/nginx/sites-available/${instance_domain} 
sudo ln -s /etc/nginx/sites-available/${instance_domain} /etc/nginx/sites-enabled/

systemctl restart nginx
certbot --nginx --agree-tos --email ${admin_email} --domains ${instance_domain} -n

######
# Run Mastodon
######
cd $MOUNT_POINT/mastodon
docker compose up -d

This works now that the volume is populated, but initially I had to log on to the server to configure Mastodon, run migrations, and set up the initial admin user. I’m using the docker-compose.yml file from the Mastodon repo, which was fortunately was runnable without any modifications.

To generate the initial .env.production configuration, I ran docker compose run --rm web rake mastodon:setup. This was an interactive setup that produced the configuration settings for the server – though I did also need to add WEB_DOMAIN. My setup failed creating the first user because I entered my SMTP settings incorrectly. To recover, I just went through the “forgot password” flow in the running site, and then ran admin CLI commands using docker compose run --rm web <command> to approve the account and change the role to owner.

For future releases, I’ll need to update the pinned version in the docker-compose.yml and possibly run database migrations.