+21
-12
docs/install-alpine.md
+21
-12
docs/install-alpine.md
···
1
1
# BSPDS Production Installation on Alpine Linux
2
2
> **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified.
3
+
3
4
This guide covers installing BSPDS on Alpine Linux 3.23 (current stable as of December 2025).
4
-
## Choose Your Installation Method
5
-
| Method | Best For |
6
-
|--------|----------|
7
-
| **Native (this guide)** | Maximum performance, minimal footprint, full control |
8
-
| **[Containerized](install-containers.md)** | Easier updates, isolation, reproducible deployments |
9
-
| **[Kubernetes](install-kubernetes.md)** | Multi-node, high availability, auto-scaling |
10
-
This guide covers native installation. For containerized deployment with podman and systemd quadlets, see the [container guide](install-containers.md).
11
-
---
5
+
12
6
## Prerequisites
13
7
- A VPS with at least 2GB RAM and 20GB disk
14
8
- A domain name pointing to your server's IP
9
+
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
15
10
- Root access
16
11
## 1. System Setup
17
12
```sh
···
178
173
rc-update add nginx
179
174
rc-service nginx start
180
175
```
181
-
## 12. Obtain SSL Certificate
176
+
## 12. Obtain Wildcard SSL Certificate
177
+
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate.
178
+
179
+
Wildcard certs require DNS-01 validation. For manual DNS validation (works with any provider):
182
180
```sh
183
-
certbot --nginx -d pds.example.com
181
+
certbot certonly --manual --preferred-challenges dns \
182
+
-d pds.example.com -d '*.pds.example.com'
184
183
```
185
-
Set up auto-renewal:
184
+
Follow the prompts to add TXT records to your DNS.
185
+
186
+
If your DNS provider has a certbot plugin, you can use that for auto-renewal:
186
187
```sh
187
-
echo "0 0 * * * certbot renew --quiet" | crontab -
188
+
apk add certbot-dns-cloudflare
189
+
certbot certonly --dns-cloudflare \
190
+
--dns-cloudflare-credentials /etc/cloudflare.ini \
191
+
-d pds.example.com -d '*.pds.example.com'
192
+
```
193
+
194
+
After obtaining the cert, update nginx to use it, then set up auto-renewal:
195
+
```sh
196
+
echo "0 0 * * * certbot renew --quiet && rc-service nginx reload" | crontab -
188
197
```
189
198
## 13. Configure Firewall
190
199
```sh
+72
-28
docs/install-containers.md
+72
-28
docs/install-containers.md
···
6
6
## Prerequisites
7
7
- A VPS with at least 2GB RAM and 20GB disk
8
8
- A domain name pointing to your server's IP
9
+
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
9
10
- Root or sudo access
10
11
## Quick Start (Docker/Podman Compose)
11
12
If you just want to get running quickly:
12
13
```sh
13
14
cp .env.example .env
14
-
# Edit .env with your values
15
-
# Generate secrets: openssl rand -base64 48
16
-
# Build and start
15
+
```
16
+
17
+
Edit `.env` with your values. Generate secrets with `openssl rand -base64 48`.
18
+
19
+
Build and start:
20
+
```sh
17
21
podman-compose -f docker-compose.prod.yml up -d
18
-
# Get initial certificate (after DNS is configured)
22
+
```
23
+
24
+
Get initial certificate (after DNS is configured):
25
+
```sh
19
26
podman-compose -f docker-compose.prod.yml run --rm certbot certonly \
20
27
--webroot -w /var/www/acme -d pds.example.com
21
-
# Restart nginx to load certificate
22
28
podman-compose -f docker-compose.prod.yml restart nginx
23
29
```
24
30
For production setups with proper service management, continue to either the Debian or Alpine section below.
···
74
80
systemctl daemon-reload
75
81
systemctl start bspds-db bspds-minio bspds-valkey
76
82
sleep 10
77
-
# Create MinIO bucket
83
+
```
84
+
85
+
Create the minio bucket:
86
+
```bash
78
87
podman run --rm --pod bspds \
79
88
-e MINIO_ROOT_USER=minioadmin \
80
89
-e MINIO_ROOT_PASSWORD=your-minio-password \
81
90
docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \
82
91
sh -c "mc alias set local http://localhost:9000 \$MINIO_ROOT_USER \$MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/pds-blobs"
83
-
# Run migrations
92
+
```
93
+
94
+
Run migrations:
95
+
```bash
84
96
cargo install sqlx-cli --no-default-features --features postgres
85
97
DATABASE_URL="postgres://bspds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/bspds/migrations
86
98
```
87
-
## 9. Obtain SSL Certificate
88
-
Create temporary self-signed cert:
99
+
## 9. Obtain Wildcard SSL Certificate
100
+
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
101
+
102
+
Create temporary self-signed cert to start services:
89
103
```bash
90
104
openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
91
105
-keyout /srv/bspds/certs/privkey.pem \
92
106
-out /srv/bspds/certs/fullchain.pem \
93
107
-subj "/CN=pds.example.com"
94
108
systemctl start bspds-app bspds-nginx
95
-
# Get real certificate
96
-
podman run --rm \
109
+
```
110
+
111
+
Get a wildcard certificate using DNS validation:
112
+
```bash
113
+
podman run --rm -it \
97
114
-v /srv/bspds/certs:/etc/letsencrypt:Z \
98
-
-v /srv/bspds/acme:/var/www/acme:Z \
99
115
docker.io/certbot/certbot:v5.2.2 certonly \
100
-
--webroot -w /var/www/acme -d pds.example.com --agree-tos --email you@example.com
101
-
# Link certificates
116
+
--manual --preferred-challenges dns \
117
+
-d pds.example.com -d '*.pds.example.com' \
118
+
--agree-tos --email you@example.com
119
+
```
120
+
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
121
+
122
+
For automated renewal, use a DNS provider plugin (e.g., cloudflare, route53).
123
+
124
+
Link certificates and restart:
125
+
```bash
102
126
ln -sf /srv/bspds/certs/live/pds.example.com/fullchain.pem /srv/bspds/certs/fullchain.pem
103
127
ln -sf /srv/bspds/certs/live/pds.example.com/privkey.pem /srv/bspds/certs/privkey.pem
104
128
systemctl restart bspds-nginx
···
200
224
chmod +x /etc/init.d/bspds
201
225
```
202
226
## 7. Initialize Services
227
+
Start services:
203
228
```sh
204
-
# Start services
205
229
rc-service bspds start
206
230
sleep 15
207
-
# Create MinIO bucket
231
+
```
232
+
233
+
Create the minio bucket:
234
+
```sh
208
235
source /srv/bspds/config/bspds.env
209
236
podman run --rm --network bspds_default \
210
237
-e MINIO_ROOT_USER="$MINIO_ROOT_USER" \
211
238
-e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \
212
239
docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \
213
240
sh -c 'mc alias set local http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/pds-blobs'
214
-
# Run migrations
241
+
```
242
+
243
+
Run migrations:
244
+
```sh
215
245
apk add rustup
216
246
rustup-init -y
217
247
source ~/.cargo/env
218
248
cargo install sqlx-cli --no-default-features --features postgres
219
-
# Get database container IP
220
249
DB_IP=$(podman inspect bspds-db-1 --format '{{.NetworkSettings.Networks.bspds_default.IPAddress}}')
221
250
DATABASE_URL="postgres://bspds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/bspds/migrations
222
251
```
223
-
## 8. Obtain SSL Certificate
224
-
Create temporary self-signed cert:
252
+
## 8. Obtain Wildcard SSL Certificate
253
+
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
254
+
255
+
Create temporary self-signed cert to start services:
225
256
```sh
226
257
openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
227
258
-keyout /srv/bspds/data/certs/privkey.pem \
228
259
-out /srv/bspds/data/certs/fullchain.pem \
229
260
-subj "/CN=pds.example.com"
230
261
rc-service bspds restart
231
-
# Get real certificate
232
-
podman run --rm \
262
+
```
263
+
264
+
Get a wildcard certificate using DNS validation:
265
+
```sh
266
+
podman run --rm -it \
233
267
-v /srv/bspds/data/certs:/etc/letsencrypt \
234
-
-v /srv/bspds/data/acme:/var/www/acme \
235
-
--network bspds_default \
236
268
docker.io/certbot/certbot:v5.2.2 certonly \
237
-
--webroot -w /var/www/acme -d pds.example.com --agree-tos --email you@example.com
238
-
# Link certificates
269
+
--manual --preferred-challenges dns \
270
+
-d pds.example.com -d '*.pds.example.com' \
271
+
--agree-tos --email you@example.com
272
+
```
273
+
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
274
+
275
+
Link certificates and restart:
276
+
```sh
239
277
ln -sf /srv/bspds/data/certs/live/pds.example.com/fullchain.pem /srv/bspds/data/certs/fullchain.pem
240
278
ln -sf /srv/bspds/data/certs/live/pds.example.com/privkey.pem /srv/bspds/data/certs/privkey.pem
241
279
rc-service bspds restart
···
292
330
cd /opt/bspds
293
331
git pull
294
332
podman build -t bspds:latest .
295
-
# Debian:
333
+
```
334
+
335
+
Debian:
336
+
```bash
296
337
systemctl restart bspds-app
297
-
# Alpine:
338
+
```
339
+
340
+
Alpine:
341
+
```sh
298
342
rc-service bspds restart
299
343
```
300
344
## Backup Database
+20
-11
docs/install-debian.md
+20
-11
docs/install-debian.md
···
1
1
# BSPDS Production Installation on Debian
2
2
> **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified.
3
+
3
4
This guide covers installing BSPDS on Debian 13 "Trixie" (current stable as of December 2025).
4
-
## Choose Your Installation Method
5
-
| Method | Best For |
6
-
|--------|----------|
7
-
| **Native (this guide)** | Maximum performance, full control, simpler debugging |
8
-
| **[Containerized](install-containers.md)** | Easier updates, isolation, reproducible deployments |
9
-
| **[Kubernetes](install-kubernetes.md)** | Multi-node, high availability, auto-scaling |
10
-
This guide covers native installation. For containerized deployment with podman and systemd quadlets, see the [container guide](install-containers.md).
11
-
---
5
+
12
6
## Prerequisites
13
7
- A VPS with at least 2GB RAM and 20GB disk
14
8
- A domain name pointing to your server's IP
9
+
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
15
10
- Root or sudo access
16
11
## 1. System Setup
17
12
```bash
···
168
163
nginx -t
169
164
systemctl reload nginx
170
165
```
171
-
## 12. Obtain SSL Certificate
166
+
## 12. Obtain Wildcard SSL Certificate
167
+
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate.
168
+
169
+
Wildcard certs require DNS-01 validation. If your DNS provider has a certbot plugin:
172
170
```bash
173
-
certbot --nginx -d pds.example.com
171
+
apt install -y python3-certbot-dns-cloudflare
172
+
certbot certonly --dns-cloudflare \
173
+
--dns-cloudflare-credentials /etc/cloudflare.ini \
174
+
-d pds.example.com -d '*.pds.example.com'
174
175
```
175
-
Certbot automatically configures nginx for HTTP/2 and sets up auto-renewal.
176
+
177
+
For manual DNS validation (works with any provider):
178
+
```bash
179
+
certbot certonly --manual --preferred-challenges dns \
180
+
-d pds.example.com -d '*.pds.example.com'
181
+
```
182
+
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
183
+
184
+
After obtaining the cert, update nginx to use it and reload.
176
185
## 13. Configure Firewall
177
186
```bash
178
187
apt install -y ufw
+23
-859
docs/install-kubernetes.md
+23
-859
docs/install-kubernetes.md
···
1
-
# BSPDS Production Kubernetes Deployment
2
-
> **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified.
3
-
This guide covers deploying BSPDS on a production multi-node Kubernetes cluster with high availability, auto-scaling, and proper secrets management.
4
-
## Architecture Overview
5
-
```
6
-
┌─────────────────────────────────────────────────┐
7
-
│ Kubernetes Cluster │
8
-
│ │
9
-
Internet ──────►│ Ingress Controller (nginx/traefik) │
10
-
│ │ │
11
-
│ ▼ │
12
-
│ ┌─────────────┐ │
13
-
│ │ Service │◄── HPA (2-10 replicas) │
14
-
│ └──────┬──────┘ │
15
-
│ │ │
16
-
│ ┌────┴────┐ │
17
-
│ ▼ ▼ │
18
-
│ ┌─────┐ ┌─────┐ │
19
-
│ │BSPDS│ │BSPDS│ ... (pods) │
20
-
│ └──┬──┘ └──┬──┘ │
21
-
│ │ │ │
22
-
│ ▼ ▼ │
23
-
│ ┌──────────────────────────────────────┐ │
24
-
│ │ PostgreSQL │ MinIO │ Valkey │ │
25
-
│ │ (HA/Operator)│ (StatefulSet) │ (Sentinel) │
26
-
│ └──────────────────────────────────────┘ │
27
-
└─────────────────────────────────────────────────┘
28
-
```
29
-
## Prerequisites
30
-
- Kubernetes cluster (1.30+) with at least 3 nodes (1.34 is current stable)
31
-
- `kubectl` configured to access your cluster
32
-
- `helm` 3.x installed
33
-
- Storage class that supports `ReadWriteOnce` (for databases)
34
-
- Ingress controller installed (nginx-ingress or traefik)
35
-
- cert-manager installed for TLS certificates
36
-
### Quick Prerequisites Setup
37
-
If you need to install prerequisites:
38
-
```bash
39
-
# Install nginx-ingress (chart v4.14.1 - December 2025)
40
-
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
41
-
helm repo update
42
-
helm install ingress-nginx ingress-nginx/ingress-nginx \
43
-
--namespace ingress-nginx --create-namespace \
44
-
--version 4.14.1
45
-
# Install cert-manager (v1.19.2 - December 2025)
46
-
helm repo add jetstack https://charts.jetstack.io
47
-
helm repo update
48
-
helm install cert-manager jetstack/cert-manager \
49
-
--namespace cert-manager --create-namespace \
50
-
--version v1.19.2 \
51
-
--set installCRDs=true
52
-
```
53
-
---
54
-
## 1. Create Namespace
55
-
```bash
56
-
kubectl create namespace bspds
57
-
kubectl config set-context --current --namespace=bspds
58
-
```
59
-
## 2. Create Secrets
60
-
Generate secure passwords and secrets:
61
-
```bash
62
-
# Generate secrets
63
-
DB_PASSWORD=$(openssl rand -base64 32)
64
-
MINIO_PASSWORD=$(openssl rand -base64 32)
65
-
JWT_SECRET=$(openssl rand -base64 48)
66
-
DPOP_SECRET=$(openssl rand -base64 48)
67
-
MASTER_KEY=$(openssl rand -base64 48)
68
-
# Create Kubernetes secrets
69
-
kubectl create secret generic bspds-db-credentials \
70
-
--from-literal=username=bspds \
71
-
--from-literal=password="$DB_PASSWORD"
72
-
kubectl create secret generic bspds-minio-credentials \
73
-
--from-literal=root-user=minioadmin \
74
-
--from-literal=root-password="$MINIO_PASSWORD"
75
-
kubectl create secret generic bspds-secrets \
76
-
--from-literal=jwt-secret="$JWT_SECRET" \
77
-
--from-literal=dpop-secret="$DPOP_SECRET" \
78
-
--from-literal=master-key="$MASTER_KEY"
79
-
# Save secrets locally (KEEP SECURE!)
80
-
echo "DB_PASSWORD=$DB_PASSWORD" > secrets.txt
81
-
echo "MINIO_PASSWORD=$MINIO_PASSWORD" >> secrets.txt
82
-
echo "JWT_SECRET=$JWT_SECRET" >> secrets.txt
83
-
echo "DPOP_SECRET=$DPOP_SECRET" >> secrets.txt
84
-
echo "MASTER_KEY=$MASTER_KEY" >> secrets.txt
85
-
chmod 600 secrets.txt
86
-
```
87
-
## 3. Deploy PostgreSQL
88
-
### Option A: CloudNativePG Operator (Recommended for HA)
89
-
```bash
90
-
# Install CloudNativePG operator (v1.28.0 - December 2025)
91
-
kubectl apply --server-side -f \
92
-
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.28/releases/cnpg-1.28.0.yaml
93
-
# Wait for operator
94
-
kubectl wait --for=condition=available --timeout=120s \
95
-
deployment/cnpg-controller-manager -n cnpg-system
96
-
```
97
-
```bash
98
-
cat <<EOF | kubectl apply -f -
99
-
apiVersion: postgresql.cnpg.io/v1
100
-
kind: Cluster
101
-
metadata:
102
-
name: bspds-db
103
-
namespace: bspds
104
-
spec:
105
-
instances: 3
106
-
postgresql:
107
-
parameters:
108
-
max_connections: "200"
109
-
shared_buffers: "256MB"
110
-
bootstrap:
111
-
initdb:
112
-
database: pds
113
-
owner: bspds
114
-
secret:
115
-
name: bspds-db-credentials
116
-
storage:
117
-
size: 20Gi
118
-
storageClass: standard # adjust for your cluster
119
-
resources:
120
-
requests:
121
-
memory: "512Mi"
122
-
cpu: "250m"
123
-
limits:
124
-
memory: "1Gi"
125
-
cpu: "1000m"
126
-
affinity:
127
-
podAntiAffinityType: required
128
-
EOF
129
-
```
130
-
### Option B: Simple StatefulSet (Single Instance)
131
-
```bash
132
-
cat <<EOF | kubectl apply -f -
133
-
apiVersion: v1
134
-
kind: PersistentVolumeClaim
135
-
metadata:
136
-
name: bspds-db-pvc
137
-
namespace: bspds
138
-
spec:
139
-
accessModes:
140
-
- ReadWriteOnce
141
-
resources:
142
-
requests:
143
-
storage: 20Gi
144
-
---
145
-
apiVersion: apps/v1
146
-
kind: StatefulSet
147
-
metadata:
148
-
name: bspds-db
149
-
namespace: bspds
150
-
spec:
151
-
serviceName: bspds-db
152
-
replicas: 1
153
-
selector:
154
-
matchLabels:
155
-
app: bspds-db
156
-
template:
157
-
metadata:
158
-
labels:
159
-
app: bspds-db
160
-
spec:
161
-
containers:
162
-
- name: postgres
163
-
image: postgres:18-alpine
164
-
ports:
165
-
- containerPort: 5432
166
-
env:
167
-
- name: POSTGRES_DB
168
-
value: pds
169
-
- name: POSTGRES_USER
170
-
valueFrom:
171
-
secretKeyRef:
172
-
name: bspds-db-credentials
173
-
key: username
174
-
- name: POSTGRES_PASSWORD
175
-
valueFrom:
176
-
secretKeyRef:
177
-
name: bspds-db-credentials
178
-
key: password
179
-
- name: PGDATA
180
-
value: /var/lib/postgresql/data/pgdata
181
-
volumeMounts:
182
-
- name: data
183
-
mountPath: /var/lib/postgresql/data
184
-
resources:
185
-
requests:
186
-
memory: "256Mi"
187
-
cpu: "100m"
188
-
limits:
189
-
memory: "1Gi"
190
-
cpu: "500m"
191
-
livenessProbe:
192
-
exec:
193
-
command: ["pg_isready", "-U", "bspds", "-d", "pds"]
194
-
initialDelaySeconds: 30
195
-
periodSeconds: 10
196
-
readinessProbe:
197
-
exec:
198
-
command: ["pg_isready", "-U", "bspds", "-d", "pds"]
199
-
initialDelaySeconds: 5
200
-
periodSeconds: 5
201
-
volumes:
202
-
- name: data
203
-
persistentVolumeClaim:
204
-
claimName: bspds-db-pvc
205
-
---
206
-
apiVersion: v1
207
-
kind: Service
208
-
metadata:
209
-
name: bspds-db-rw
210
-
namespace: bspds
211
-
spec:
212
-
selector:
213
-
app: bspds-db
214
-
ports:
215
-
- port: 5432
216
-
targetPort: 5432
217
-
EOF
218
-
```
219
-
## 4. Deploy MinIO
220
-
```bash
221
-
cat <<EOF | kubectl apply -f -
222
-
apiVersion: v1
223
-
kind: PersistentVolumeClaim
224
-
metadata:
225
-
name: bspds-minio-pvc
226
-
namespace: bspds
227
-
spec:
228
-
accessModes:
229
-
- ReadWriteOnce
230
-
resources:
231
-
requests:
232
-
storage: 50Gi
233
-
---
234
-
apiVersion: apps/v1
235
-
kind: StatefulSet
236
-
metadata:
237
-
name: bspds-minio
238
-
namespace: bspds
239
-
spec:
240
-
serviceName: bspds-minio
241
-
replicas: 1
242
-
selector:
243
-
matchLabels:
244
-
app: bspds-minio
245
-
template:
246
-
metadata:
247
-
labels:
248
-
app: bspds-minio
249
-
spec:
250
-
containers:
251
-
- name: minio
252
-
image: minio/minio:RELEASE.2025-10-15T17-29-55Z
253
-
args:
254
-
- server
255
-
- /data
256
-
- --console-address
257
-
- ":9001"
258
-
ports:
259
-
- containerPort: 9000
260
-
name: api
261
-
- containerPort: 9001
262
-
name: console
263
-
env:
264
-
- name: MINIO_ROOT_USER
265
-
valueFrom:
266
-
secretKeyRef:
267
-
name: bspds-minio-credentials
268
-
key: root-user
269
-
- name: MINIO_ROOT_PASSWORD
270
-
valueFrom:
271
-
secretKeyRef:
272
-
name: bspds-minio-credentials
273
-
key: root-password
274
-
volumeMounts:
275
-
- name: data
276
-
mountPath: /data
277
-
resources:
278
-
requests:
279
-
memory: "256Mi"
280
-
cpu: "100m"
281
-
limits:
282
-
memory: "512Mi"
283
-
cpu: "500m"
284
-
livenessProbe:
285
-
httpGet:
286
-
path: /minio/health/live
287
-
port: 9000
288
-
initialDelaySeconds: 30
289
-
periodSeconds: 10
290
-
readinessProbe:
291
-
httpGet:
292
-
path: /minio/health/ready
293
-
port: 9000
294
-
initialDelaySeconds: 10
295
-
periodSeconds: 5
296
-
volumes:
297
-
- name: data
298
-
persistentVolumeClaim:
299
-
claimName: bspds-minio-pvc
300
-
---
301
-
apiVersion: v1
302
-
kind: Service
303
-
metadata:
304
-
name: bspds-minio
305
-
namespace: bspds
306
-
spec:
307
-
selector:
308
-
app: bspds-minio
309
-
ports:
310
-
- port: 9000
311
-
targetPort: 9000
312
-
name: api
313
-
- port: 9001
314
-
targetPort: 9001
315
-
name: console
316
-
EOF
317
-
```
318
-
### Initialize MinIO Bucket
319
-
```bash
320
-
kubectl run minio-init --rm -it --restart=Never \
321
-
--image=minio/mc:RELEASE.2025-07-16T15-35-03Z \
322
-
--env="MINIO_ROOT_USER=minioadmin" \
323
-
--env="MINIO_ROOT_PASSWORD=$(kubectl get secret bspds-minio-credentials -o jsonpath='{.data.root-password}' | base64 -d)" \
324
-
--command -- sh -c "
325
-
mc alias set local http://bspds-minio:9000 \$MINIO_ROOT_USER \$MINIO_ROOT_PASSWORD &&
326
-
mc mb --ignore-existing local/pds-blobs
327
-
"
328
-
```
329
-
## 5. Deploy Valkey
330
-
```bash
331
-
cat <<EOF | kubectl apply -f -
332
-
apiVersion: v1
333
-
kind: PersistentVolumeClaim
334
-
metadata:
335
-
name: bspds-valkey-pvc
336
-
namespace: bspds
337
-
spec:
338
-
accessModes:
339
-
- ReadWriteOnce
340
-
resources:
341
-
requests:
342
-
storage: 5Gi
343
-
---
344
-
apiVersion: apps/v1
345
-
kind: StatefulSet
346
-
metadata:
347
-
name: bspds-valkey
348
-
namespace: bspds
349
-
spec:
350
-
serviceName: bspds-valkey
351
-
replicas: 1
352
-
selector:
353
-
matchLabels:
354
-
app: bspds-valkey
355
-
template:
356
-
metadata:
357
-
labels:
358
-
app: bspds-valkey
359
-
spec:
360
-
containers:
361
-
- name: valkey
362
-
image: valkey/valkey:9-alpine
363
-
args:
364
-
- valkey-server
365
-
- --appendonly
366
-
- "yes"
367
-
- --maxmemory
368
-
- "256mb"
369
-
- --maxmemory-policy
370
-
- allkeys-lru
371
-
ports:
372
-
- containerPort: 6379
373
-
volumeMounts:
374
-
- name: data
375
-
mountPath: /data
376
-
resources:
377
-
requests:
378
-
memory: "128Mi"
379
-
cpu: "50m"
380
-
limits:
381
-
memory: "300Mi"
382
-
cpu: "200m"
383
-
livenessProbe:
384
-
exec:
385
-
command: ["valkey-cli", "ping"]
386
-
initialDelaySeconds: 10
387
-
periodSeconds: 5
388
-
readinessProbe:
389
-
exec:
390
-
command: ["valkey-cli", "ping"]
391
-
initialDelaySeconds: 5
392
-
periodSeconds: 3
393
-
volumes:
394
-
- name: data
395
-
persistentVolumeClaim:
396
-
claimName: bspds-valkey-pvc
397
-
---
398
-
apiVersion: v1
399
-
kind: Service
400
-
metadata:
401
-
name: bspds-valkey
402
-
namespace: bspds
403
-
spec:
404
-
selector:
405
-
app: bspds-valkey
406
-
ports:
407
-
- port: 6379
408
-
targetPort: 6379
409
-
EOF
410
-
```
411
-
## 6. Build and Push BSPDS Image
412
-
```bash
413
-
# Build image
414
-
cd /path/to/bspds
415
-
docker build -t your-registry.com/bspds:latest .
416
-
docker push your-registry.com/bspds:latest
417
-
```
418
-
If using a private registry, create an image pull secret:
419
-
```bash
420
-
kubectl create secret docker-registry regcred \
421
-
--docker-server=your-registry.com \
422
-
--docker-username=your-username \
423
-
--docker-password=your-password \
424
-
--docker-email=your-email
425
-
```
426
-
## 7. Run Database Migrations
427
-
BSPDS runs migrations automatically on startup. However, if you want to run migrations separately (recommended for zero-downtime deployments), you can use a Job:
428
-
```bash
429
-
cat <<'EOF' | kubectl apply -f -
430
-
apiVersion: batch/v1
431
-
kind: Job
432
-
metadata:
433
-
name: bspds-migrate
434
-
namespace: bspds
435
-
spec:
436
-
ttlSecondsAfterFinished: 300
437
-
template:
438
-
spec:
439
-
restartPolicy: Never
440
-
containers:
441
-
- name: migrate
442
-
image: your-registry.com/bspds:latest
443
-
command: ["/usr/local/bin/bspds"]
444
-
args: ["--migrate-only"] # Add this flag to your app, or remove this Job
445
-
env:
446
-
- name: DB_PASSWORD
447
-
valueFrom:
448
-
secretKeyRef:
449
-
name: bspds-db-credentials
450
-
key: password
451
-
- name: DATABASE_URL
452
-
value: "postgres://bspds:$(DB_PASSWORD)@bspds-db-rw:5432/pds"
453
-
EOF
454
-
kubectl wait --for=condition=complete --timeout=120s job/bspds-migrate
455
-
```
456
-
> **Note**: If your BSPDS image doesn't have a `--migrate-only` flag, you can skip this step. The app will run migrations on first startup. Alternatively, build a separate migration image with `sqlx-cli` installed.
457
-
## 8. Deploy BSPDS Application
458
-
```bash
459
-
cat <<EOF | kubectl apply -f -
460
-
apiVersion: v1
461
-
kind: ConfigMap
462
-
metadata:
463
-
name: bspds-config
464
-
namespace: bspds
465
-
data:
466
-
PDS_HOSTNAME: "pds.example.com"
467
-
SERVER_HOST: "0.0.0.0"
468
-
SERVER_PORT: "3000"
469
-
S3_ENDPOINT: "http://bspds-minio:9000"
470
-
AWS_REGION: "us-east-1"
471
-
S3_BUCKET: "pds-blobs"
472
-
VALKEY_URL: "redis://bspds-valkey:6379"
473
-
APPVIEW_URL: "https://api.bsky.app"
474
-
CRAWLERS: "https://bsky.network"
475
-
FRONTEND_DIR: "/app/frontend/dist"
476
-
---
477
-
apiVersion: apps/v1
478
-
kind: Deployment
479
-
metadata:
480
-
name: bspds
481
-
namespace: bspds
482
-
spec:
483
-
replicas: 2
484
-
selector:
485
-
matchLabels:
486
-
app: bspds
487
-
template:
488
-
metadata:
489
-
labels:
490
-
app: bspds
491
-
spec:
492
-
imagePullSecrets:
493
-
- name: regcred # Remove if using public registry
494
-
affinity:
495
-
podAntiAffinity:
496
-
preferredDuringSchedulingIgnoredDuringExecution:
497
-
- weight: 100
498
-
podAffinityTerm:
499
-
labelSelector:
500
-
matchLabels:
501
-
app: bspds
502
-
topologyKey: kubernetes.io/hostname
503
-
containers:
504
-
- name: bspds
505
-
image: your-registry.com/bspds:latest
506
-
ports:
507
-
- containerPort: 3000
508
-
name: http
509
-
envFrom:
510
-
- configMapRef:
511
-
name: bspds-config
512
-
env:
513
-
- name: DB_PASSWORD
514
-
valueFrom:
515
-
secretKeyRef:
516
-
name: bspds-db-credentials
517
-
key: password
518
-
- name: DATABASE_URL
519
-
value: "postgres://bspds:$(DB_PASSWORD)@bspds-db-rw:5432/pds"
520
-
- name: AWS_ACCESS_KEY_ID
521
-
valueFrom:
522
-
secretKeyRef:
523
-
name: bspds-minio-credentials
524
-
key: root-user
525
-
- name: AWS_SECRET_ACCESS_KEY
526
-
valueFrom:
527
-
secretKeyRef:
528
-
name: bspds-minio-credentials
529
-
key: root-password
530
-
- name: JWT_SECRET
531
-
valueFrom:
532
-
secretKeyRef:
533
-
name: bspds-secrets
534
-
key: jwt-secret
535
-
- name: DPOP_SECRET
536
-
valueFrom:
537
-
secretKeyRef:
538
-
name: bspds-secrets
539
-
key: dpop-secret
540
-
- name: MASTER_KEY
541
-
valueFrom:
542
-
secretKeyRef:
543
-
name: bspds-secrets
544
-
key: master-key
545
-
resources:
546
-
requests:
547
-
memory: "256Mi"
548
-
cpu: "100m"
549
-
limits:
550
-
memory: "1Gi"
551
-
cpu: "1000m"
552
-
livenessProbe:
553
-
httpGet:
554
-
path: /xrpc/_health
555
-
port: 3000
556
-
initialDelaySeconds: 30
557
-
periodSeconds: 10
558
-
failureThreshold: 3
559
-
readinessProbe:
560
-
httpGet:
561
-
path: /xrpc/_health
562
-
port: 3000
563
-
initialDelaySeconds: 5
564
-
periodSeconds: 5
565
-
failureThreshold: 3
566
-
securityContext:
567
-
runAsNonRoot: true
568
-
runAsUser: 1000
569
-
allowPrivilegeEscalation: false
570
-
---
571
-
apiVersion: v1
572
-
kind: Service
573
-
metadata:
574
-
name: bspds
575
-
namespace: bspds
576
-
spec:
577
-
selector:
578
-
app: bspds
579
-
ports:
580
-
- port: 80
581
-
targetPort: 3000
582
-
name: http
583
-
EOF
584
-
```
585
-
## 9. Configure Horizontal Pod Autoscaler
586
-
```bash
587
-
cat <<EOF | kubectl apply -f -
588
-
apiVersion: autoscaling/v2
589
-
kind: HorizontalPodAutoscaler
590
-
metadata:
591
-
name: bspds
592
-
namespace: bspds
593
-
spec:
594
-
scaleTargetRef:
595
-
apiVersion: apps/v1
596
-
kind: Deployment
597
-
name: bspds
598
-
minReplicas: 2
599
-
maxReplicas: 10
600
-
metrics:
601
-
- type: Resource
602
-
resource:
603
-
name: cpu
604
-
target:
605
-
type: Utilization
606
-
averageUtilization: 70
607
-
- type: Resource
608
-
resource:
609
-
name: memory
610
-
target:
611
-
type: Utilization
612
-
averageUtilization: 80
613
-
behavior:
614
-
scaleDown:
615
-
stabilizationWindowSeconds: 300
616
-
policies:
617
-
- type: Pods
618
-
value: 1
619
-
periodSeconds: 60
620
-
scaleUp:
621
-
stabilizationWindowSeconds: 0
622
-
policies:
623
-
- type: Percent
624
-
value: 100
625
-
periodSeconds: 15
626
-
- type: Pods
627
-
value: 4
628
-
periodSeconds: 15
629
-
selectPolicy: Max
630
-
EOF
631
-
```
632
-
## 10. Configure Pod Disruption Budget
633
-
```bash
634
-
cat <<EOF | kubectl apply -f -
635
-
apiVersion: policy/v1
636
-
kind: PodDisruptionBudget
637
-
metadata:
638
-
name: bspds
639
-
namespace: bspds
640
-
spec:
641
-
minAvailable: 1
642
-
selector:
643
-
matchLabels:
644
-
app: bspds
645
-
EOF
646
-
```
647
-
## 11. Configure TLS with cert-manager
648
-
```bash
649
-
cat <<EOF | kubectl apply -f -
650
-
apiVersion: cert-manager.io/v1
651
-
kind: ClusterIssuer
652
-
metadata:
653
-
name: letsencrypt-prod
654
-
spec:
655
-
acme:
656
-
server: https://acme-v02.api.letsencrypt.org/directory
657
-
email: your-email@example.com
658
-
privateKeySecretRef:
659
-
name: letsencrypt-prod
660
-
solvers:
661
-
- http01:
662
-
ingress:
663
-
class: nginx
664
-
EOF
665
-
```
666
-
## 12. Configure Ingress
667
-
```bash
668
-
cat <<EOF | kubectl apply -f -
669
-
apiVersion: networking.k8s.io/v1
670
-
kind: Ingress
671
-
metadata:
672
-
name: bspds
673
-
namespace: bspds
674
-
annotations:
675
-
cert-manager.io/cluster-issuer: letsencrypt-prod
676
-
nginx.ingress.kubernetes.io/proxy-read-timeout: "86400"
677
-
nginx.ingress.kubernetes.io/proxy-send-timeout: "86400"
678
-
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
679
-
nginx.ingress.kubernetes.io/proxy-buffering: "off"
680
-
nginx.ingress.kubernetes.io/websocket-services: "bspds"
681
-
spec:
682
-
ingressClassName: nginx
683
-
tls:
684
-
- hosts:
685
-
- pds.example.com
686
-
secretName: bspds-tls
687
-
rules:
688
-
- host: pds.example.com
689
-
http:
690
-
paths:
691
-
- path: /
692
-
pathType: Prefix
693
-
backend:
694
-
service:
695
-
name: bspds
696
-
port:
697
-
number: 80
698
-
EOF
699
-
```
700
-
## 13. Configure Network Policies (Optional but Recommended)
701
-
```bash
702
-
cat <<EOF | kubectl apply -f -
703
-
apiVersion: networking.k8s.io/v1
704
-
kind: NetworkPolicy
705
-
metadata:
706
-
name: bspds-network-policy
707
-
namespace: bspds
708
-
spec:
709
-
podSelector:
710
-
matchLabels:
711
-
app: bspds
712
-
policyTypes:
713
-
- Ingress
714
-
- Egress
715
-
ingress:
716
-
- from:
717
-
- namespaceSelector:
718
-
matchLabels:
719
-
kubernetes.io/metadata.name: ingress-nginx
720
-
ports:
721
-
- protocol: TCP
722
-
port: 3000
723
-
egress:
724
-
- to:
725
-
- podSelector:
726
-
matchLabels:
727
-
app: bspds-db
728
-
ports:
729
-
- protocol: TCP
730
-
port: 5432
731
-
- to:
732
-
- podSelector:
733
-
matchLabels:
734
-
app: bspds-minio
735
-
ports:
736
-
- protocol: TCP
737
-
port: 9000
738
-
- to:
739
-
- podSelector:
740
-
matchLabels:
741
-
app: bspds-valkey
742
-
ports:
743
-
- protocol: TCP
744
-
port: 6379
745
-
- to: # Allow DNS
746
-
- namespaceSelector: {}
747
-
podSelector:
748
-
matchLabels:
749
-
k8s-app: kube-dns
750
-
ports:
751
-
- protocol: UDP
752
-
port: 53
753
-
- to: # Allow external HTTPS (for federation)
754
-
- ipBlock:
755
-
cidr: 0.0.0.0/0
756
-
ports:
757
-
- protocol: TCP
758
-
port: 443
759
-
EOF
760
-
```
761
-
## 14. Deploy Prometheus Monitoring (Optional)
762
-
```bash
763
-
cat <<EOF | kubectl apply -f -
764
-
apiVersion: monitoring.coreos.com/v1
765
-
kind: ServiceMonitor
766
-
metadata:
767
-
name: bspds
768
-
namespace: bspds
769
-
labels:
770
-
release: prometheus
771
-
spec:
772
-
selector:
773
-
matchLabels:
774
-
app: bspds
775
-
endpoints:
776
-
- port: http
777
-
path: /metrics
778
-
interval: 30s
779
-
EOF
780
-
```
781
-
---
782
-
## Verification
783
-
```bash
784
-
# Check all pods are running
785
-
kubectl get pods -n bspds
786
-
# Check services
787
-
kubectl get svc -n bspds
788
-
# Check ingress
789
-
kubectl get ingress -n bspds
790
-
# Check certificate
791
-
kubectl get certificate -n bspds
792
-
# Test health endpoint
793
-
curl -s https://pds.example.com/xrpc/_health | jq
794
-
# Test DID endpoint
795
-
curl -s https://pds.example.com/.well-known/atproto-did
796
-
```
797
-
---
798
-
## Maintenance
799
-
### View Logs
800
-
```bash
801
-
# All BSPDS pods
802
-
kubectl logs -l app=bspds -n bspds -f
803
-
# Specific pod
804
-
kubectl logs -f deployment/bspds -n bspds
805
-
```
806
-
### Scale Manually
807
-
```bash
808
-
kubectl scale deployment bspds --replicas=5 -n bspds
809
-
```
810
-
### Update BSPDS
811
-
```bash
812
-
# Build and push new image
813
-
docker build -t your-registry.com/bspds:v1.2.3 .
814
-
docker push your-registry.com/bspds:v1.2.3
815
-
# Update deployment
816
-
kubectl set image deployment/bspds bspds=your-registry.com/bspds:v1.2.3 -n bspds
817
-
# Watch rollout
818
-
kubectl rollout status deployment/bspds -n bspds
819
-
```
820
-
### Backup Database
821
-
```bash
822
-
# For CloudNativePG
823
-
kubectl cnpg backup bspds-db -n bspds
824
-
# For StatefulSet
825
-
kubectl exec -it bspds-db-0 -n bspds -- pg_dump -U bspds pds > backup-$(date +%Y%m%d).sql
826
-
```
827
-
### Run Migrations
828
-
If you have a migration Job defined, you can re-run it:
829
-
```bash
830
-
# Delete old job first (if exists)
831
-
kubectl delete job bspds-migrate -n bspds --ignore-not-found
832
-
# Re-apply the migration job from step 7
833
-
# Or simply restart the deployment - BSPDS runs migrations on startup
834
-
kubectl rollout restart deployment/bspds -n bspds
835
-
```
836
-
---
837
-
## Troubleshooting
838
-
### Pod Won't Start
839
-
```bash
840
-
kubectl describe pod -l app=bspds -n bspds
841
-
kubectl logs -l app=bspds -n bspds --previous
842
-
```
843
-
### Database Connection Issues
844
-
```bash
845
-
# Test connectivity from a debug pod
846
-
kubectl run debug --rm -it --restart=Never --image=postgres:18-alpine -- \
847
-
psql "postgres://bspds:PASSWORD@bspds-db-rw:5432/pds" -c "SELECT 1"
848
-
```
849
-
### Certificate Issues
850
-
```bash
851
-
kubectl describe certificate bspds-tls -n bspds
852
-
kubectl describe certificaterequest -n bspds
853
-
kubectl logs -l app.kubernetes.io/name=cert-manager -n cert-manager
854
-
```
855
-
### View Resource Usage
856
-
```bash
857
-
kubectl top pods -n bspds
858
-
kubectl top nodes
859
-
```
1
+
# BSPDS on Kubernetes
2
+
3
+
If you're reaching for kubernetes for this app, you're experienced enough to know how to spin up:
4
+
5
+
- cloudnativepg (or your preferred postgres operator)
6
+
- valkey
7
+
- s3-compatible object storage (minio operator, or just use a managed service)
8
+
- the app itself (it's just a container with some env vars)
9
+
10
+
You'll need a wildcard TLS certificate for `*.your-pds-hostname.example.com` — user handles are served as subdomains.
11
+
12
+
The container image expects:
13
+
- `DATABASE_URL` - postgres connection string
14
+
- `S3_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `S3_BUCKET`
15
+
- `VALKEY_URL` - redis:// connection string
16
+
- `PDS_HOSTNAME` - your PDS hostname (without protocol)
17
+
- `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48`
18
+
- `APPVIEW_URL` - typically `https://api.bsky.app`
19
+
- `CRAWLERS` - typically `https://bsky.network`
20
+
and more, check the .env.example.
21
+
22
+
Health check: `GET /xrpc/_health`
23
+
+21
-25
docs/install-openbsd.md
+21
-25
docs/install-openbsd.md
···
4
4
## Prerequisites
5
5
- A VPS with at least 2GB RAM and 20GB disk
6
6
- A domain name pointing to your server's IP
7
+
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
7
8
- Root access (or doas configured)
8
9
## Why nginx over relayd?
9
10
OpenBSD's native `relayd` supports WebSockets but does **not** support HTTP/2. For a modern PDS deployment, we recommend nginx which provides HTTP/2, WebSocket support, and automatic OCSP stapling.
···
80
81
mc mb local/pds-blobs
81
82
```
82
83
## 5. Install redis
83
-
OpenBSD has redis in ports (valkey may not be available yet):
84
+
OpenBSD has redis in ports (valkey not available yet):
84
85
```sh
85
86
pkg_add redis
86
87
rcctl enable redis
···
194
195
mkdir -p /var/www/acme
195
196
rcctl enable nginx
196
197
```
197
-
## 12. Obtain SSL Certificate with acme-client
198
-
OpenBSD's native acme-client works well:
198
+
## 12. Obtain Wildcard SSL Certificate
199
+
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate.
200
+
201
+
OpenBSD's native `acme-client` only supports HTTP-01 validation, which can't issue wildcard certs. You have a few options:
202
+
203
+
**Option A: Use certbot with DNS validation (recommended)**
199
204
```sh
200
-
cat >> /etc/acme-client.conf << 'EOF'
201
-
authority letsencrypt {
202
-
api url "https://acme-v02.api.letsencrypt.org/directory"
203
-
account key "/etc/acme/letsencrypt-privkey.pem"
204
-
}
205
-
domain pds.example.com {
206
-
domain key "/etc/ssl/private/pds.example.com.key"
207
-
domain full chain certificate "/etc/ssl/pds.example.com.fullchain.pem"
208
-
sign with letsencrypt
209
-
}
210
-
EOF
211
-
mkdir -p /etc/acme
212
-
rcctl start nginx
213
-
acme-client -v pds.example.com
214
-
rcctl restart nginx
205
+
pkg_add certbot
206
+
certbot certonly --manual --preferred-challenges dns \
207
+
-d pds.example.com -d '*.pds.example.com'
215
208
```
216
-
Set up auto-renewal in root's crontab:
209
+
Follow the prompts to add TXT records to your DNS. Then update nginx.conf to point to the certbot certs.
210
+
211
+
**Option B: Use a managed DNS provider with API**
212
+
If your DNS provider has a certbot plugin, you can automate renewal.
213
+
214
+
**Option C: Use acme.sh**
215
+
[acme.sh](https://github.com/acmesh-official/acme.sh) supports many DNS providers for automated wildcard cert renewal.
216
+
217
+
After obtaining the cert, update nginx to use it and restart:
217
218
```sh
218
-
crontab -e
219
-
```
220
-
Add:
221
-
```
222
-
0 0 * * * acme-client pds.example.com && rcctl reload nginx
219
+
rcctl restart nginx
223
220
```
224
221
## 13. Configure Packet Filter (pf)
225
222
```sh
226
223
cat >> /etc/pf.conf << 'EOF'
227
-
# BSPDS rules
228
224
pass in on egress proto tcp from any to any port { 22, 80, 443 }
229
225
EOF
230
226
pfctl -f /etc/pf.conf
+224
-280
scripts/install-debian.sh
+224
-280
scripts/install-debian.sh
···
1
1
#!/bin/bash
2
2
set -euo pipefail
3
+
3
4
RED='\033[0;31m'
4
5
GREEN='\033[0;32m'
5
6
YELLOW='\033[1;33m'
6
7
BLUE='\033[0;34m'
7
-
CYAN='\033[0;36m'
8
8
NC='\033[0m'
9
+
9
10
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
10
11
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
11
12
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
12
13
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
14
+
13
15
if [[ $EUID -ne 0 ]]; then
14
16
log_error "This script must be run as root"
15
17
exit 1
16
18
fi
19
+
17
20
if ! grep -qi "debian" /etc/os-release 2>/dev/null; then
18
21
log_warn "This script is designed for Debian. Proceed with caution on other distros."
19
22
fi
23
+
20
24
nuke_installation() {
21
-
echo -e "${RED}"
22
-
echo "╔═══════════════════════════════════════════════════════════════════╗"
23
-
echo "║ NUKING EXISTING INSTALLATION ║"
24
-
echo "╚═══════════════════════════════════════════════════════════════════╝"
25
-
echo -e "${NC}"
25
+
log_warn "NUKING EXISTING INSTALLATION"
26
26
log_info "Stopping services..."
27
27
systemctl stop bspds 2>/dev/null || true
28
28
systemctl disable bspds 2>/dev/null || true
29
+
29
30
log_info "Removing BSPDS files..."
30
31
rm -rf /opt/bspds
31
32
rm -rf /var/lib/bspds
···
35
36
rm -rf /var/spool/bspds-mail
36
37
rm -f /etc/systemd/system/bspds.service
37
38
systemctl daemon-reload
39
+
38
40
log_info "Removing BSPDS configuration..."
39
41
rm -rf /etc/bspds
42
+
40
43
log_info "Dropping postgres database and user..."
41
44
sudo -u postgres psql -c "DROP DATABASE IF EXISTS pds;" 2>/dev/null || true
42
45
sudo -u postgres psql -c "DROP USER IF EXISTS bspds;" 2>/dev/null || true
43
-
log_info "Removing minio bucket and resetting minio..."
46
+
47
+
log_info "Removing minio bucket..."
44
48
if command -v mc &>/dev/null; then
45
49
mc rb local/pds-blobs --force 2>/dev/null || true
46
50
mc alias remove local 2>/dev/null || true
···
48
52
systemctl stop minio 2>/dev/null || true
49
53
rm -rf /var/lib/minio/data/.minio.sys 2>/dev/null || true
50
54
rm -f /etc/default/minio 2>/dev/null || true
55
+
51
56
log_info "Removing nginx config..."
52
57
rm -f /etc/nginx/sites-enabled/bspds
53
58
rm -f /etc/nginx/sites-available/bspds
54
59
systemctl reload nginx 2>/dev/null || true
55
-
log_success "Previous installation nuked!"
56
-
echo ""
60
+
61
+
log_success "Previous installation nuked"
57
62
}
63
+
58
64
if [[ -f /etc/bspds/bspds.env ]] || [[ -d /opt/bspds ]] || [[ -f /usr/local/bin/bspds ]]; then
59
-
echo -e "${YELLOW}"
60
-
echo "╔═══════════════════════════════════════════════════════════════════╗"
61
-
echo "║ EXISTING INSTALLATION DETECTED ║"
62
-
echo "╚═══════════════════════════════════════════════════════════════════╝"
63
-
echo -e "${NC}"
65
+
log_warn "Existing installation detected"
64
66
echo ""
65
67
echo "Options:"
66
68
echo " 1) Nuke everything and start fresh (destroys database!)"
···
68
70
echo " 3) Exit"
69
71
echo ""
70
72
read -p "Choose an option [1/2/3]: " INSTALL_CHOICE
73
+
71
74
case "$INSTALL_CHOICE" in
72
75
1)
73
76
echo ""
74
-
echo -e "${RED}WARNING: This will DELETE:${NC}"
77
+
log_warn "This will DELETE:"
75
78
echo " - PostgreSQL database 'pds' and all data"
76
79
echo " - All BSPDS configuration and credentials"
77
80
echo " - All source code in /opt/bspds"
78
81
echo " - MinIO bucket 'pds-blobs' and all blobs"
79
-
echo " - Mail queue contents"
80
82
echo ""
81
-
read -p "Type 'NUKE' to confirm destruction: " CONFIRM_NUKE
83
+
read -p "Type 'NUKE' to confirm: " CONFIRM_NUKE
82
84
if [[ "$CONFIRM_NUKE" == "NUKE" ]]; then
83
85
nuke_installation
84
86
else
85
-
log_error "Nuke cancelled. Exiting."
87
+
log_error "Nuke cancelled"
86
88
exit 1
87
89
fi
88
90
;;
···
90
92
log_info "Continuing with existing installation..."
91
93
;;
92
94
3)
93
-
log_info "Exiting."
94
95
exit 0
95
96
;;
96
97
*)
97
-
log_error "Invalid option. Exiting."
98
+
log_error "Invalid option"
98
99
exit 1
99
100
;;
100
101
esac
101
102
fi
102
-
echo -e "${CYAN}"
103
-
echo "╔═══════════════════════════════════════════════════════════════════╗"
104
-
echo "║ BSPDS Installation Script for Debian ║"
105
-
echo "║ AT Protocol Personal Data Server in Rust ║"
106
-
echo "╚═══════════════════════════════════════════════════════════════════╝"
107
-
echo -e "${NC}"
103
+
104
+
echo ""
105
+
log_info "BSPDS Installation Script for Debian"
106
+
echo ""
107
+
108
108
get_public_ips() {
109
109
IPV4=$(curl -4 -s --max-time 5 ifconfig.me 2>/dev/null || curl -4 -s --max-time 5 icanhazip.com 2>/dev/null || echo "Could not detect")
110
-
IPV6=$(curl -6 -s --max-time 5 ifconfig.me 2>/dev/null || curl -6 -s --max-time 5 icanhazip.com 2>/dev/null || echo "Not available")
110
+
IPV6=$(curl -6 -s --max-time 5 ifconfig.me 2>/dev/null || curl -6 -s --max-time 5 icanhazip.com 2>/dev/null || echo "")
111
111
}
112
+
112
113
log_info "Detecting public IP addresses..."
113
114
get_public_ips
115
+
echo " IPv4: ${IPV4}"
116
+
[[ -n "$IPV6" ]] && echo " IPv6: ${IPV6}"
114
117
echo ""
115
-
echo -e "${CYAN}Your server's public IPs:${NC}"
116
-
echo -e " IPv4: ${GREEN}${IPV4}${NC}"
117
-
echo -e " IPv6: ${GREEN}${IPV6}${NC}"
118
-
echo ""
118
+
119
119
read -p "Enter your PDS domain (e.g., pds.example.com): " PDS_DOMAIN
120
120
if [[ -z "$PDS_DOMAIN" ]]; then
121
121
log_error "Domain cannot be empty"
122
122
exit 1
123
123
fi
124
-
read -p "Enter your email for Let's Encrypt notifications: " CERTBOT_EMAIL
124
+
125
+
read -p "Enter your email for Let's Encrypt: " CERTBOT_EMAIL
125
126
if [[ -z "$CERTBOT_EMAIL" ]]; then
126
127
log_error "Email cannot be empty"
127
128
exit 1
128
129
fi
129
-
echo ""
130
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
131
-
echo -e "${YELLOW}DNS RECORDS REQUIRED${NC}"
132
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
133
-
echo ""
134
-
echo "Before continuing, create these DNS records at your registrar:"
135
-
echo ""
136
-
echo -e "${GREEN}A Record:${NC}"
137
-
echo " Name: ${PDS_DOMAIN}"
138
-
echo " Type: A"
139
-
echo " Value: ${IPV4}"
130
+
140
131
echo ""
141
-
if [[ "$IPV6" != "Not available" ]]; then
142
-
echo -e "${GREEN}AAAA Record:${NC}"
143
-
echo " Name: ${PDS_DOMAIN}"
144
-
echo " Type: AAAA"
145
-
echo " Value: ${IPV6}"
132
+
log_info "DNS records required (create these now if you haven't):"
146
133
echo ""
147
-
fi
148
-
echo -e "${GREEN}Wildcard A Record (for user handles):${NC}"
149
-
echo " Name: *.${PDS_DOMAIN}"
150
-
echo " Type: A"
151
-
echo " Value: ${IPV4}"
152
-
echo ""
153
-
if [[ "$IPV6" != "Not available" ]]; then
154
-
echo -e "${GREEN}Wildcard AAAA Record (for user handles):${NC}"
155
-
echo " Name: *.${PDS_DOMAIN}"
156
-
echo " Type: AAAA"
157
-
echo " Value: ${IPV6}"
158
-
echo ""
159
-
fi
160
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
134
+
echo " ${PDS_DOMAIN} A ${IPV4}"
135
+
[[ -n "$IPV6" ]] && echo " ${PDS_DOMAIN} AAAA ${IPV6}"
136
+
echo " *.${PDS_DOMAIN} A ${IPV4} (for user handles)"
137
+
[[ -n "$IPV6" ]] && echo " *.${PDS_DOMAIN} AAAA ${IPV6} (for user handles)"
161
138
echo ""
162
139
read -p "Have you created these DNS records? (y/N): " DNS_CONFIRMED
163
140
if [[ ! "$DNS_CONFIRMED" =~ ^[Yy]$ ]]; then
164
141
log_warn "Please create the DNS records and run this script again."
165
142
exit 0
166
143
fi
144
+
167
145
CREDENTIALS_FILE="/etc/bspds/.credentials"
168
146
if [[ -f "$CREDENTIALS_FILE" ]]; then
169
-
log_info "Loading existing credentials from previous installation..."
147
+
log_info "Loading existing credentials..."
170
148
source "$CREDENTIALS_FILE"
171
-
log_success "Credentials loaded"
172
149
else
173
-
log_info "Generating secure secrets..."
150
+
log_info "Generating secrets..."
174
151
JWT_SECRET=$(openssl rand -base64 48)
175
152
DPOP_SECRET=$(openssl rand -base64 48)
176
153
MASTER_KEY=$(openssl rand -base64 48)
177
154
DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
178
155
MINIO_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
156
+
179
157
mkdir -p /etc/bspds
180
158
cat > "$CREDENTIALS_FILE" << EOF
181
159
JWT_SECRET="$JWT_SECRET"
···
185
163
MINIO_PASSWORD="$MINIO_PASSWORD"
186
164
EOF
187
165
chmod 600 "$CREDENTIALS_FILE"
188
-
log_success "Secrets generated and saved"
166
+
log_success "Secrets generated"
189
167
fi
168
+
190
169
log_info "Checking swap space..."
191
170
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
192
171
TOTAL_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
172
+
193
173
if [[ $TOTAL_SWAP_KB -lt 2000000 ]]; then
194
-
log_info "Adding swap space (needed for compilation)..."
195
174
if [[ ! -f /swapfile ]]; then
175
+
log_info "Adding swap space for compilation..."
196
176
SWAP_SIZE="4G"
197
-
if [[ $TOTAL_MEM_KB -lt 2000000 ]]; then
198
-
SWAP_SIZE="4G"
199
-
elif [[ $TOTAL_MEM_KB -lt 4000000 ]]; then
200
-
SWAP_SIZE="2G"
201
-
fi
177
+
[[ $TOTAL_MEM_KB -ge 4000000 ]] && SWAP_SIZE="2G"
202
178
fallocate -l $SWAP_SIZE /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=4096
203
179
chmod 600 /swapfile
204
180
mkswap /swapfile
205
181
swapon /swapfile
206
182
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
207
-
log_success "Swap space added ($SWAP_SIZE)"
183
+
log_success "Swap added ($SWAP_SIZE)"
208
184
else
209
185
swapon /swapfile 2>/dev/null || true
210
-
log_success "Existing swap enabled"
211
186
fi
212
-
else
213
-
log_success "Sufficient swap already configured"
214
187
fi
188
+
215
189
log_info "Updating system packages..."
216
190
apt update && apt upgrade -y
217
-
log_success "System updated"
191
+
218
192
log_info "Installing build dependencies..."
219
193
apt install -y curl git build-essential pkg-config libssl-dev ca-certificates gnupg lsb-release unzip xxd
220
-
log_success "Build dependencies installed"
194
+
221
195
log_info "Installing postgres..."
222
196
apt install -y postgresql postgresql-contrib
223
197
systemctl enable postgresql
···
226
200
sudo -u postgres psql -c "ALTER USER bspds WITH PASSWORD '${DB_PASSWORD}';"
227
201
sudo -u postgres psql -c "CREATE DATABASE pds OWNER bspds;" 2>/dev/null || true
228
202
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO bspds;"
229
-
log_success "postgres installed and configured"
203
+
log_success "postgres configured"
204
+
230
205
log_info "Installing valkey..."
231
-
apt install -y valkey || {
232
-
log_warn "valkey not in repos, trying redis..."
206
+
apt install -y valkey 2>/dev/null || {
207
+
log_warn "valkey not in repos, installing redis..."
233
208
apt install -y redis-server
234
209
systemctl enable redis-server
235
210
systemctl start redis-server
236
211
}
237
212
systemctl enable valkey-server 2>/dev/null || true
238
213
systemctl start valkey-server 2>/dev/null || true
239
-
log_success "valkey/redis installed"
214
+
240
215
log_info "Installing minio..."
241
216
if [[ ! -f /usr/local/bin/minio ]]; then
242
217
ARCH=$(dpkg --print-architecture)
243
-
if [[ "$ARCH" == "amd64" ]]; then
244
-
curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-amd64/minio
245
-
elif [[ "$ARCH" == "arm64" ]]; then
246
-
curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-arm64/minio
247
-
else
248
-
log_error "Unsupported architecture: $ARCH"
249
-
exit 1
250
-
fi
218
+
case "$ARCH" in
219
+
amd64) curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-amd64/minio ;;
220
+
arm64) curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-arm64/minio ;;
221
+
*) log_error "Unsupported architecture: $ARCH"; exit 1 ;;
222
+
esac
251
223
chmod +x /tmp/minio
252
224
mv /tmp/minio /usr/local/bin/
253
225
fi
226
+
254
227
mkdir -p /var/lib/minio/data
255
228
id -u minio-user &>/dev/null || useradd -r -s /sbin/nologin minio-user
256
229
chown -R minio-user:minio-user /var/lib/minio
230
+
257
231
cat > /etc/default/minio << EOF
258
232
MINIO_ROOT_USER=minioadmin
259
233
MINIO_ROOT_PASSWORD=${MINIO_PASSWORD}
···
261
235
MINIO_OPTS="--console-address :9001"
262
236
EOF
263
237
chmod 600 /etc/default/minio
238
+
264
239
cat > /etc/systemd/system/minio.service << 'EOF'
265
240
[Unit]
266
241
Description=MinIO Object Storage
267
242
After=network.target
243
+
268
244
[Service]
269
245
User=minio-user
270
246
Group=minio-user
···
272
248
ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
273
249
Restart=always
274
250
LimitNOFILE=65536
251
+
275
252
[Install]
276
253
WantedBy=multi-user.target
277
254
EOF
255
+
278
256
systemctl daemon-reload
279
257
systemctl enable minio
280
258
systemctl start minio
281
259
log_success "minio installed"
282
-
log_info "Waiting for minio to start..."
260
+
261
+
log_info "Waiting for minio..."
283
262
sleep 5
284
-
log_info "Installing minio client and creating bucket..."
263
+
285
264
if [[ ! -f /usr/local/bin/mc ]]; then
286
265
ARCH=$(dpkg --print-architecture)
287
-
if [[ "$ARCH" == "amd64" ]]; then
288
-
curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-amd64/mc
289
-
elif [[ "$ARCH" == "arm64" ]]; then
290
-
curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-arm64/mc
291
-
fi
266
+
case "$ARCH" in
267
+
amd64) curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-amd64/mc ;;
268
+
arm64) curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-arm64/mc ;;
269
+
esac
292
270
chmod +x /tmp/mc
293
271
mv /tmp/mc /usr/local/bin/
294
272
fi
273
+
295
274
mc alias remove local 2>/dev/null || true
296
275
mc alias set local http://localhost:9000 minioadmin "${MINIO_PASSWORD}" --api S3v4
297
276
mc mb local/pds-blobs --ignore-existing
298
277
log_success "minio bucket created"
278
+
299
279
log_info "Installing rust..."
300
280
if [[ -f "$HOME/.cargo/env" ]]; then
301
281
source "$HOME/.cargo/env"
···
304
284
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
305
285
source "$HOME/.cargo/env"
306
286
fi
307
-
log_success "rust installed"
287
+
308
288
log_info "Installing deno..."
309
289
export PATH="$HOME/.deno/bin:$PATH"
310
290
if ! command -v deno &>/dev/null && [[ ! -f "$HOME/.deno/bin/deno" ]]; then
311
291
curl -fsSL https://deno.land/install.sh | sh
312
292
grep -q 'deno/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
313
293
fi
314
-
log_success "deno installed"
294
+
315
295
log_info "Cloning BSPDS..."
316
296
if [[ ! -d /opt/bspds ]]; then
317
297
git clone https://tangled.org/lewis.moe/bspds-sandbox /opt/bspds
318
298
else
319
-
log_warn "/opt/bspds already exists, pulling latest..."
320
299
cd /opt/bspds && git pull
321
300
fi
322
301
cd /opt/bspds
323
-
log_success "BSPDS cloned"
302
+
324
303
log_info "Building frontend..."
325
-
cd /opt/bspds/frontend
326
-
"$HOME/.deno/bin/deno" task build
327
-
cd /opt/bspds
304
+
"$HOME/.deno/bin/deno" task build --filter=frontend
328
305
log_success "Frontend built"
329
-
log_info "Building BSPDS (this may take a while)..."
306
+
307
+
log_info "Building BSPDS (this takes a while)..."
330
308
source "$HOME/.cargo/env"
331
-
NPROC=$(nproc)
332
309
if [[ $TOTAL_MEM_KB -lt 4000000 ]]; then
333
-
log_info "Low memory detected, limiting parallel jobs..."
310
+
log_info "Low memory - limiting parallel jobs"
334
311
CARGO_BUILD_JOBS=1 cargo build --release
335
312
else
336
313
cargo build --release
337
314
fi
338
315
log_success "BSPDS built"
339
-
log_info "Installing sqlx-cli and running migrations..."
316
+
317
+
log_info "Running migrations..."
340
318
cargo install sqlx-cli --no-default-features --features postgres
341
319
export DATABASE_URL="postgres://bspds:${DB_PASSWORD}@localhost:5432/pds"
342
320
"$HOME/.cargo/bin/sqlx" migrate run
343
321
log_success "Migrations complete"
344
-
log_info "Setting up mail trap for testing..."
322
+
323
+
log_info "Setting up mail trap..."
345
324
mkdir -p /var/spool/bspds-mail
346
-
chown root:root /var/spool/bspds-mail
347
325
chmod 1777 /var/spool/bspds-mail
326
+
348
327
cat > /usr/local/bin/bspds-sendmail << 'SENDMAIL_EOF'
349
328
#!/bin/bash
350
329
MAIL_DIR="/var/spool/bspds-mail"
···
359
338
cat
360
339
} > "$MAIL_FILE"
361
340
chmod 644 "$MAIL_FILE"
362
-
echo "Mail saved to: $MAIL_FILE" >&2
363
341
exit 0
364
342
SENDMAIL_EOF
365
343
chmod +x /usr/local/bin/bspds-sendmail
344
+
366
345
cat > /usr/local/bin/bspds-mailq << 'MAILQ_EOF'
367
346
#!/bin/bash
368
347
MAIL_DIR="/var/spool/bspds-mail"
369
-
RED='\033[0;31m'
370
-
GREEN='\033[0;32m'
371
-
YELLOW='\033[1;33m'
372
-
BLUE='\033[0;34m'
373
-
CYAN='\033[0;36m'
374
-
NC='\033[0m'
375
-
show_help() {
376
-
echo "bspds-mailq - View captured emails from BSPDS mail trap"
377
-
echo ""
378
-
echo "Usage:"
379
-
echo " bspds-mailq List all captured emails"
380
-
echo " bspds-mailq <number> View email by number (from list)"
381
-
echo " bspds-mailq <filename> View email by filename"
382
-
echo " bspds-mailq latest View the most recent email"
383
-
echo " bspds-mailq clear Delete all captured emails"
384
-
echo " bspds-mailq watch Watch for new emails (tail -f style)"
385
-
echo " bspds-mailq count Show count of emails in queue"
386
-
echo ""
387
-
}
388
-
list_emails() {
389
-
if [[ ! -d "$MAIL_DIR" ]] || [[ -z "$(ls -A "$MAIL_DIR" 2>/dev/null)" ]]; then
390
-
echo -e "${YELLOW}No emails in queue.${NC}"
391
-
return
392
-
fi
393
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
394
-
echo -e "${GREEN} BSPDS Mail Queue${NC}"
395
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
396
-
echo ""
397
-
local i=1
398
-
for f in $(ls -t "$MAIL_DIR"/*.eml 2>/dev/null); do
399
-
local filename=$(basename "$f")
400
-
local received=$(grep "^X-BSPDS-Received:" "$f" 2>/dev/null | cut -d' ' -f2-)
401
-
local to=$(grep -i "^To:" "$f" 2>/dev/null | head -1 | cut -d' ' -f2-)
402
-
local subject=$(grep -i "^Subject:" "$f" 2>/dev/null | head -1 | sed 's/^Subject: *//')
403
-
echo -e "${BLUE}[$i]${NC} ${filename}"
404
-
echo -e " To: ${GREEN}${to:-unknown}${NC}"
405
-
echo -e " Subject: ${YELLOW}${subject:-<no subject>}${NC}"
406
-
echo -e " Received: ${received:-unknown}"
407
-
echo ""
408
-
((i++))
409
-
done
410
-
echo -e "${CYAN}Total: $((i-1)) email(s)${NC}"
411
-
}
412
-
view_email() {
413
-
local target="$1"
414
-
local file=""
415
-
if [[ "$target" == "latest" ]]; then
416
-
file=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | head -1)
417
-
elif [[ "$target" =~ ^[0-9]+$ ]]; then
418
-
file=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | sed -n "${target}p")
419
-
elif [[ -f "$MAIL_DIR/$target" ]]; then
420
-
file="$MAIL_DIR/$target"
421
-
elif [[ -f "$target" ]]; then
422
-
file="$target"
423
-
fi
424
-
if [[ -z "$file" ]] || [[ ! -f "$file" ]]; then
425
-
echo -e "${RED}Email not found: $target${NC}"
426
-
return 1
427
-
fi
428
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
429
-
echo -e "${GREEN} $(basename "$file")${NC}"
430
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
431
-
cat "$file"
432
-
echo ""
433
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
434
-
}
435
-
clear_queue() {
436
-
local count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
437
-
if [[ "$count" -eq 0 ]]; then
438
-
echo -e "${YELLOW}Queue is already empty.${NC}"
439
-
return
440
-
fi
441
-
rm -f "$MAIL_DIR"/*.eml
442
-
echo -e "${GREEN}Cleared $count email(s) from queue.${NC}"
443
-
}
444
-
watch_queue() {
445
-
echo -e "${CYAN}Watching for new emails... (Ctrl+C to stop)${NC}"
446
-
echo ""
447
-
local last_count=0
448
-
while true; do
449
-
local current_count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
450
-
if [[ "$current_count" -gt "$last_count" ]]; then
451
-
echo -e "${GREEN}[$(date +%H:%M:%S)] New email received!${NC}"
452
-
view_email latest
453
-
last_count=$current_count
454
-
fi
455
-
sleep 1
456
-
done
457
-
}
458
-
count_queue() {
459
-
local count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
460
-
echo "$count"
461
-
}
462
-
case "${1:-}" in
463
-
""|list)
464
-
list_emails
348
+
case "${1:-list}" in
349
+
list)
350
+
ls -lt "$MAIL_DIR"/*.eml 2>/dev/null | head -20 || echo "No emails"
465
351
;;
466
-
latest|[0-9]*)
467
-
view_email "$1"
352
+
latest)
353
+
f=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | head -1)
354
+
[[ -f "$f" ]] && cat "$f" || echo "No emails"
468
355
;;
469
356
clear)
470
-
clear_queue
471
-
;;
472
-
watch)
473
-
watch_queue
357
+
rm -f "$MAIL_DIR"/*.eml
358
+
echo "Cleared"
474
359
;;
475
360
count)
476
-
count_queue
361
+
ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l
477
362
;;
478
-
help|--help|-h)
479
-
show_help
363
+
[0-9]*)
364
+
f=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | sed -n "${1}p")
365
+
[[ -f "$f" ]] && cat "$f" || echo "Not found"
480
366
;;
481
367
*)
482
-
if [[ -f "$MAIL_DIR/$1" ]] || [[ -f "$1" ]]; then
483
-
view_email "$1"
484
-
else
485
-
echo -e "${RED}Unknown command: $1${NC}"
486
-
show_help
487
-
exit 1
488
-
fi
368
+
[[ -f "$MAIL_DIR/$1" ]] && cat "$MAIL_DIR/$1" || echo "Usage: bspds-mailq [list|latest|clear|count|N]"
489
369
;;
490
370
esac
491
371
MAILQ_EOF
492
372
chmod +x /usr/local/bin/bspds-mailq
493
-
log_success "Mail trap configured"
373
+
494
374
log_info "Creating BSPDS configuration..."
495
-
mkdir -p /etc/bspds
496
375
cat > /etc/bspds/bspds.env << EOF
497
376
SERVER_HOST=127.0.0.1
498
377
SERVER_PORT=3000
···
518
397
SENDMAIL_PATH=/usr/local/bin/bspds-sendmail
519
398
EOF
520
399
chmod 600 /etc/bspds/bspds.env
521
-
log_success "Configuration created"
522
-
log_info "Creating BSPDS service user..."
400
+
401
+
log_info "Installing BSPDS..."
523
402
id -u bspds &>/dev/null || useradd -r -s /sbin/nologin bspds
524
403
cp /opt/bspds/target/release/bspds /usr/local/bin/
525
404
mkdir -p /var/lib/bspds
526
405
cp -r /opt/bspds/frontend/dist /var/lib/bspds/frontend
527
406
chown -R bspds:bspds /var/lib/bspds
528
-
log_success "BSPDS binary installed"
529
-
log_info "Creating systemd service..."
407
+
530
408
cat > /etc/systemd/system/bspds.service << 'EOF'
531
409
[Unit]
532
410
Description=BSPDS - AT Protocol PDS
533
411
After=network.target postgresql.service minio.service
412
+
534
413
[Service]
535
414
Type=simple
536
415
User=bspds
···
540
419
ExecStart=/usr/local/bin/bspds
541
420
Restart=always
542
421
RestartSec=5
422
+
543
423
[Install]
544
424
WantedBy=multi-user.target
545
425
EOF
426
+
546
427
systemctl daemon-reload
547
428
systemctl enable bspds
548
429
systemctl start bspds
549
-
log_success "BSPDS service created and started"
430
+
log_success "BSPDS service started"
431
+
550
432
log_info "Installing nginx..."
551
-
apt install -y nginx certbot python3-certbot-nginx
552
-
log_success "nginx installed"
553
-
log_info "Configuring nginx..."
433
+
apt install -y nginx
554
434
cat > /etc/nginx/sites-available/bspds << EOF
555
435
server {
556
436
listen 80;
557
437
listen [::]:80;
558
438
server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
439
+
440
+
location /.well-known/acme-challenge/ {
441
+
root /var/www/html;
442
+
}
443
+
559
444
location / {
560
445
proxy_pass http://127.0.0.1:3000;
561
446
proxy_http_version 1.1;
···
571
456
}
572
457
}
573
458
EOF
459
+
574
460
ln -sf /etc/nginx/sites-available/bspds /etc/nginx/sites-enabled/
575
461
rm -f /etc/nginx/sites-enabled/default
576
462
nginx -t
577
463
systemctl reload nginx
578
464
log_success "nginx configured"
579
-
log_info "Configuring firewall (ufw)..."
465
+
466
+
log_info "Configuring firewall..."
580
467
apt install -y ufw
581
468
ufw --force reset
582
469
ufw default deny incoming
583
470
ufw default allow outgoing
584
-
ufw allow ssh comment 'SSH'
585
-
ufw allow 80/tcp comment 'HTTP'
586
-
ufw allow 443/tcp comment 'HTTPS'
471
+
ufw allow ssh
472
+
ufw allow 80/tcp
473
+
ufw allow 443/tcp
587
474
ufw --force enable
588
475
log_success "Firewall configured"
589
-
log_info "Obtaining SSL certificate..."
590
-
certbot --nginx -d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" --email "${CERTBOT_EMAIL}" --agree-tos --non-interactive || {
591
-
log_warn "Wildcard cert failed (requires DNS challenge). Trying single domain..."
592
-
certbot --nginx -d "${PDS_DOMAIN}" --email "${CERTBOT_EMAIL}" --agree-tos --non-interactive
476
+
477
+
echo ""
478
+
log_info "Obtaining wildcard SSL certificate..."
479
+
echo ""
480
+
echo "User handles are served as subdomains (e.g., alice.${PDS_DOMAIN}),"
481
+
echo "so you need a wildcard certificate. This requires DNS validation."
482
+
echo ""
483
+
echo "You'll need to add a TXT record to your DNS when prompted."
484
+
echo ""
485
+
read -p "Ready to proceed? (y/N): " CERT_READY
486
+
487
+
if [[ "$CERT_READY" =~ ^[Yy]$ ]]; then
488
+
apt install -y certbot python3-certbot-nginx
489
+
490
+
log_info "Running certbot with DNS challenge..."
491
+
echo ""
492
+
echo "When prompted, add the TXT record to your DNS, wait a minute"
493
+
echo "for propagation, then press Enter to continue."
494
+
echo ""
495
+
496
+
if certbot certonly --manual --preferred-challenges dns \
497
+
-d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" \
498
+
--email "${CERTBOT_EMAIL}" --agree-tos; then
499
+
500
+
cat > /etc/nginx/sites-available/bspds << EOF
501
+
server {
502
+
listen 80;
503
+
listen [::]:80;
504
+
server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
505
+
506
+
location /.well-known/acme-challenge/ {
507
+
root /var/www/html;
508
+
}
509
+
510
+
location / {
511
+
return 301 https://\$host\$request_uri;
512
+
}
593
513
}
594
-
log_success "SSL certificate obtained"
514
+
515
+
server {
516
+
listen 443 ssl http2;
517
+
listen [::]:443 ssl http2;
518
+
server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
519
+
520
+
ssl_certificate /etc/letsencrypt/live/${PDS_DOMAIN}/fullchain.pem;
521
+
ssl_certificate_key /etc/letsencrypt/live/${PDS_DOMAIN}/privkey.pem;
522
+
ssl_protocols TLSv1.2 TLSv1.3;
523
+
ssl_ciphers HIGH:!aNULL:!MD5;
524
+
ssl_prefer_server_ciphers on;
525
+
ssl_session_cache shared:SSL:10m;
526
+
527
+
location / {
528
+
proxy_pass http://127.0.0.1:3000;
529
+
proxy_http_version 1.1;
530
+
proxy_set_header Upgrade \$http_upgrade;
531
+
proxy_set_header Connection "upgrade";
532
+
proxy_set_header Host \$host;
533
+
proxy_set_header X-Real-IP \$remote_addr;
534
+
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
535
+
proxy_set_header X-Forwarded-Proto \$scheme;
536
+
proxy_read_timeout 86400;
537
+
proxy_send_timeout 86400;
538
+
client_max_body_size 100M;
539
+
}
540
+
}
541
+
EOF
542
+
nginx -t && systemctl reload nginx
543
+
log_success "Wildcard SSL certificate installed"
544
+
545
+
echo ""
546
+
log_warn "Certificate renewal note:"
547
+
echo "Manual DNS challenges don't auto-renew. Before expiry, run:"
548
+
echo " certbot renew --manual"
549
+
echo ""
550
+
echo "For auto-renewal, consider using a DNS provider plugin:"
551
+
echo " apt install python3-certbot-dns-cloudflare # or your provider"
552
+
echo ""
553
+
else
554
+
log_warn "Wildcard cert failed. You can retry later with:"
555
+
echo " certbot certonly --manual --preferred-challenges dns \\"
556
+
echo " -d ${PDS_DOMAIN} -d '*.${PDS_DOMAIN}'"
557
+
fi
558
+
else
559
+
log_warn "Skipping SSL. Your PDS is running on HTTP only."
560
+
echo "To add SSL later, run:"
561
+
echo " certbot certonly --manual --preferred-challenges dns \\"
562
+
echo " -d ${PDS_DOMAIN} -d '*.${PDS_DOMAIN}'"
563
+
fi
564
+
595
565
log_info "Verifying installation..."
596
566
sleep 3
597
567
if curl -s "http://localhost:3000/xrpc/_health" | grep -q "version"; then
598
-
log_success "BSPDS is responding!"
568
+
log_success "BSPDS is responding"
599
569
else
600
-
log_warn "BSPDS may still be starting up. Check: journalctl -u bspds -f"
570
+
log_warn "BSPDS may still be starting. Check: journalctl -u bspds -f"
601
571
fi
572
+
602
573
echo ""
603
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
604
-
echo -e "${GREEN} INSTALLATION COMPLETE!${NC}"
605
-
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
574
+
log_success "Installation complete"
606
575
echo ""
607
-
echo -e "Your PDS is now running at: ${GREEN}https://${PDS_DOMAIN}${NC}"
576
+
echo "PDS: https://${PDS_DOMAIN}"
608
577
echo ""
609
-
echo -e "${YELLOW}IMPORTANT: Save these credentials securely!${NC}"
578
+
echo "Credentials (also in /etc/bspds/.credentials):"
579
+
echo " DB password: ${DB_PASSWORD}"
580
+
echo " MinIO password: ${MINIO_PASSWORD}"
610
581
echo ""
611
-
echo "Database password: ${DB_PASSWORD}"
612
-
echo "MinIO password: ${MINIO_PASSWORD}"
582
+
echo "Commands:"
583
+
echo " journalctl -u bspds -f # logs"
584
+
echo " systemctl restart bspds # restart"
585
+
echo " bspds-mailq # view trapped emails"
613
586
echo ""
614
-
echo "Configuration file: /etc/bspds/bspds.env"
615
-
echo ""
616
-
echo -e "${CYAN}Useful commands:${NC}"
617
-
echo " journalctl -u bspds -f # View BSPDS logs"
618
-
echo " systemctl status bspds # Check BSPDS status"
619
-
echo " systemctl restart bspds # Restart BSPDS"
620
-
echo " curl https://${PDS_DOMAIN}/xrpc/_health # Health check"
621
-
echo ""
622
-
echo -e "${CYAN}Mail queue (for testing):${NC}"
623
-
echo " bspds-mailq # List all captured emails"
624
-
echo " bspds-mailq latest # View most recent email"
625
-
echo " bspds-mailq 1 # View email #1 from list"
626
-
echo " bspds-mailq watch # Watch for new emails live"
627
-
echo " bspds-mailq clear # Clear all captured emails"
628
-
echo ""
629
-
echo " Emails are saved to: /var/spool/bspds-mail/"
630
-
echo ""
631
-
echo -e "${CYAN}DNS Records Summary:${NC}"
632
-
echo ""
633
-
echo " ${PDS_DOMAIN} A ${IPV4}"
634
-
if [[ "$IPV6" != "Not available" ]]; then
635
-
echo " ${PDS_DOMAIN} AAAA ${IPV6}"
636
-
fi
637
-
echo " *.${PDS_DOMAIN} A ${IPV4}"
638
-
if [[ "$IPV6" != "Not available" ]]; then
639
-
echo " *.${PDS_DOMAIN} AAAA ${IPV6}"
640
-
fi
641
-
echo ""
642
-
echo -e "${GREEN}Enjoy your new AT Protocol PDS!${NC}"