How I deployed treff.tech

I've already written about why treff.tech exists. This time, I want to go a level deeper and walk through how it's actually deployed. The infrastructure, not the idea.

The setup: Terraform provisions the server, Ansible configures it, and Mobilizon (the software behind treff.tech) runs in Docker. Nothing exotic, but I think there's value in showing how the pieces fit together, especially for anyone considering self-hosting a Fediverse project of their own.

The stack at a glance

├── ansible/
│   ├── group_vars/
│   ├── inventory/
│   ├── playbook.yml
│   └── roles/
│       ├── docker/
│       └── security/
├── Makefile
└── terraform/
    ├── main.tf
    ├── outputs.tf
    ├── terraform.tfvars.dev
    └── variables.tf

Terraform handles the "what exists" the server itself and its networking. Ansible handles "what's on it" Docker, hardening, and the running services. A Makefile ties the two together so a full deploy is a single command, not a sequence of steps I have to remember.

Provisioning with Terraform

terraform/main.tf defines the actual infrastructure: the [cloud provider / VPS] instance treff.tech runs on, along with networking and DNS. variables.tf and terraform.tfvars.dev keep environment-specific values (like instance size and region) separate from the logic, so promoting the same setup to a production environment later is mostly a matter of swapping the .tfvars file. outputs.tf exposes things like the server's IP address, which Ansible's inventory then picks up.

Keeping this layer thin was a deliberate choice: Terraform's only job is to make the server exist. Everything about what runs on that server is Ansible's responsibility.

Configuration with Ansible

The ansible/ directory has two roles doing the real work:

docker — installs and configures Docker itself, so the host is ready to run containers. This is where Mobilizon (and its dependencies, like PostgreSQL) actually run.

security — hardens the server: [firewall rules / SSH configuration / fail2ban / automatic security updates — whichever you actually use]. This is a role I'd rather run once and mostly forget about, which is exactly the point of automating it — it runs identically every time, instead of relying on me remembering every step manually.

group_vars/all.yml.dev and inventory/hosts.yml.dev follow the same environment-separation pattern as the Terraform side — dev values are kept apart from what would eventually be production values.

Tying it together with Make

The Makefile wraps the Terraform and Ansible steps into simple commands — plan and apply infrastructure, then run the Ansible playbook against whatever Terraform just created. In practice, a full deploy or update usually comes down to one or two make commands rather than remembering the right terraform and ansible-playbook invocations each time.

Why this stack, and not something else

Mobilizon itself is straightforward to run — it's a Dockerized app, so the actual "install Mobilizon" step is small. The more interesting engineering problem was making the infrastructure reproducible: if the server disappeared tomorrow, I want to be able to stand it back up from these two directories, not from memory.

Terraform and Ansible are also just tools I'm comfortable with — nothing about treff.tech needed something more elaborate like Kubernetes. For a single-instance Fediverse platform, that would have been over-engineering for its own sake.

Takeaways

A few things I'd tell someone setting up something similar: