···
1
+
# NixOS Service Modules
3
+
This directory contains reusable NixOS service modules for deploying applications.
7
+
Each service module follows a common pattern for deploying TypeScript/Bun applications:
9
+
### Directory Structure
10
+
- **Data Directory**: `/var/lib/<service-name>/`
11
+
- `app/` - Git repository clone
12
+
- `data/` - Application data (databases, uploads, etc.)
14
+
### Systemd Service Pattern
16
+
1. **ExecStartPre** (runs as root with `+` prefix):
17
+
- Creates data directories
18
+
- Sets ownership to service user
19
+
- Ensures proper permissions
21
+
2. **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
27
+
3. **ExecStart** (runs as service user):
28
+
- Starts the application with `bun start`
32
+
All service modules support:
35
+
atelier.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
46
+
### Secrets Management
48
+
Secrets are managed using [agenix](https://github.com/ryantm/agenix):
50
+
1. Add secret to `secrets/secrets.nix`:
52
+
"service-name.age".publicKeys = [ kierank ];
55
+
2. Create and encrypt the secret:
57
+
agenix -e secrets/service-name.age
60
+
3. Add environment variables (one per line):
62
+
DATABASE_URL=postgres://...
67
+
4. Reference in machine config:
69
+
age.secrets.service-name = {
70
+
file = ../../secrets/service-name.age;
71
+
owner = "service-name";
74
+
atelier.services.service-name = {
75
+
secretsFile = config.age.secrets.service-name.path;
79
+
### Reverse Proxy (Caddy)
81
+
Each service automatically configures a Caddy virtual host with:
82
+
- Cloudflare DNS challenge for TLS
83
+
- Reverse proxy to the application port
85
+
## GitHub Actions Deployment
87
+
Services can be deployed via GitHub Actions using SSH over Tailscale.
91
+
1. **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`
99
+
- Add the service user to Tailscale SSH ACLs
100
+
- Example in `tailscale.com/admin/acls`:
104
+
"action": "accept",
106
+
"dst": ["tag:server"],
107
+
"users": ["cachet", "hn-alerts", "root"]
112
+
### Workflow Template
114
+
Create `.github/workflows/deploy-<service>.yaml`:
117
+
name: Deploy <Service Name>
127
+
runs-on: ubuntu-latest
129
+
- uses: actions/checkout@v3
131
+
- name: Setup Tailscale
132
+
uses: tailscale/github-action@v3
134
+
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
135
+
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
139
+
- name: Configure SSH
142
+
echo "StrictHostKeyChecking no" >> ~/.ssh/config
144
+
- name: Deploy to server
146
+
ssh <service-user>@<hostname> << 'EOF'
147
+
cd /var/lib/<service>/app
149
+
git reset --hard origin/main
151
+
sudo /run/current-system/sw/bin/systemctl restart <service>.service
154
+
- name: Wait for service to start
157
+
- name: Health check
159
+
HEALTH_URL="https://<domain>/health"
163
+
for i in $(seq 1 $MAX_RETRIES); do
164
+
echo "Health check attempt $i/$MAX_RETRIES..."
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')
170
+
if [ "$HTTP_CODE" = "200" ]; then
171
+
echo "✅ Service is healthy"
176
+
echo "❌ Health check failed with HTTP $HTTP_CODE"
179
+
if [ $i -lt $MAX_RETRIES ]; then
180
+
echo "Retrying in ${RETRY_DELAY}s..."
185
+
echo "❌ Health check failed after $MAX_RETRIES attempts"
189
+
### Deployment Flow
191
+
1. Push to `main` branch triggers workflow
192
+
2. GitHub Actions runner joins Tailscale network
193
+
3. SSH to service user on target server
194
+
4. Git pull latest changes
195
+
5. Install dependencies
196
+
6. Restart systemd service
197
+
7. Verify health check endpoint
199
+
## Creating a New Service Module
201
+
1. Copy an existing module (e.g., `cachet.nix` or `hn-alerts.nix`)
202
+
2. Update service name, user, and group
203
+
3. Adjust environment variables as needed
204
+
4. Add database initialization if required
205
+
5. Configure secrets in `secrets/secrets.nix`
206
+
6. Import in machine config
207
+
7. Create GitHub Actions workflow (if needed)
209
+
## Example Services
211
+
- **cachet** - Slack emoji/profile cache
212
+
- **hn-alerts** - Hacker News monitoring and alerts