[READ-ONLY] Mirror of https://github.com/ewanc26/nix-starter. a starter NixOS configuration
nix nixos template
README.md

server #

A combined NixOS server configuration running an AT Protocol Personal Data Server (PDS) and a Mastodon instance on the same machine, exposed to the internet via a Cloudflare Tunnel.


What's included #

  • PDS (modules/pds.nix) — the official bluesky-pds daemon, proxied by nginx
  • Mastodon (modules/mastodon.nix) — ActivityPub-compatible microblogging, nginx vhost managed by the NixOS module
  • Cloudflare Tunnel (modules/cloudflare-tunnel.nix) — single outbound tunnel routing both hostnames; no inbound ports beyond SSH
  • nginx — shared reverse proxy; serves plain HTTP on localhost; Cloudflare terminates TLS at the edge
  • SSH — hardened, key-only
  • fail2ban — bans IPs that hammer SSH
  • Firewall — port 22 only (no 80/443 needed server-side)

File layout #

hosts/server/
├── default.nix                  # host skeleton — boot, network, user, SSH, nginx, nix
├── hardware-configuration.nix   # generated from your hardware — replace the stub
├── README.md                    # this file
└── modules/
    ├── cloudflare-tunnel.nix    # cloudflared service + ingress rules for both hostnames
    ├── pds.nix                  # bluesky-pds + nginx vhost
    └── mastodon.nix             # mastodon + nginx vhost overrides

Prerequisites #

Before you start you need:

  • A server or VPS with a public IP (any cloud provider)
  • A domain managed by Cloudflare (free plan is fine)
  • Two DNS hostnames pointing at the same server — these will become CNAMEs to the tunnel (see Cloudflare Tunnel setup below)
  • An SSH key pair on your local machine (ssh-keygen -t ed25519 if you don't have one)
  • cloudflared on your local machine:
    # macOS
    brew install cloudflared
    # NixOS / Linux
    nix-shell -p cloudflared
    

Cloudflare Tunnel setup #

Do this on your local machine before deploying. You need your domain on Cloudflare first.

1. Authenticate #

cloudflared tunnel login

A browser window opens. Select the domain you want to use.

2. Create the tunnel #

cloudflared tunnel create server

Note the UUID printed — you'll put it in modules/cloudflare-tunnel.nix as tunnelId. The credentials JSON is saved to ~/.cloudflared/<uuid>.json.

3. Add DNS records in Cloudflare #

In the Cloudflare dashboard for your domain, add two CNAMEs:

Type Name Target Proxy status
CNAME pds <uuid>.cfargotunnel.com Proxied
CNAME social <uuid>.cfargotunnel.com Proxied

Replace pds and social with whatever subdomains match your chosen hostnames.

4. Set SSL/TLS mode #

In the Cloudflare dashboard: SSL/TLS → Overview → Full (not "Full (strict)").

nginx serves plain HTTP on localhost; Cloudflare handles TLS at the edge. "Full (strict)" would fail because there is no valid cert server-side.

5. Update the configuration #

In modules/cloudflare-tunnel.nix, set:

tunnelId        = "<your-tunnel-uuid>";
pdsHostname      = "pds.example.com";     # your actual PDS domain
mastodonHostname = "social.example.com";  # your actual Mastodon domain

Also update the matching hostname values in modules/pds.nix and modules/mastodon.nix — all three must agree.


First-time installation #

1. Install NixOS on the server #

Boot from the Minimal ISO and verify connectivity:

ping nixos.org

Partition and format the disk (replace vda with your disk from lsblk):

sudo parted /dev/vda -- mklabel gpt
sudo parted /dev/vda -- mkpart root ext4 512MB 100%
sudo parted /dev/vda -- mkpart ESP fat32 1MB 512MB
sudo parted /dev/vda -- set 2 esp on

sudo mkfs.ext4 -L nixos /dev/vda1
sudo mkfs.fat -F 32 -n boot /dev/vda2

Recommended — add a second disk for /srv. All service data (PDS repository, Mastodon media and database, secrets) is stored under /srv. Keeping it on a separate volume means you can resize, snapshot, or migrate it without touching the OS disk, and a full-disk event on one side won't take down the other.

If your provider lets you attach a second disk (e.g. /dev/vdb), format it before mounting:

sudo mkfs.ext4 -L data /dev/vdb

Then uncomment the fileSystems."/srv" block in hosts/server/hardware-configuration.nix and update the device label if needed. On a single-disk setup the paths still work — /srv will just be a directory on the root filesystem.

Mount:

sudo mount /dev/disk/by-label/nixos /mnt
sudo mkdir -p /mnt/boot /mnt/srv
sudo mount -o umask=077 /dev/disk/by-label/boot /mnt/boot
# If you formatted a second disk:
sudo mount /dev/disk/by-label/data /mnt/srv

Generate hardware config:

sudo nixos-generate-config --root /mnt

2. Clone this repo #

nix-shell -p git
git clone https://github.com/ewanc26/nix-starter.git /mnt/etc/nixos

3. Copy the hardware config #

cp /mnt/etc/nixos/hardware-configuration.nix \
   /mnt/etc/nixos/hosts/server/hardware-configuration.nix

4. Edit the configuration #

There are four places to fill in your details. Open each file and update the values marked TODO.

hosts/server/default.nix:

adminUser = "yourname";

And replace the placeholder SSH key:

openssh.authorizedKeys.keys = [
  "ssh-ed25519 AAAA... you@yourhost"
];

Your public key is in ~/.ssh/id_ed25519.pub on your local machine.

hosts/server/modules/cloudflare-tunnel.nix:

tunnelId         = "<your-tunnel-uuid>";
pdsHostname      = "pds.example.com";
mastodonHostname = "social.example.com";

hosts/server/modules/pds.nix:

hostname   = "pds.example.com";
adminEmail = "you@example.com";

hosts/server/modules/mastodon.nix:

hostname = "social.example.com";

And your SMTP details (the config defaults to Resend — sign up, create an API key, and put it in the smtp-password secret file):

smtp = {
  host        = "smtp.resend.com";
  port        = 587;
  user        = "resend";
  fromAddress = "notifications@social.example.com";
};

5. Install #

sudo nixos-install --flake /mnt/etc/nixos#server --root /mnt

Set a root password when prompted, then set your user password:

sudo nixos-enter --root /mnt
passwd yourname
exit

6. Reboot #

sudo reboot

SSH in after reboot:

ssh yourname@<server-ip>

Secrets #

Neither service will start until their secret files exist on the server. Create them after first boot, before starting anything.

PDS secrets #

The PDS needs three values that must not be committed to git.

Generate them:

openssl rand --hex 16   # PDS_JWT_SECRET
openssl rand --hex 16   # PDS_ADMIN_PASSWORD
openssl ecparam -name secp256k1 -genkey -noout \
  | openssl ec -text -noout 2>/dev/null \
  | grep priv -A 3 \
  | tail -3 \
  | tr -d ' \n:' \
  | sed 's/^00//'       # PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX

Create the env file:

sudo mkdir -p /var/lib/pds
sudo nano /var/lib/pds/pds.env

Contents:

PDS_JWT_SECRET=<output of first command>
PDS_ADMIN_PASSWORD=<output of second command>
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=<output of third command>

Lock it down:

sudo chown bluesky-pds:bluesky-pds /var/lib/pds/pds.env
sudo chmod 400 /var/lib/pds/pds.env

Mastodon secrets #

Mastodon needs four secret files plus an SMTP password file.

sudo mkdir -p /var/lib/mastodon/secrets

# Secret key base
sudo sh -c 'openssl rand -hex 64 > /var/lib/mastodon/secrets/secret-key-base'

# VAPID keys for Web Push notifications
nix-shell -p nodejs --run "node -e \"
  const {generateVAPIDKeys} = require('web-push');
  const k = generateVAPIDKeys();
  const fs = require('fs');
  fs.writeFileSync('/var/lib/mastodon/secrets/vapid-private-key', k.privateKey);
  fs.writeFileSync('/var/lib/mastodon/secrets/vapid-public-key',  k.publicKey);
\""

# SMTP password — paste your Resend API key here (one line, no trailing newline)
# Get one from https://resend.com/api-keys
sudo sh -c 'echo -n "re_xxxxxxxxxxxxxxxxxxxx" > /srv/mastodon/secrets/smtp-password'

# Fix ownership and permissions
sudo chown -R mastodon:mastodon /srv/mastodon/secrets
sudo chmod 400 /srv/mastodon/secrets/*

Cloudflare Tunnel credentials #

Copy the credentials JSON from your local machine to the server:

# Create the directory on the server
ssh yourname@<server-ip> \
  "sudo mkdir -p /var/lib/cloudflared && \
   sudo chown cloudflared:cloudflared /var/lib/cloudflared"

# Copy the credentials file
scp ~/.cloudflared/<uuid>.json \
    yourname@<server-ip>:/tmp/<uuid>.json

# Move it into place on the server
ssh yourname@<server-ip> \
  "sudo mv /tmp/<uuid>.json /var/lib/cloudflared/<uuid>.json && \
   sudo chown cloudflared:cloudflared /var/lib/cloudflared/<uuid>.json && \
   sudo chmod 400 /var/lib/cloudflared/<uuid>.json"

Starting the services #

Once all secrets are in place, start everything:

sudo systemctl start bluesky-pds
sudo systemctl start mastodon-web mastodon-sidekiq mastodon-streaming
sudo systemctl start cloudflared-tunnel-<uuid>

Check status:

sudo systemctl status bluesky-pds
sudo systemctl status mastodon-web
sudo systemctl status mastodon-sidekiq
sudo systemctl status mastodon-streaming
sudo systemctl status cloudflared-tunnel-<uuid>

Verify through the tunnel:

curl https://pds.example.com/xrpc/_health
# {"version":"..."}

curl https://social.example.com/api/v1/instance
# JSON with instance info

Creating accounts #

First PDS account #

goat account create \
  --pds-host https://pds.example.com \
  --handle yourhandle.pds.example.com \
  --email you@example.com \
  --invite-code $(goat admin invite-code create \
      --pds-host https://pds.example.com \
      --admin-password <your PDS_ADMIN_PASSWORD>)

First Mastodon admin account #

sudo -u mastodon mastodon-env bundle exec bin/tootctl accounts create \
  youruser \
  --email you@example.com \
  --confirmed \
  --role Owner

Ongoing maintenance #

Applying configuration changes #

After editing any .nix file:

sudo nixos-rebuild switch --flake /etc/nixos#server

Updating packages #

The package versions are pinned to the nixpkgs input. To update:

cd /etc/nixos
sudo nix flake update
sudo nixos-rebuild switch --flake /etc/nixos#server

Rolling back #

sudo nixos-rebuild switch --rollback

Or reboot and select an older generation from the boot menu.

Viewing logs #

sudo journalctl -u bluesky-pds -f
sudo journalctl -u mastodon-web -f
sudo journalctl -u mastodon-sidekiq -f
sudo journalctl -u mastodon-streaming -f
sudo journalctl -u nginx -f
sudo journalctl -u cloudflared-tunnel-<uuid> -f

Backing up #

Everything important lives in /var/lib/pds/ and /var/lib/mastodon/. Back them up regularly.

# PDS — database and secrets
sudo tar -czf pds-backup-$(date +%Y%m%d).tar.gz /var/lib/pds/

# Mastodon — PostgreSQL database
sudo pg_dump mastodon_production | gzip > mastodon-db-$(date +%Y%m%d).sql.gz

# Mastodon — media uploads and secrets
sudo tar -czf mastodon-data-$(date +%Y%m%d).tar.gz \
  /var/lib/mastodon/secrets \
  /var/lib/mastodon/public/system

Losing /var/lib/pds/ means losing all accounts on your PDS and the ability to recover them. Losing the Mastodon database means losing all posts, follows, and account data. Back both up off-server.


Traffic flow #

Browser
  │  HTTPS
  ▼
Cloudflare edge  ←──── cloudflared (outbound from server)
  │  HTTP (Host: pds.example.com or social.example.com)
  ▼
nginx :80 (localhost)
  ├── pds.example.com     → bluesky-pds :3000
  └── social.example.com  → mastodon-web / mastodon-streaming

nginx never sees raw HTTPS; Cloudflare decrypts at the edge and forwards plain HTTP tagged with the original Host header. nginx uses that header to route to the right backend.


Getting help #