alf: the atproto Latency Fabric
alf.fly.dev/
1# Deploying ALF
2
3This guide covers running ALF in production across several common platforms.
4
5> **Important:** `SERVICE_URL` must be an HTTPS URL in all production deployments. ATProto OAuth requires that ALF's client metadata be served over HTTPS. Without a valid HTTPS `SERVICE_URL`, the OAuth authorization flow will fail.
6
7---
8
9## Environment variable reference
10
11| Variable | Required | Default | Description |
12|----------|----------|---------|-------------|
13| `PORT` | No | `3005` | HTTP port ALF listens on |
14| `SERVICE_URL` | **Yes (production)** | `http://localhost:3005` | Public HTTPS URL of this deployment |
15| `ENCRYPTION_KEY` | **Yes** | — | 64-char hex string (32 bytes) for AES-256-GCM encryption of stored tokens |
16| `DATABASE_TYPE` | No | `sqlite` | `sqlite` or `postgres` |
17| `DATABASE_PATH` | No | `./data/alf.db` | SQLite file path (ignored when using Postgres) |
18| `DATABASE_URL` | If postgres | — | PostgreSQL connection string |
19| `PLC_ROOT` | No | `https://plc.directory` | ATProto PLC directory |
20| `HANDLE_RESOLVER_URL` | No | `https://api.bsky.app` | Handle-to-DID resolver |
21| `POST_PUBLISH_WEBHOOK_URL` | No | — | URL to POST to after each successful publish |
22
23Generate an encryption key before any deployment:
24
25```bash
26node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
27```
28
29Store the resulting 64-character hex string as `ENCRYPTION_KEY`. Treat it like a private key — if it is lost, all stored OAuth tokens become unreadable.
30
31---
32
33## Docker (standalone)
34
35### Prerequisites
36
37- Docker installed and running
38- A domain with HTTPS (or a reverse proxy such as Caddy or nginx providing TLS termination in front of port 3005)
39
40### Steps
41
42```bash
43# 1. Build the image
44git clone https://github.com/your-org/alf.git
45cd alf
46docker build -t alf .
47
48# 2. Create a data directory for the SQLite database
49mkdir -p ./data
50
51# 3. Run the container
52docker run -d \
53 --name alf \
54 --restart unless-stopped \
55 -p 3005:3005 \
56 -e ENCRYPTION_KEY=your-64-char-hex-key \
57 -e SERVICE_URL=https://alf.example.com \
58 -e DATABASE_TYPE=sqlite \
59 -e DATABASE_PATH=/data/alf.db \
60 -v $(pwd)/data:/data \
61 alf
62
63# 4. Verify
64curl https://alf.example.com/health
65# {"status":"ok","service":"alf"}
66```
67
68### Using PostgreSQL instead of SQLite
69
70```bash
71docker run -d \
72 --name alf \
73 --restart unless-stopped \
74 -p 3005:3005 \
75 -e ENCRYPTION_KEY=your-64-char-hex-key \
76 -e SERVICE_URL=https://alf.example.com \
77 -e DATABASE_TYPE=postgres \
78 -e DATABASE_URL=postgresql://user:pass@db.example.com:5432/alf \
79 alf
80```
81
82### Production checklist
83
84- `SERVICE_URL` is set to your public HTTPS URL
85- `ENCRYPTION_KEY` is a securely generated 64-char hex string
86- The `/data` volume (or Postgres) is backed up regularly
87- A reverse proxy handles TLS in front of port 3005
88
89---
90
91## Docker Compose
92
93The repository includes a `docker-compose.yml` that runs ALF with a persistent named volume for the SQLite database.
94
95### Prerequisites
96
97- Docker and Docker Compose v2 installed
98- A domain with HTTPS termination (handled externally — the Compose file exposes port 3005)
99
100### Steps
101
102```bash
103# 1. Clone the repository
104git clone https://github.com/your-org/alf.git
105cd alf
106
107# 2. Create your .env file
108cp .env.example .env
109```
110
111Edit `.env` and set at minimum:
112
113```dotenv
114ENCRYPTION_KEY=your-64-char-hex-key
115SERVICE_URL=https://alf.example.com
116```
117
118For Postgres, also set:
119
120```dotenv
121DATABASE_TYPE=postgres
122DATABASE_URL=postgresql://user:pass@db.example.com:5432/alf
123```
124
125```bash
126# 3. Start ALF
127docker compose up -d
128
129# 4. Verify
130curl http://localhost:3005/health
131# {"status":"ok","service":"alf"}
132```
133
134### Updating
135
136```bash
137git pull
138docker compose build
139docker compose up -d
140```
141
142### Production checklist
143
144- `SERVICE_URL` is set to your public HTTPS URL in `.env`
145- `.env` is not committed to version control
146- `ENCRYPTION_KEY` is a securely generated 64-char hex string
147- The `alf-data` named volume is included in your backup strategy
148- TLS is terminated upstream (nginx, Caddy, Traefik, etc.)
149
150---
151
152## Fly.io
153
154### Prerequisites
155
156- [`flyctl`](https://fly.io/docs/hands-on/install-flyctl/) installed and authenticated (`fly auth login`)
157- A Fly.io account
158
159### Steps
160
161```bash
162# 1. Clone the repository
163git clone https://github.com/your-org/alf.git
164cd alf
165
166# 2. Create a new Fly app (accept defaults or customise as prompted)
167fly launch
168
169# 3. Set secrets (never put these in fly.toml)
170fly secrets set ENCRYPTION_KEY=your-64-char-hex-key
171fly secrets set SERVICE_URL=https://alf.your-app.fly.dev
172
173# If using Postgres:
174fly secrets set DATABASE_TYPE=postgres
175fly secrets set DATABASE_URL=postgresql://user:pass@your-fly-pg.internal:5432/alf
176
177# 4. Deploy
178fly deploy
179
180# 5. Verify
181curl https://alf.your-app.fly.dev/health
182# {"status":"ok","service":"alf"}
183```
184
185### Persistent storage for SQLite
186
187If you are using SQLite (the default), attach a Fly volume so the database survives restarts and deployments:
188
189```bash
190fly volumes create alf_data --region <your-region> --size 1
191```
192
193Add the following to your `fly.toml`:
194
195```toml
196[mounts]
197 source = "alf_data"
198 destination = "/data"
199```
200
201Then set `DATABASE_PATH=/data/alf.db` as a secret or in `fly.toml` under `[env]`.
202
203For multi-region or multi-instance deployments, use Postgres (`fly postgres create`) rather than SQLite.
204
205### Production checklist
206
207- `SERVICE_URL` is set to your `https://your-app.fly.dev` URL (or custom domain with HTTPS)
208- `ENCRYPTION_KEY` is set as a secret (not in `fly.toml`)
209- A Fly volume or Fly Postgres is configured for persistence
210- Health check passes: `fly status`
211
212---
213
214## Railway
215
216### Prerequisites
217
218- A [Railway](https://railway.app) account
219- Your ALF repository pushed to GitHub
220
221### Steps
222
2231. Go to [railway.app](https://railway.app) and click **New Project**.
2242. Select **Deploy from GitHub repo** and choose your ALF repository.
2253. Railway will detect the `Dockerfile` and build automatically.
2264. Click on your service, then go to **Variables** and add:
227
228| Variable | Value |
229|----------|-------|
230| `ENCRYPTION_KEY` | your-64-char-hex-key |
231| `SERVICE_URL` | `https://<your-railway-app>.up.railway.app` (set after domain is assigned) |
232| `DATABASE_TYPE` | `sqlite` or `postgres` |
233| `DATABASE_URL` | (if using Postgres — see below) |
234
2355. To add a managed Postgres database: click **New** in your project, choose **Database > Add PostgreSQL**. Railway will inject `DATABASE_URL` automatically; you only need to set `DATABASE_TYPE=postgres`.
236
2376. For SQLite persistence, add a **Volume** to your service and mount it at `/data`, then set `DATABASE_PATH=/data/alf.db`.
238
2397. Once deployed, copy the public URL Railway assigns and update `SERVICE_URL` to that HTTPS URL.
240
2418. Trigger a redeploy so the updated `SERVICE_URL` takes effect.
242
243### Production checklist
244
245- `SERVICE_URL` is set to the Railway-provided HTTPS URL or your custom domain
246- `ENCRYPTION_KEY` is set in the Variables panel
247- Persistent storage (volume or Postgres) is configured
248- Health check URL (`/health`) is configured in the Railway service settings
249
250---
251
252## Render
253
254### Prerequisites
255
256- A [Render](https://render.com) account
257- Your ALF repository pushed to GitHub
258
259### Steps
260
2611. Go to the Render dashboard and click **New > Web Service**.
2622. Connect your GitHub repository.
2633. Render will detect the `Dockerfile`. Set:
264 - **Name:** `alf` (or your preferred name)
265 - **Region:** choose the region closest to your users
266 - **Instance type:** Starter or above (Starter is sufficient for low traffic)
2674. Under **Environment Variables**, add:
268
269| Key | Value |
270|-----|-------|
271| `ENCRYPTION_KEY` | your-64-char-hex-key |
272| `SERVICE_URL` | `https://<your-render-app>.onrender.com` (update after deploy) |
273| `DATABASE_TYPE` | `sqlite` or `postgres` |
274
2755. For Postgres: click **New > PostgreSQL** in the Render dashboard to create a managed database. Copy the **Internal Database URL** into `DATABASE_URL` and set `DATABASE_TYPE=postgres`.
276
2776. For SQLite persistence: add a **Disk** to your service, mounted at `/data`, with at least 1 GB. Then set `DATABASE_PATH=/data/alf.db`.
278
2797. Click **Create Web Service**. After the first deploy completes, copy the Render-provided URL and update `SERVICE_URL` in your environment variables. Render will trigger an automatic redeploy.
280
281### Production checklist
282
283- `SERVICE_URL` is set to the Render-provided HTTPS URL or your custom domain
284- `ENCRYPTION_KEY` is set in the environment variables panel
285- A Render Disk (for SQLite) or Render PostgreSQL is attached
286- The health check path is set to `/health` in the Render service settings
287
288---
289
290## Bare-metal / VPS
291
292This section covers running ALF directly on a Linux server (Ubuntu, Debian, etc.) as a systemd service.
293
294### Prerequisites
295
296- Node.js 24 or later (required — ALF uses `"engines": { "node": ">=24" }`)
297- npm 10 or later (bundled with Node 24)
298- A reverse proxy (Caddy or nginx) for HTTPS termination
299- A domain name pointed at your server's IP
300
301### Install Node.js 24
302
303```bash
304# Using NodeSource (recommended)
305curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
306sudo apt-get install -y nodejs
307
308node --version # should print v24.x.x
309```
310
311### Clone and build ALF
312
313```bash
314sudo mkdir -p /opt/alf
315sudo chown $USER:$USER /opt/alf
316
317git clone https://github.com/your-org/alf.git /opt/alf
318cd /opt/alf
319
320npm ci --omit=dev
321npm run build
322```
323
324### Create a dedicated user
325
326```bash
327sudo useradd --system --no-create-home --shell /usr/sbin/nologin alf
328sudo chown -R alf:alf /opt/alf
329
330# Create the data directory
331sudo mkdir -p /var/lib/alf
332sudo chown alf:alf /var/lib/alf
333```
334
335### Configure environment
336
337```bash
338sudo nano /etc/alf/env
339```
340
341Set the following (create `/etc/alf/` first: `sudo mkdir -p /etc/alf`):
342
343```dotenv
344PORT=3005
345SERVICE_URL=https://alf.example.com
346ENCRYPTION_KEY=your-64-char-hex-key
347DATABASE_TYPE=sqlite
348DATABASE_PATH=/var/lib/alf/alf.db
349# Or for Postgres:
350# DATABASE_TYPE=postgres
351# DATABASE_URL=postgresql://user:pass@localhost:5432/alf
352PLC_ROOT=https://plc.directory
353HANDLE_RESOLVER_URL=https://api.bsky.app
354```
355
356Restrict permissions on the env file:
357
358```bash
359sudo chmod 600 /etc/alf/env
360sudo chown root:alf /etc/alf/env
361```
362
363### systemd unit file
364
365Create `/etc/systemd/system/alf.service`:
366
367```ini
368[Unit]
369Description=ALF — Atproto Latency Fabric
370After=network.target
371
372[Service]
373Type=simple
374User=alf
375Group=alf
376WorkingDirectory=/opt/alf
377EnvironmentFile=/etc/alf/env
378ExecStart=/usr/bin/node /opt/alf/dist/index.js
379Restart=on-failure
380RestartSec=5
381StandardOutput=journal
382StandardError=journal
383SyslogIdentifier=alf
384
385# Hardening
386NoNewPrivileges=true
387PrivateTmp=true
388ProtectSystem=full
389ReadWritePaths=/var/lib/alf
390
391[Install]
392WantedBy=multi-user.target
393```
394
395Enable and start the service:
396
397```bash
398sudo systemctl daemon-reload
399sudo systemctl enable alf
400sudo systemctl start alf
401sudo systemctl status alf
402```
403
404View logs:
405
406```bash
407journalctl -u alf -f
408```
409
410### Reverse proxy with Caddy (recommended)
411
412Install Caddy and add to `/etc/caddy/Caddyfile`:
413
414```
415alf.example.com {
416 reverse_proxy localhost:3005
417}
418```
419
420Caddy handles HTTPS automatically via Let's Encrypt. Reload:
421
422```bash
423sudo systemctl reload caddy
424```
425
426### Reverse proxy with nginx
427
428```nginx
429server {
430 listen 80;
431 server_name alf.example.com;
432 return 301 https://$host$request_uri;
433}
434
435server {
436 listen 443 ssl;
437 server_name alf.example.com;
438
439 ssl_certificate /etc/letsencrypt/live/alf.example.com/fullchain.pem;
440 ssl_certificate_key /etc/letsencrypt/live/alf.example.com/privkey.pem;
441
442 location / {
443 proxy_pass http://localhost:3005;
444 proxy_http_version 1.1;
445 proxy_set_header Host $host;
446 proxy_set_header X-Real-IP $remote_addr;
447 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
448 proxy_set_header X-Forwarded-Proto $scheme;
449 }
450}
451```
452
453Use `certbot --nginx -d alf.example.com` to obtain a Let's Encrypt certificate.
454
455### Updating
456
457```bash
458cd /opt/alf
459git pull
460npm ci --omit=dev
461npm run build
462sudo systemctl restart alf
463```
464
465### Production checklist
466
467- Node.js 24 or later is installed (`node --version`)
468- `SERVICE_URL` is set to your public HTTPS URL
469- `ENCRYPTION_KEY` is a securely generated 64-char hex string
470- `/etc/alf/env` has permissions `600` (readable only by root and the `alf` user)
471- `/var/lib/alf` is backed up regularly (contains the SQLite database)
472- The systemd service is enabled and set to restart on failure
473- A reverse proxy with TLS is in front of port 3005
474- Health check passes: `curl https://alf.example.com/health`