← Back to Guide Index

Service Recipes: Copy-Pasteable Configurations

This guide provides complete, ready-to-use NixOS configurations for each service. Each recipe includes all necessary configuration, secrets, DNS setup, and verification steps.

Table of Contents

1. Recipe Structure

2. Bluesky PDS

3. Tangled Knot

4. Tangled Spindle

5. Microcosm Spacedust

6. Microcosm Slingshot

7. Lycan Feed Generator

8. Prometheus & Grafana

9. QuickDID

10. Adding Multiple Services

Recipe Structure

Each recipe follows this format:

Service Name
├── Prerequisites (what you need first)
├── Configuration (copy-pasteable Nix code)
├── Secrets Required (what to generate)
├── DNS Records (what to add)
├── Verification (how to test)
└── Troubleshooting (common issues)
}}}

== Bluesky PDS ==

=== Prerequisites ===
- [ ] Caddy configured with working HTTPS
- [ ] Secrets management set up
- [ ] Domain `pds.yourdomain.com` ready
- [ ] Email SMTP account (for password resets)

=== Configuration ===

Add to `/etc/nixos/configuration.nix`:

{ config, pkgs, ... }:

let

domainName = "yourdomain.com";

pdsPort = 2583; # Changed from default 3000 to avoid conflicts

in

{

# Add PDS secrets file

sops.secrets = {

PDSJWTSECRET = {};

PDSADMINPASSWORD = {};

PDSPLCROTATIONKEYK256PRIVATEKEY_HEX = {};

PDSEMAILSMTP_URL = {};

PDSEMAILFROM_ADDRESS = {};

};

sops.templates."pds-env".content = ''

PDSJWTSECRET=${config.sops.placeholder.PDSJWTSECRET}

PDSADMINPASSWORD=${config.sops.placeholder.PDSADMINPASSWORD}

PDSPLCROTATIONKEYK256PRIVATEKEYHEX=${config.sops.placeholder.PDSPLCROTATIONKEYK256PRIVATEKEYHEX}

PDSEMAILSMTPURL=${config.sops.placeholder.PDSEMAILSMTPURL}

PDSEMAILFROMADDRESS=${config.sops.placeholder.PDSEMAILFROMADDRESS}

'';

# Bluesky PDS service

services.bluesky-pds = {

enable = true;

package = pkgs.bluesky-pds;

environmentFiles = [ config.sops.templates."pds-env".path ];

settings = {

PDS_HOSTNAME = "pds.${domainName}";

PDSSERVICEDID = "did:web:pds.${domainName}";

PDS_PORT = pdsPort;

PDSDATADIRECTORY = "/var/lib/pds";

PDSBLOBSTOREDISK_LOCATION = "/var/lib/pds/blocks";

PDSBLOBUPLOAD_LIMIT = "104857600"; # 100MB

PDSDIDPLC_URL = "https://plc.directory";

PDSBSKYAPPVIEWURL = "https://api.bsky.app";

PDSBSKYAPPVIEWDID = "did:web:api.bsky.app";

PDSREPORTSERVICE_URL = "https://mod.bsky.app";

PDS_CRAWLERS = "https://bsky.network,https://api.bsky.app";

PDSINVITEREQUIRED = "true";

PDSRATELIMIT_ENABLED = "true";

LOG_ENABLED = "1";

LOG_LEVEL = "info";

};

};

# Caddy virtual host for PDS (wildcards for user subdomains)

services.caddy.virtualHosts = {

"*.pds.${domainName}, pds.${domainName}" = {

extraConfig = ''

tls {

on_demand

}

reverse_proxy http://127.0.0.1:${toString pdsPort} {

transport http {

read_timeout 0

write_timeout 0

dial_timeout 30s

keepalive 90s

keepaliveidleconns 10

}

}

'';

};

};

}

}}}

Secrets Required

Create /etc/nixos/secrets/bluesky-pds.yaml:

PDS_JWT_SECRET: "$(openssl rand -hex 32)"
PDS_ADMIN_PASSWORD: "$(pwgen -s 32 1)"
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: "$(openssl ecparam -name secp256k1 -genkey -noout | openssl ec -outform DER | tail -c 32 | xxd -p -c 64)"
PDS_EMAIL_SMTP_URL: "smtps://your-email@gmail.com:app-password@smtp.gmail.com:465"
PDS_EMAIL_FROM_ADDRESS: "noreply@yourdomain.com"
}}}

*Important:* Replace with actual generated values.

=== DNS Records ===

pds.yourdomain.com A YOURSERVERIP

*.pds.yourdomain.com A YOURSERVERIP

}}}

Verification

# 1. Check service is running
systemctl status bluesky-pds

# 2. Check health endpoint
curl https://pds.yourdomain.com/xrpc/_health

# 3. Check DID document
curl https://pds.yourdomain.com/.well-known/did.json

# 4. Check admin API (create invite)
# First, get admin password
sudo cat /run/secrets/PDS_ADMIN_PASSWORD

# Then test
curl -X POST \
  -u "admin:YOUR_ADMIN_PASSWORD" \
  https://pds.yourdomain.com/xrpc/com.atproto.server.createInviteCode \
  -H "Content-Type: application/json" \
  -d '{"useCount": 1}'
}}}

=== Troubleshooting ===

*"DID resolution failed"*
- Check DNS: `dig pds.yourdomain.com`
- Verify Caddy is serving `.well-known/did.json`
- Check PDS logs: `journalctl -u bluesky-pds -f`

*"Cannot create account"*
- Verify PLC rotation key is valid hex string
- Check SMTP settings in logs
- Ensure PDS_EMAIL_FROM_ADDRESS matches SMTP account

*"Certificate errors for user subdomains"*
- Verify wildcard DNS record exists: `*.pds.yourdomain.com`
- Check Caddy logs for on-demand TLS failures
- Ensure PDS is responding to `/tls-check` endpoint

---

== Tangled Knot ==

=== Prerequisites ===
- [ ] Caddy configured
- [ ] ATProto DID (from your Bluesky account or PDS)

=== Configuration ===

{ config, pkgs, ... }:

let

domainName = "yourdomain.com";

ownerDid = "did:plc:YOURDIDHERE"; # Get from Bluesky

in

{

services.tangled-knot = {

enable = true;

server = {

hostname = "knot.${domainName}";

owner = ownerDid;

};

openFirewall = true;

};

services.caddy.virtualHosts = {

"knot.${domainName}" = {

extraConfig = ''

route {

@api_limit {

not remote_ip 127.0.0.1

}

ratelimit @apilimit {

rate 10r/s

burst 20

}

reverse_proxy http://127.0.0.1:5555

}

'';

};

};

}

}}}

DNS Records

knot.yourdomain.com       A     YOUR_SERVER_IP
}}}

=== Verification ===

# Check service

systemctl status tangled-knot

# Clone test repo (from your local machine)

git clone https://knot.yourdomain.com/your-did/test-repo

# Check git protocol works

curl -I https://knot.yourdomain.com

}}}

---

Tangled Spindle

Prerequisites

Configuration

{ config, pkgs, ... }:

let
  domainName = "yourdomain.com";
  ownerDid = "did:plc:YOUR_DID_HERE";
in
{
  services.tangled-spindle = {
    enable = true;
    server = {
      hostname = "spindle.${domainName}";
      owner = ownerDid;
      listenAddr = "0.0.0.0:6555";
      jetstreamEndpoint = "wss://jetstream1.us-west.bsky.network/subscribe";
      maxJobCount = 4;
      queueSize = 100;
      secrets.provider = "sqlite";
    };
    endpoints = {
      appview = "https://tangled.org";
      knot = "https://knot.${domainName}";
      jetstream = "wss://jetstream.tangled.sh";
      nixery = "https://nixery.tangled.sh";
      atproto = "https://bsky.social";
      plc = "https://plc.directory";
    };
    pipelines.workflowTimeout = "10m";
    openFirewall = true;
  };

  services.caddy.virtualHosts = {
    "spindle.${domainName}, sp.${domainName}" = {
      extraConfig = ''
        route {
          @api_limit {
            not remote_ip 127.0.0.1
          }
          rate_limit @api_limit {
            rate 10r/s
            burst 20
          }
          reverse_proxy http://127.0.0.1:6555
        }
      '';
    };
  };
}
}}}

=== DNS Records ===

spindle.yourdomain.com A YOURSERVERIP

sp.yourdomain.com A YOURSERVERIP

}}}

Verification

# Check service
systemctl status tangled-spindle

# Check job queue
journalctl -u tangled-spindle -f | grep -i "job\|queue"
}}}

---

== Microcosm Spacedust ==

=== Prerequisites ===
- [ ] Basic NixOS setup
- [ ] Jetstream URL

=== Configuration ===

{ config, pkgs, ... }:

let

jetstreamUrl = "wss://jetstream1.us-west.bsky.network/subscribe";

in

{

services.microcosm-spacedust = {

enable = true;

jetstream = jetstreamUrl;

package = pkgs.microcosm-spacedust;

};

services.caddy.virtualHosts = {

"spacedust.yourdomain.com, sd.yourdomain.com" = {

extraConfig = ''

route {

@api_limit {

not remote_ip 127.0.0.1

}

ratelimit @apilimit {

rate 10r/s

burst 20

}

reverse_proxy http://127.0.0.1:9998

}

'';

};

};

}

}}}

DNS Records

spacedust.yourdomain.com  A     YOUR_SERVER_IP
sd.yourdomain.com         A     YOUR_SERVER_IP
}}}

---

== Microcosm Slingshot ==

=== Configuration ===

{ config, pkgs, ... }:

let

jetstreamUrl = "wss://jetstream1.us-west.bsky.network/subscribe";

in

{

services.microcosm-slingshot = {

enable = true;

jetstream = jetstreamUrl;

package = pkgs.microcosm-slingshot;

};

services.caddy.virtualHosts = {

"slingshot.yourdomain.com" = {

extraConfig = ''

route {

@api_limit {

not remote_ip 127.0.0.1

}

ratelimit @apilimit {

rate 10r/s

burst 20

}

reverse_proxy http://127.0.0.1:3000

}

'';

};

"ss.yourdomain.com" = {

extraConfig = ''

redir https://slingshot.yourdomain.com{uri} permanent

'';

};

};

}

}}}

DNS Records

slingshot.yourdomain.com  A     YOUR_SERVER_IP
ss.yourdomain.com         A     YOUR_SERVER_IP
}}}

---

== Lycan Feed Generator ==

=== Prerequisites ===
- [ ] PostgreSQL enabled
- [ ] Jetstream access

=== Configuration ===

{ config, pkgs, ... }:

let

domainName = "yourdomain.com";

in

{

# PostgreSQL for Lycan

services.postgresql = {

enable = true;

ensureDatabases = [ "lycan" ];

ensureUsers = [{

name = "lycan";

ensureDBOwnership = true;

}];

};

# Lycan service

services.mackuba-lycan = {

enable = true;

hostname = "lycan.${domainName}";

port = 3100;

allowedHosts = [ "lycan.${domainName}" "ly.${domainName}" ];

database.createLocally = true;

relay.jetstreamHost = "jetstream1.us-east.bsky.network";

firehose.userAgent = "Lycan (@${domainName})";

};

services.caddy.virtualHosts = {

"lycan.${domainName}, ly.${domainName}" = {

extraConfig = ''

route {

@api_limit {

not remote_ip 127.0.0.1

}

ratelimit @apilimit {

rate 10r/s

burst 20

}

reverse_proxy http://127.0.0.1:3100

}

'';

};

};

}

}}}

DNS Records

lycan.yourdomain.com      A     YOUR_SERVER_IP
ly.yourdomain.com         A     YOUR_SERVER_IP
}}}

=== Verification ===

# Check PostgreSQL

sudo -u postgres psql -c "\l" | grep lycan

# Check Lycan

systemctl status lycan

# Check feed endpoint

curl https://lycan.yourdomain.com/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:plc:xxx/app.bsky.feed.generator/feed-name

}}}

---

Prometheus & Grafana

Configuration

{ config, pkgs, ... }:

let
  domainName = "yourdomain.com";
in
{
  # Prometheus
  services.prometheus = {
    enable = true;
    port = 9090;
    
    scrapeConfigs = [
      {
        job_name = "prometheus";
        static_configs = [{
          targets = [ "localhost:9090" ];
          labels.instance = "prometheus";
        }];
      }
      {
        job_name = "node-exporter";
        static_configs = [{
          targets = [ "localhost:9100" ];
          labels.instance = "snek";
        }];
      }
    ];

    exporters.node = {
      enable = true;
      port = 9100;
      enabledCollectors = [ "systemd" "processes" ];
    };
  };

  # Grafana
  services.grafana = {
    enable = true;
    settings = {
      server = {
        http_addr = "127.0.0.1";
        http_port = 3001;
        domain = "grafana.${domainName}";
        root_url = "https://grafana.${domainName}";
      };
      "auth.anonymous" = {
        enabled = true;
        org_role = "Viewer";
      };
    };
    
    provision = {
      enable = true;
      datasources.settings.datasources = [{
        name = "Prometheus";
        type = "prometheus";
        access = "proxy";
        url = "http://localhost:9090";
        isDefault = true;
      }];
    };
  };

  # Caddy virtual hosts
  services.caddy.virtualHosts = {
    "grafana.${domainName}" = {
      extraConfig = ''
        reverse_proxy http://127.0.0.1:3001
        header {
          X-Frame-Options "SAMEORIGIN"
          X-Content-Type-Options "nosniff"
        }
      '';
    };
  };

  # Dashboard files
  environment.etc."grafana-dashboards/system-health.json".source = ./grafana-dashboards/system-health.json;
}
}}}

=== DNS Records ===

grafana.yourdomain.com A YOURSERVERIP

}}}

Verification

# Check Prometheus
curl http://localhost:9090/api/v1/status/targets

# Check Grafana
curl -I https://grafana.yourdomain.com
}}}

---

== QuickDID ==

=== Configuration ===

{ config, pkgs, ... }:

let

domainName = "yourdomain.com";

in

{

services.smokesignal-events-quickdid = {

enable = true;

package = pkgs.quickdid;

settings = {

httpExternal = "quickdid.${domainName}";

};

};

services.caddy.virtualHosts = {

"quickdid.${domainName}, qd.${domainName}" = {

extraConfig = ''

route {

@api_limit {

not remote_ip 127.0.0.1

}

ratelimit @apilimit {

rate 10r/s

burst 20

}

reverse_proxy http://127.0.0.1:8080

}

'';

};

};

}

}}}

DNS Records

quickdid.yourdomain.com   A     YOUR_SERVER_IP
qd.yourdomain.com         A     YOUR_SERVER_IP
}}}

---

== Adding Multiple Services ==

When adding multiple services, organize your configuration:

{ config, pkgs, ... }:

let

domainName = "yourdomain.com";

ownerDid = "did:plc:YOUR_DID";

jetstreamUrl = "wss://jetstream1.us-west.bsky.network/subscribe";

in

{

# ============================================

# SECRETS

# ============================================

sops.secrets = {

PDSJWTSECRET = {};

PDSADMINPASSWORD = {};

# ... other secrets ...

};

sops.templates."pds-env".content = ''

# ... template content ...

'';

# ============================================

# SERVICES

# ============================================

# Bluesky PDS

services.bluesky-pds = {

# ... PDS config ...

};

# Tangled Services

services.tangled-knot = {

# ... Knot config ...

};

services.tangled-spindle = {

# ... Spindle config ...

};

# Microcosm Services

services.microcosm-spacedust = {

# ... Spacedust config ...

};

services.microcosm-slingshot = {

# ... Slingshot config ...

};

# Lycan

services.postgresql = {

# ... PostgreSQL config ...

};

services.mackuba-lycan = {

# ... Lycan config ...

};

# Monitoring

services.prometheus = {

# ... Prometheus config ...

};

services.grafana = {

# ... Grafana config ...

};

# ============================================

# CADDY VIRTUAL HOSTS

# ============================================

services.caddy.virtualHosts = {

# PDS (wildcards)

"*.pds.${domainName}, pds.${domainName}" = {

# ... PDS config ...

};

# Tangled

"knot.${domainName}" = {

# ... Knot config ...

};

"spindle.${domainName}" = {

# ... Spindle config ...

};

# Microcosm

"spacedust.${domainName}" = {

# ... Spacedust config ...

};

"slingshot.${domainName}" = {

# ... Slingshot config ...

};

# Lycan

"lycan.${domainName}" = {

# ... Lycan config ...

};

# Monitoring

"grafana.${domainName}" = {

# ... Grafana config ...

};

};

}

}}}

Order of Operations

When adding multiple services, do it in this order:

1. *Base system* (SSH, users, flakes)

2. *Caddy* (for HTTPS)

3. *Secrets management* (sops-nix)

4. *QuickDID* (simplest service to test setup)

5. *Tangled Knot* (if using Tangled)

6. *Tangled Spindle* (depends on Knot)

7. *Microcosm services* (Spacedust, Slingshot)

8. *PostgreSQL + Lycan* (needs database)

9. *Bluesky PDS* (most complex, needs all infrastructure)

10. *Monitoring* (Prometheus + Grafana)

Common Issues When Adding Multiple Services

*Port Conflicts:*

# Check for port conflicts
ss -tlnp | grep -E "(2583|3000|5555|6555|8080|9998|3100)"
}}}

**Service Dependencies:**
- Lycan needs PostgreSQL first
- Spindle needs Knot
- Some services need secrets configured

**Resource Limits:**
- Monitor RAM usage: `free -h`
- Check if services are OOM killed: `dmesg | grep -i "killed process"`

== References ==

- [[https://search.nixos.org/options|NixOS Service Options]]
- [[https://github.com/atproto-nix/nur|ATProto NUR Packages]]
- [[https://caddyserver.com/docs/caddyfile/directives/reverse_proxy|Caddy Reverse Proxy]]