Kieran's opinionated (and probably slightly dumb) nix config
1# NixOS Service Modules 2 3This directory contains reusable NixOS service modules for deploying applications. 4 5## Architecture 6 7Each service module follows a common pattern for deploying TypeScript/Bun applications: 8 9### Directory Structure 10- **Data Directory**: `/var/lib/<service-name>/` 11 - `app/` - Git repository clone 12 - `data/` - Application data (databases, uploads, etc.) 13 14### Systemd Service Pattern 15 161. **ExecStartPre** (runs as root with `+` prefix): 17 - Creates data directories 18 - Sets ownership to service user 19 - Ensures proper permissions 20 212. **preStart** (runs as service user): 22 - Clones git repository if needed 23 - Pulls latest changes (if `autoUpdate` enabled) 24 - Runs `bun install` 25 - Initializes database if needed 26 273. **ExecStart** (runs as service user): 28 - Starts the application with `bun start` 29 30### Common Options 31 32All service modules support: 33 34```nix 35atelier.services.<service-name> = { 36 enable = true; # Enable the service 37 domain = "app.example.com"; # Domain for Caddy reverse proxy 38 port = 3000; # Port the app listens on 39 dataDir = "/var/lib/<service>"; # Data storage location 40 secretsFile = path; # agenix secrets file 41 repository = "https://..."; # Git repository URL 42 autoUpdate = true; # Git pull on service restart 43}; 44``` 45 46### Secrets Management 47 48Secrets are managed using [agenix](https://github.com/ryantm/agenix): 49 501. Add secret to `secrets/secrets.nix`: 51 ```nix 52 "service-name.age".publicKeys = [ kierank ]; 53 ``` 54 552. Create and encrypt the secret: 56 ```bash 57 agenix -e secrets/service-name.age 58 ``` 59 603. Add environment variables (one per line): 61 ``` 62 DATABASE_URL=postgres://... 63 API_KEY=xxxxx 64 SECRET_TOKEN=yyyyy 65 ``` 66 674. Reference in machine config: 68 ```nix 69 age.secrets.service-name = { 70 file = ../../secrets/service-name.age; 71 owner = "service-name"; 72 }; 73 74 atelier.services.service-name = { 75 secretsFile = config.age.secrets.service-name.path; 76 }; 77 ``` 78 79### Reverse Proxy (Caddy) 80 81Each service automatically configures a Caddy virtual host with: 82- Cloudflare DNS challenge for TLS 83- Reverse proxy to the application port 84 85## GitHub Actions Deployment 86 87Services can be deployed via GitHub Actions using SSH over Tailscale. 88 89### Prerequisites 90 911. **Tailscale OAuth Client**: 92 - Create at https://login.tailscale.com/admin/settings/oauth 93 - Required scope: `auth_keys` (to authenticate ephemeral nodes) 94 - Add to GitHub repo secrets: 95 - `TS_OAUTH_CLIENT_ID` 96 - `TS_OAUTH_SECRET` 97 982. **SSH Access**: 99 - Add the service user to Tailscale SSH ACLs 100 - Example in `tailscale.com/admin/acls`: 101 ```json 102 "ssh": [ 103 { 104 "action": "accept", 105 "src": ["tag:ci"], 106 "dst": ["tag:server"], 107 "users": ["cachet", "hn-alerts", "root"] 108 } 109 ] 110 ``` 111 112### Workflow Template 113 114Create `.github/workflows/deploy-<service>.yaml`: 115 116```yaml 117name: Deploy <Service Name> 118 119on: 120 push: 121 branches: 122 - main 123 workflow_dispatch: 124 125jobs: 126 deploy: 127 runs-on: ubuntu-latest 128 steps: 129 - uses: actions/checkout@v3 130 131 - name: Setup Tailscale 132 uses: tailscale/github-action@v3 133 with: 134 oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} 135 oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} 136 tags: tag:ci 137 use-cache: "true" 138 139 - name: Configure SSH 140 run: | 141 mkdir -p ~/.ssh 142 echo "StrictHostKeyChecking no" >> ~/.ssh/config 143 144 - name: Deploy to server 145 run: | 146 ssh <service-user>@<hostname> << 'EOF' 147 cd /var/lib/<service>/app 148 git fetch --all 149 git reset --hard origin/main 150 bun install 151 sudo /run/current-system/sw/bin/systemctl restart <service>.service 152 EOF 153 154 - name: Wait for service to start 155 run: sleep 10 156 157 - name: Health check 158 run: | 159 HEALTH_URL="https://<domain>/health" 160 MAX_RETRIES=6 161 RETRY_DELAY=5 162 163 for i in $(seq 1 $MAX_RETRIES); do 164 echo "Health check attempt $i/$MAX_RETRIES..." 165 166 RESPONSE=$(curl -s -w "\n%{http_code}" "$HEALTH_URL" || echo "000") 167 HTTP_CODE=$(echo "$RESPONSE" | tail -n1) 168 BODY=$(echo "$RESPONSE" | sed '$d') 169 170 if [ "$HTTP_CODE" = "200" ]; then 171 echo "✅ Service is healthy" 172 echo "$BODY" 173 exit 0 174 fi 175 176 echo "❌ Health check failed with HTTP $HTTP_CODE" 177 echo "$BODY" 178 179 if [ $i -lt $MAX_RETRIES ]; then 180 echo "Retrying in ${RETRY_DELAY}s..." 181 sleep $RETRY_DELAY 182 fi 183 done 184 185 echo "❌ Health check failed after $MAX_RETRIES attempts" 186 exit 1 187``` 188 189### Deployment Flow 190 1911. Push to `main` branch triggers workflow 1922. GitHub Actions runner joins Tailscale network 1933. SSH to service user on target server 1944. Git pull latest changes 1955. Install dependencies 1966. Restart systemd service 1977. Verify health check endpoint 198 199## Creating a New Service Module 200 2011. Copy an existing module (e.g., `cachet.nix` or `hn-alerts.nix`) 2022. Update service name, user, and group 2033. Adjust environment variables as needed 2044. Add database initialization if required 2055. Configure secrets in `secrets/secrets.nix` 2066. Import in machine config 2077. Create GitHub Actions workflow (if needed) 208 209## Example Services 210 211- **cachet** - Slack emoji/profile cache 212- **hn-alerts** - Hacker News monitoring and alerts