mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-02-08 13:20:41 +00:00
Clean up installation instructions
This commit is contained in:
@@ -1,17 +1,12 @@
|
||||
# BSPDS Production Installation on Alpine Linux
|
||||
> **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.
|
||||
|
||||
This guide covers installing BSPDS on Alpine Linux 3.23 (current stable as of December 2025).
|
||||
## Choose Your Installation Method
|
||||
| Method | Best For |
|
||||
|--------|----------|
|
||||
| **Native (this guide)** | Maximum performance, minimal footprint, full control |
|
||||
| **[Containerized](install-containers.md)** | Easier updates, isolation, reproducible deployments |
|
||||
| **[Kubernetes](install-kubernetes.md)** | Multi-node, high availability, auto-scaling |
|
||||
This guide covers native installation. For containerized deployment with podman and systemd quadlets, see the [container guide](install-containers.md).
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
- A VPS with at least 2GB RAM and 20GB disk
|
||||
- A domain name pointing to your server's IP
|
||||
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
|
||||
- Root access
|
||||
## 1. System Setup
|
||||
```sh
|
||||
@@ -178,13 +173,27 @@ EOF
|
||||
rc-update add nginx
|
||||
rc-service nginx start
|
||||
```
|
||||
## 12. Obtain SSL Certificate
|
||||
## 12. Obtain Wildcard SSL Certificate
|
||||
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate.
|
||||
|
||||
Wildcard certs require DNS-01 validation. For manual DNS validation (works with any provider):
|
||||
```sh
|
||||
certbot --nginx -d pds.example.com
|
||||
certbot certonly --manual --preferred-challenges dns \
|
||||
-d pds.example.com -d '*.pds.example.com'
|
||||
```
|
||||
Set up auto-renewal:
|
||||
Follow the prompts to add TXT records to your DNS.
|
||||
|
||||
If your DNS provider has a certbot plugin, you can use that for auto-renewal:
|
||||
```sh
|
||||
echo "0 0 * * * certbot renew --quiet" | crontab -
|
||||
apk add certbot-dns-cloudflare
|
||||
certbot certonly --dns-cloudflare \
|
||||
--dns-cloudflare-credentials /etc/cloudflare.ini \
|
||||
-d pds.example.com -d '*.pds.example.com'
|
||||
```
|
||||
|
||||
After obtaining the cert, update nginx to use it, then set up auto-renewal:
|
||||
```sh
|
||||
echo "0 0 * * * certbot renew --quiet && rc-service nginx reload" | crontab -
|
||||
```
|
||||
## 13. Configure Firewall
|
||||
```sh
|
||||
|
||||
@@ -6,19 +6,25 @@ This guide covers deploying BSPDS using containers with podman.
|
||||
## Prerequisites
|
||||
- A VPS with at least 2GB RAM and 20GB disk
|
||||
- A domain name pointing to your server's IP
|
||||
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
|
||||
- Root or sudo access
|
||||
## Quick Start (Docker/Podman Compose)
|
||||
If you just want to get running quickly:
|
||||
```sh
|
||||
cp .env.example .env
|
||||
# Edit .env with your values
|
||||
# Generate secrets: openssl rand -base64 48
|
||||
# Build and start
|
||||
```
|
||||
|
||||
Edit `.env` with your values. Generate secrets with `openssl rand -base64 48`.
|
||||
|
||||
Build and start:
|
||||
```sh
|
||||
podman-compose -f docker-compose.prod.yml up -d
|
||||
# Get initial certificate (after DNS is configured)
|
||||
```
|
||||
|
||||
Get initial certificate (after DNS is configured):
|
||||
```sh
|
||||
podman-compose -f docker-compose.prod.yml run --rm certbot certonly \
|
||||
--webroot -w /var/www/acme -d pds.example.com
|
||||
# Restart nginx to load certificate
|
||||
podman-compose -f docker-compose.prod.yml restart nginx
|
||||
```
|
||||
For production setups with proper service management, continue to either the Debian or Alpine section below.
|
||||
@@ -74,31 +80,49 @@ echo "$MINIO_ROOT_PASSWORD" | podman secret create bspds-minio-password -
|
||||
systemctl daemon-reload
|
||||
systemctl start bspds-db bspds-minio bspds-valkey
|
||||
sleep 10
|
||||
# Create MinIO bucket
|
||||
```
|
||||
|
||||
Create the minio bucket:
|
||||
```bash
|
||||
podman run --rm --pod bspds \
|
||||
-e MINIO_ROOT_USER=minioadmin \
|
||||
-e MINIO_ROOT_PASSWORD=your-minio-password \
|
||||
docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \
|
||||
sh -c "mc alias set local http://localhost:9000 \$MINIO_ROOT_USER \$MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/pds-blobs"
|
||||
# Run migrations
|
||||
```
|
||||
|
||||
Run migrations:
|
||||
```bash
|
||||
cargo install sqlx-cli --no-default-features --features postgres
|
||||
DATABASE_URL="postgres://bspds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/bspds/migrations
|
||||
```
|
||||
## 9. Obtain SSL Certificate
|
||||
Create temporary self-signed cert:
|
||||
## 9. Obtain Wildcard SSL Certificate
|
||||
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
|
||||
|
||||
Create temporary self-signed cert to start services:
|
||||
```bash
|
||||
openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
|
||||
-keyout /srv/bspds/certs/privkey.pem \
|
||||
-out /srv/bspds/certs/fullchain.pem \
|
||||
-subj "/CN=pds.example.com"
|
||||
systemctl start bspds-app bspds-nginx
|
||||
# Get real certificate
|
||||
podman run --rm \
|
||||
```
|
||||
|
||||
Get a wildcard certificate using DNS validation:
|
||||
```bash
|
||||
podman run --rm -it \
|
||||
-v /srv/bspds/certs:/etc/letsencrypt:Z \
|
||||
-v /srv/bspds/acme:/var/www/acme:Z \
|
||||
docker.io/certbot/certbot:v5.2.2 certonly \
|
||||
--webroot -w /var/www/acme -d pds.example.com --agree-tos --email you@example.com
|
||||
# Link certificates
|
||||
--manual --preferred-challenges dns \
|
||||
-d pds.example.com -d '*.pds.example.com' \
|
||||
--agree-tos --email you@example.com
|
||||
```
|
||||
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
|
||||
|
||||
For automated renewal, use a DNS provider plugin (e.g., cloudflare, route53).
|
||||
|
||||
Link certificates and restart:
|
||||
```bash
|
||||
ln -sf /srv/bspds/certs/live/pds.example.com/fullchain.pem /srv/bspds/certs/fullchain.pem
|
||||
ln -sf /srv/bspds/certs/live/pds.example.com/privkey.pem /srv/bspds/certs/privkey.pem
|
||||
systemctl restart bspds-nginx
|
||||
@@ -200,42 +224,56 @@ EOF
|
||||
chmod +x /etc/init.d/bspds
|
||||
```
|
||||
## 7. Initialize Services
|
||||
Start services:
|
||||
```sh
|
||||
# Start services
|
||||
rc-service bspds start
|
||||
sleep 15
|
||||
# Create MinIO bucket
|
||||
```
|
||||
|
||||
Create the minio bucket:
|
||||
```sh
|
||||
source /srv/bspds/config/bspds.env
|
||||
podman run --rm --network bspds_default \
|
||||
-e MINIO_ROOT_USER="$MINIO_ROOT_USER" \
|
||||
-e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \
|
||||
docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \
|
||||
sh -c 'mc alias set local http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/pds-blobs'
|
||||
# Run migrations
|
||||
```
|
||||
|
||||
Run migrations:
|
||||
```sh
|
||||
apk add rustup
|
||||
rustup-init -y
|
||||
source ~/.cargo/env
|
||||
cargo install sqlx-cli --no-default-features --features postgres
|
||||
# Get database container IP
|
||||
DB_IP=$(podman inspect bspds-db-1 --format '{{.NetworkSettings.Networks.bspds_default.IPAddress}}')
|
||||
DATABASE_URL="postgres://bspds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/bspds/migrations
|
||||
```
|
||||
## 8. Obtain SSL Certificate
|
||||
Create temporary self-signed cert:
|
||||
## 8. Obtain Wildcard SSL Certificate
|
||||
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
|
||||
|
||||
Create temporary self-signed cert to start services:
|
||||
```sh
|
||||
openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
|
||||
-keyout /srv/bspds/data/certs/privkey.pem \
|
||||
-out /srv/bspds/data/certs/fullchain.pem \
|
||||
-subj "/CN=pds.example.com"
|
||||
rc-service bspds restart
|
||||
# Get real certificate
|
||||
podman run --rm \
|
||||
```
|
||||
|
||||
Get a wildcard certificate using DNS validation:
|
||||
```sh
|
||||
podman run --rm -it \
|
||||
-v /srv/bspds/data/certs:/etc/letsencrypt \
|
||||
-v /srv/bspds/data/acme:/var/www/acme \
|
||||
--network bspds_default \
|
||||
docker.io/certbot/certbot:v5.2.2 certonly \
|
||||
--webroot -w /var/www/acme -d pds.example.com --agree-tos --email you@example.com
|
||||
# Link certificates
|
||||
--manual --preferred-challenges dns \
|
||||
-d pds.example.com -d '*.pds.example.com' \
|
||||
--agree-tos --email you@example.com
|
||||
```
|
||||
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
|
||||
|
||||
Link certificates and restart:
|
||||
```sh
|
||||
ln -sf /srv/bspds/data/certs/live/pds.example.com/fullchain.pem /srv/bspds/data/certs/fullchain.pem
|
||||
ln -sf /srv/bspds/data/certs/live/pds.example.com/privkey.pem /srv/bspds/data/certs/privkey.pem
|
||||
rc-service bspds restart
|
||||
@@ -292,9 +330,15 @@ podman logs -f bspds-bspds-1
|
||||
cd /opt/bspds
|
||||
git pull
|
||||
podman build -t bspds:latest .
|
||||
# Debian:
|
||||
```
|
||||
|
||||
Debian:
|
||||
```bash
|
||||
systemctl restart bspds-app
|
||||
# Alpine:
|
||||
```
|
||||
|
||||
Alpine:
|
||||
```sh
|
||||
rc-service bspds restart
|
||||
```
|
||||
## Backup Database
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
# BSPDS Production Installation on Debian
|
||||
> **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.
|
||||
|
||||
This guide covers installing BSPDS on Debian 13 "Trixie" (current stable as of December 2025).
|
||||
## Choose Your Installation Method
|
||||
| Method | Best For |
|
||||
|--------|----------|
|
||||
| **Native (this guide)** | Maximum performance, full control, simpler debugging |
|
||||
| **[Containerized](install-containers.md)** | Easier updates, isolation, reproducible deployments |
|
||||
| **[Kubernetes](install-kubernetes.md)** | Multi-node, high availability, auto-scaling |
|
||||
This guide covers native installation. For containerized deployment with podman and systemd quadlets, see the [container guide](install-containers.md).
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
- A VPS with at least 2GB RAM and 20GB disk
|
||||
- A domain name pointing to your server's IP
|
||||
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
|
||||
- Root or sudo access
|
||||
## 1. System Setup
|
||||
```bash
|
||||
@@ -168,11 +163,25 @@ rm -f /etc/nginx/sites-enabled/default
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
```
|
||||
## 12. Obtain SSL Certificate
|
||||
## 12. Obtain Wildcard SSL Certificate
|
||||
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate.
|
||||
|
||||
Wildcard certs require DNS-01 validation. If your DNS provider has a certbot plugin:
|
||||
```bash
|
||||
certbot --nginx -d pds.example.com
|
||||
apt install -y python3-certbot-dns-cloudflare
|
||||
certbot certonly --dns-cloudflare \
|
||||
--dns-cloudflare-credentials /etc/cloudflare.ini \
|
||||
-d pds.example.com -d '*.pds.example.com'
|
||||
```
|
||||
Certbot automatically configures nginx for HTTP/2 and sets up auto-renewal.
|
||||
|
||||
For manual DNS validation (works with any provider):
|
||||
```bash
|
||||
certbot certonly --manual --preferred-challenges dns \
|
||||
-d pds.example.com -d '*.pds.example.com'
|
||||
```
|
||||
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
|
||||
|
||||
After obtaining the cert, update nginx to use it and reload.
|
||||
## 13. Configure Firewall
|
||||
```bash
|
||||
apt install -y ufw
|
||||
|
||||
@@ -1,859 +1,23 @@
|
||||
# BSPDS Production Kubernetes Deployment
|
||||
> **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.
|
||||
This guide covers deploying BSPDS on a production multi-node Kubernetes cluster with high availability, auto-scaling, and proper secrets management.
|
||||
## Architecture Overview
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Kubernetes Cluster │
|
||||
│ │
|
||||
Internet ──────►│ Ingress Controller (nginx/traefik) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ Service │◄── HPA (2-10 replicas) │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ┌────┴────┐ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────┐ ┌─────┐ │
|
||||
│ │BSPDS│ │BSPDS│ ... (pods) │
|
||||
│ └──┬──┘ └──┬──┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ PostgreSQL │ MinIO │ Valkey │ │
|
||||
│ │ (HA/Operator)│ (StatefulSet) │ (Sentinel) │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
## Prerequisites
|
||||
- Kubernetes cluster (1.30+) with at least 3 nodes (1.34 is current stable)
|
||||
- `kubectl` configured to access your cluster
|
||||
- `helm` 3.x installed
|
||||
- Storage class that supports `ReadWriteOnce` (for databases)
|
||||
- Ingress controller installed (nginx-ingress or traefik)
|
||||
- cert-manager installed for TLS certificates
|
||||
### Quick Prerequisites Setup
|
||||
If you need to install prerequisites:
|
||||
```bash
|
||||
# Install nginx-ingress (chart v4.14.1 - December 2025)
|
||||
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
|
||||
helm repo update
|
||||
helm install ingress-nginx ingress-nginx/ingress-nginx \
|
||||
--namespace ingress-nginx --create-namespace \
|
||||
--version 4.14.1
|
||||
# Install cert-manager (v1.19.2 - December 2025)
|
||||
helm repo add jetstack https://charts.jetstack.io
|
||||
helm repo update
|
||||
helm install cert-manager jetstack/cert-manager \
|
||||
--namespace cert-manager --create-namespace \
|
||||
--version v1.19.2 \
|
||||
--set installCRDs=true
|
||||
```
|
||||
---
|
||||
## 1. Create Namespace
|
||||
```bash
|
||||
kubectl create namespace bspds
|
||||
kubectl config set-context --current --namespace=bspds
|
||||
```
|
||||
## 2. Create Secrets
|
||||
Generate secure passwords and secrets:
|
||||
```bash
|
||||
# Generate secrets
|
||||
DB_PASSWORD=$(openssl rand -base64 32)
|
||||
MINIO_PASSWORD=$(openssl rand -base64 32)
|
||||
JWT_SECRET=$(openssl rand -base64 48)
|
||||
DPOP_SECRET=$(openssl rand -base64 48)
|
||||
MASTER_KEY=$(openssl rand -base64 48)
|
||||
# Create Kubernetes secrets
|
||||
kubectl create secret generic bspds-db-credentials \
|
||||
--from-literal=username=bspds \
|
||||
--from-literal=password="$DB_PASSWORD"
|
||||
kubectl create secret generic bspds-minio-credentials \
|
||||
--from-literal=root-user=minioadmin \
|
||||
--from-literal=root-password="$MINIO_PASSWORD"
|
||||
kubectl create secret generic bspds-secrets \
|
||||
--from-literal=jwt-secret="$JWT_SECRET" \
|
||||
--from-literal=dpop-secret="$DPOP_SECRET" \
|
||||
--from-literal=master-key="$MASTER_KEY"
|
||||
# Save secrets locally (KEEP SECURE!)
|
||||
echo "DB_PASSWORD=$DB_PASSWORD" > secrets.txt
|
||||
echo "MINIO_PASSWORD=$MINIO_PASSWORD" >> secrets.txt
|
||||
echo "JWT_SECRET=$JWT_SECRET" >> secrets.txt
|
||||
echo "DPOP_SECRET=$DPOP_SECRET" >> secrets.txt
|
||||
echo "MASTER_KEY=$MASTER_KEY" >> secrets.txt
|
||||
chmod 600 secrets.txt
|
||||
```
|
||||
## 3. Deploy PostgreSQL
|
||||
### Option A: CloudNativePG Operator (Recommended for HA)
|
||||
```bash
|
||||
# Install CloudNativePG operator (v1.28.0 - December 2025)
|
||||
kubectl apply --server-side -f \
|
||||
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.28/releases/cnpg-1.28.0.yaml
|
||||
# Wait for operator
|
||||
kubectl wait --for=condition=available --timeout=120s \
|
||||
deployment/cnpg-controller-manager -n cnpg-system
|
||||
```
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: bspds-db
|
||||
namespace: bspds
|
||||
spec:
|
||||
instances: 3
|
||||
postgresql:
|
||||
parameters:
|
||||
max_connections: "200"
|
||||
shared_buffers: "256MB"
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: pds
|
||||
owner: bspds
|
||||
secret:
|
||||
name: bspds-db-credentials
|
||||
storage:
|
||||
size: 20Gi
|
||||
storageClass: standard # adjust for your cluster
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
affinity:
|
||||
podAntiAffinityType: required
|
||||
EOF
|
||||
```
|
||||
### Option B: Simple StatefulSet (Single Instance)
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: bspds-db-pvc
|
||||
namespace: bspds
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: bspds-db
|
||||
namespace: bspds
|
||||
spec:
|
||||
serviceName: bspds-db
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bspds-db
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bspds-db
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:18-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
value: pds
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-db-credentials
|
||||
key: username
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-db-credentials
|
||||
key: password
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "bspds", "-d", "pds"]
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "bspds", "-d", "pds"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: bspds-db-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bspds-db-rw
|
||||
namespace: bspds
|
||||
spec:
|
||||
selector:
|
||||
app: bspds-db
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
EOF
|
||||
```
|
||||
## 4. Deploy MinIO
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: bspds-minio-pvc
|
||||
namespace: bspds
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 50Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: bspds-minio
|
||||
namespace: bspds
|
||||
spec:
|
||||
serviceName: bspds-minio
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bspds-minio
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bspds-minio
|
||||
spec:
|
||||
containers:
|
||||
- name: minio
|
||||
image: minio/minio:RELEASE.2025-10-15T17-29-55Z
|
||||
args:
|
||||
- server
|
||||
- /data
|
||||
- --console-address
|
||||
- ":9001"
|
||||
ports:
|
||||
- containerPort: 9000
|
||||
name: api
|
||||
- containerPort: 9001
|
||||
name: console
|
||||
env:
|
||||
- name: MINIO_ROOT_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-minio-credentials
|
||||
key: root-user
|
||||
- name: MINIO_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-minio-credentials
|
||||
key: root-password
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /minio/health/live
|
||||
port: 9000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /minio/health/ready
|
||||
port: 9000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: bspds-minio-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bspds-minio
|
||||
namespace: bspds
|
||||
spec:
|
||||
selector:
|
||||
app: bspds-minio
|
||||
ports:
|
||||
- port: 9000
|
||||
targetPort: 9000
|
||||
name: api
|
||||
- port: 9001
|
||||
targetPort: 9001
|
||||
name: console
|
||||
EOF
|
||||
```
|
||||
### Initialize MinIO Bucket
|
||||
```bash
|
||||
kubectl run minio-init --rm -it --restart=Never \
|
||||
--image=minio/mc:RELEASE.2025-07-16T15-35-03Z \
|
||||
--env="MINIO_ROOT_USER=minioadmin" \
|
||||
--env="MINIO_ROOT_PASSWORD=$(kubectl get secret bspds-minio-credentials -o jsonpath='{.data.root-password}' | base64 -d)" \
|
||||
--command -- sh -c "
|
||||
mc alias set local http://bspds-minio:9000 \$MINIO_ROOT_USER \$MINIO_ROOT_PASSWORD &&
|
||||
mc mb --ignore-existing local/pds-blobs
|
||||
"
|
||||
```
|
||||
## 5. Deploy Valkey
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: bspds-valkey-pvc
|
||||
namespace: bspds
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: bspds-valkey
|
||||
namespace: bspds
|
||||
spec:
|
||||
serviceName: bspds-valkey
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bspds-valkey
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bspds-valkey
|
||||
spec:
|
||||
containers:
|
||||
- name: valkey
|
||||
image: valkey/valkey:9-alpine
|
||||
args:
|
||||
- valkey-server
|
||||
- --appendonly
|
||||
- "yes"
|
||||
- --maxmemory
|
||||
- "256mb"
|
||||
- --maxmemory-policy
|
||||
- allkeys-lru
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "300Mi"
|
||||
cpu: "200m"
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["valkey-cli", "ping"]
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["valkey-cli", "ping"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 3
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: bspds-valkey-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bspds-valkey
|
||||
namespace: bspds
|
||||
spec:
|
||||
selector:
|
||||
app: bspds-valkey
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
EOF
|
||||
```
|
||||
## 6. Build and Push BSPDS Image
|
||||
```bash
|
||||
# Build image
|
||||
cd /path/to/bspds
|
||||
docker build -t your-registry.com/bspds:latest .
|
||||
docker push your-registry.com/bspds:latest
|
||||
```
|
||||
If using a private registry, create an image pull secret:
|
||||
```bash
|
||||
kubectl create secret docker-registry regcred \
|
||||
--docker-server=your-registry.com \
|
||||
--docker-username=your-username \
|
||||
--docker-password=your-password \
|
||||
--docker-email=your-email
|
||||
```
|
||||
## 7. Run Database Migrations
|
||||
BSPDS runs migrations automatically on startup. However, if you want to run migrations separately (recommended for zero-downtime deployments), you can use a Job:
|
||||
```bash
|
||||
cat <<'EOF' | kubectl apply -f -
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: bspds-migrate
|
||||
namespace: bspds
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 300
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: migrate
|
||||
image: your-registry.com/bspds:latest
|
||||
command: ["/usr/local/bin/bspds"]
|
||||
args: ["--migrate-only"] # Add this flag to your app, or remove this Job
|
||||
env:
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-db-credentials
|
||||
key: password
|
||||
- name: DATABASE_URL
|
||||
value: "postgres://bspds:$(DB_PASSWORD)@bspds-db-rw:5432/pds"
|
||||
EOF
|
||||
kubectl wait --for=condition=complete --timeout=120s job/bspds-migrate
|
||||
```
|
||||
> **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.
|
||||
## 8. Deploy BSPDS Application
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: bspds-config
|
||||
namespace: bspds
|
||||
data:
|
||||
PDS_HOSTNAME: "pds.example.com"
|
||||
SERVER_HOST: "0.0.0.0"
|
||||
SERVER_PORT: "3000"
|
||||
S3_ENDPOINT: "http://bspds-minio:9000"
|
||||
AWS_REGION: "us-east-1"
|
||||
S3_BUCKET: "pds-blobs"
|
||||
VALKEY_URL: "redis://bspds-valkey:6379"
|
||||
APPVIEW_URL: "https://api.bsky.app"
|
||||
CRAWLERS: "https://bsky.network"
|
||||
FRONTEND_DIR: "/app/frontend/dist"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bspds
|
||||
namespace: bspds
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bspds
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bspds
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: regcred # Remove if using public registry
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: bspds
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- name: bspds
|
||||
image: your-registry.com/bspds:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: http
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: bspds-config
|
||||
env:
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-db-credentials
|
||||
key: password
|
||||
- name: DATABASE_URL
|
||||
value: "postgres://bspds:$(DB_PASSWORD)@bspds-db-rw:5432/pds"
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-minio-credentials
|
||||
key: root-user
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-minio-credentials
|
||||
key: root-password
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-secrets
|
||||
key: jwt-secret
|
||||
- name: DPOP_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-secrets
|
||||
key: dpop-secret
|
||||
- name: MASTER_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bspds-secrets
|
||||
key: master-key
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 3
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bspds
|
||||
namespace: bspds
|
||||
spec:
|
||||
selector:
|
||||
app: bspds
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
name: http
|
||||
EOF
|
||||
```
|
||||
## 9. Configure Horizontal Pod Autoscaler
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: bspds
|
||||
namespace: bspds
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: bspds
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Pods
|
||||
value: 1
|
||||
periodSeconds: 60
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 0
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 15
|
||||
- type: Pods
|
||||
value: 4
|
||||
periodSeconds: 15
|
||||
selectPolicy: Max
|
||||
EOF
|
||||
```
|
||||
## 10. Configure Pod Disruption Budget
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: bspds
|
||||
namespace: bspds
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bspds
|
||||
EOF
|
||||
```
|
||||
## 11. Configure TLS with cert-manager
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: your-email@example.com
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: nginx
|
||||
EOF
|
||||
```
|
||||
## 12. Configure Ingress
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: bspds
|
||||
namespace: bspds
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "86400"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "86400"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
|
||||
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||
nginx.ingress.kubernetes.io/websocket-services: "bspds"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- pds.example.com
|
||||
secretName: bspds-tls
|
||||
rules:
|
||||
- host: pds.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: bspds
|
||||
port:
|
||||
number: 80
|
||||
EOF
|
||||
```
|
||||
## 13. Configure Network Policies (Optional but Recommended)
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: bspds-network-policy
|
||||
namespace: bspds
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: bspds
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: ingress-nginx
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 3000
|
||||
egress:
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: bspds-db
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: bspds-minio
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: bspds-valkey
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
- to: # Allow DNS
|
||||
- namespaceSelector: {}
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- to: # Allow external HTTPS (for federation)
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
EOF
|
||||
```
|
||||
## 14. Deploy Prometheus Monitoring (Optional)
|
||||
```bash
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: bspds
|
||||
namespace: bspds
|
||||
labels:
|
||||
release: prometheus
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bspds
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
EOF
|
||||
```
|
||||
---
|
||||
## Verification
|
||||
```bash
|
||||
# Check all pods are running
|
||||
kubectl get pods -n bspds
|
||||
# Check services
|
||||
kubectl get svc -n bspds
|
||||
# Check ingress
|
||||
kubectl get ingress -n bspds
|
||||
# Check certificate
|
||||
kubectl get certificate -n bspds
|
||||
# Test health endpoint
|
||||
curl -s https://pds.example.com/xrpc/_health | jq
|
||||
# Test DID endpoint
|
||||
curl -s https://pds.example.com/.well-known/atproto-did
|
||||
```
|
||||
---
|
||||
## Maintenance
|
||||
### View Logs
|
||||
```bash
|
||||
# All BSPDS pods
|
||||
kubectl logs -l app=bspds -n bspds -f
|
||||
# Specific pod
|
||||
kubectl logs -f deployment/bspds -n bspds
|
||||
```
|
||||
### Scale Manually
|
||||
```bash
|
||||
kubectl scale deployment bspds --replicas=5 -n bspds
|
||||
```
|
||||
### Update BSPDS
|
||||
```bash
|
||||
# Build and push new image
|
||||
docker build -t your-registry.com/bspds:v1.2.3 .
|
||||
docker push your-registry.com/bspds:v1.2.3
|
||||
# Update deployment
|
||||
kubectl set image deployment/bspds bspds=your-registry.com/bspds:v1.2.3 -n bspds
|
||||
# Watch rollout
|
||||
kubectl rollout status deployment/bspds -n bspds
|
||||
```
|
||||
### Backup Database
|
||||
```bash
|
||||
# For CloudNativePG
|
||||
kubectl cnpg backup bspds-db -n bspds
|
||||
# For StatefulSet
|
||||
kubectl exec -it bspds-db-0 -n bspds -- pg_dump -U bspds pds > backup-$(date +%Y%m%d).sql
|
||||
```
|
||||
### Run Migrations
|
||||
If you have a migration Job defined, you can re-run it:
|
||||
```bash
|
||||
# Delete old job first (if exists)
|
||||
kubectl delete job bspds-migrate -n bspds --ignore-not-found
|
||||
# Re-apply the migration job from step 7
|
||||
# Or simply restart the deployment - BSPDS runs migrations on startup
|
||||
kubectl rollout restart deployment/bspds -n bspds
|
||||
```
|
||||
---
|
||||
## Troubleshooting
|
||||
### Pod Won't Start
|
||||
```bash
|
||||
kubectl describe pod -l app=bspds -n bspds
|
||||
kubectl logs -l app=bspds -n bspds --previous
|
||||
```
|
||||
### Database Connection Issues
|
||||
```bash
|
||||
# Test connectivity from a debug pod
|
||||
kubectl run debug --rm -it --restart=Never --image=postgres:18-alpine -- \
|
||||
psql "postgres://bspds:PASSWORD@bspds-db-rw:5432/pds" -c "SELECT 1"
|
||||
```
|
||||
### Certificate Issues
|
||||
```bash
|
||||
kubectl describe certificate bspds-tls -n bspds
|
||||
kubectl describe certificaterequest -n bspds
|
||||
kubectl logs -l app.kubernetes.io/name=cert-manager -n cert-manager
|
||||
```
|
||||
### View Resource Usage
|
||||
```bash
|
||||
kubectl top pods -n bspds
|
||||
kubectl top nodes
|
||||
```
|
||||
# BSPDS on Kubernetes
|
||||
|
||||
If you're reaching for kubernetes for this app, you're experienced enough to know how to spin up:
|
||||
|
||||
- cloudnativepg (or your preferred postgres operator)
|
||||
- valkey
|
||||
- s3-compatible object storage (minio operator, or just use a managed service)
|
||||
- the app itself (it's just a container with some env vars)
|
||||
|
||||
You'll need a wildcard TLS certificate for `*.your-pds-hostname.example.com` — user handles are served as subdomains.
|
||||
|
||||
The container image expects:
|
||||
- `DATABASE_URL` - postgres connection string
|
||||
- `S3_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `S3_BUCKET`
|
||||
- `VALKEY_URL` - redis:// connection string
|
||||
- `PDS_HOSTNAME` - your PDS hostname (without protocol)
|
||||
- `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48`
|
||||
- `APPVIEW_URL` - typically `https://api.bsky.app`
|
||||
- `CRAWLERS` - typically `https://bsky.network`
|
||||
and more, check the .env.example.
|
||||
|
||||
Health check: `GET /xrpc/_health`
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ This guide covers installing BSPDS on OpenBSD 7.8 (current release as of Decembe
|
||||
## Prerequisites
|
||||
- A VPS with at least 2GB RAM and 20GB disk
|
||||
- A domain name pointing to your server's IP
|
||||
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
|
||||
- Root access (or doas configured)
|
||||
## Why nginx over relayd?
|
||||
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,7 +81,7 @@ mc alias set local http://localhost:9000 minioadmin your-minio-password
|
||||
mc mb local/pds-blobs
|
||||
```
|
||||
## 5. Install redis
|
||||
OpenBSD has redis in ports (valkey may not be available yet):
|
||||
OpenBSD has redis in ports (valkey not available yet):
|
||||
```sh
|
||||
pkg_add redis
|
||||
rcctl enable redis
|
||||
@@ -194,37 +195,32 @@ EOF
|
||||
mkdir -p /var/www/acme
|
||||
rcctl enable nginx
|
||||
```
|
||||
## 12. Obtain SSL Certificate with acme-client
|
||||
OpenBSD's native acme-client works well:
|
||||
## 12. Obtain Wildcard SSL Certificate
|
||||
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate.
|
||||
|
||||
OpenBSD's native `acme-client` only supports HTTP-01 validation, which can't issue wildcard certs. You have a few options:
|
||||
|
||||
**Option A: Use certbot with DNS validation (recommended)**
|
||||
```sh
|
||||
pkg_add certbot
|
||||
certbot certonly --manual --preferred-challenges dns \
|
||||
-d pds.example.com -d '*.pds.example.com'
|
||||
```
|
||||
Follow the prompts to add TXT records to your DNS. Then update nginx.conf to point to the certbot certs.
|
||||
|
||||
**Option B: Use a managed DNS provider with API**
|
||||
If your DNS provider has a certbot plugin, you can automate renewal.
|
||||
|
||||
**Option C: Use acme.sh**
|
||||
[acme.sh](https://github.com/acmesh-official/acme.sh) supports many DNS providers for automated wildcard cert renewal.
|
||||
|
||||
After obtaining the cert, update nginx to use it and restart:
|
||||
```sh
|
||||
cat >> /etc/acme-client.conf << 'EOF'
|
||||
authority letsencrypt {
|
||||
api url "https://acme-v02.api.letsencrypt.org/directory"
|
||||
account key "/etc/acme/letsencrypt-privkey.pem"
|
||||
}
|
||||
domain pds.example.com {
|
||||
domain key "/etc/ssl/private/pds.example.com.key"
|
||||
domain full chain certificate "/etc/ssl/pds.example.com.fullchain.pem"
|
||||
sign with letsencrypt
|
||||
}
|
||||
EOF
|
||||
mkdir -p /etc/acme
|
||||
rcctl start nginx
|
||||
acme-client -v pds.example.com
|
||||
rcctl restart nginx
|
||||
```
|
||||
Set up auto-renewal in root's crontab:
|
||||
```sh
|
||||
crontab -e
|
||||
```
|
||||
Add:
|
||||
```
|
||||
0 0 * * * acme-client pds.example.com && rcctl reload nginx
|
||||
```
|
||||
## 13. Configure Packet Filter (pf)
|
||||
```sh
|
||||
cat >> /etc/pf.conf << 'EOF'
|
||||
# BSPDS rules
|
||||
pass in on egress proto tcp from any to any port { 22, 80, 443 }
|
||||
EOF
|
||||
pfctl -f /etc/pf.conf
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -qi "debian" /etc/os-release 2>/dev/null; then
|
||||
log_warn "This script is designed for Debian. Proceed with caution on other distros."
|
||||
fi
|
||||
|
||||
nuke_installation() {
|
||||
echo -e "${RED}"
|
||||
echo "╔═══════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ NUKING EXISTING INSTALLATION ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
log_warn "NUKING EXISTING INSTALLATION"
|
||||
log_info "Stopping services..."
|
||||
systemctl stop bspds 2>/dev/null || true
|
||||
systemctl disable bspds 2>/dev/null || true
|
||||
|
||||
log_info "Removing BSPDS files..."
|
||||
rm -rf /opt/bspds
|
||||
rm -rf /var/lib/bspds
|
||||
@@ -35,12 +36,15 @@ nuke_installation() {
|
||||
rm -rf /var/spool/bspds-mail
|
||||
rm -f /etc/systemd/system/bspds.service
|
||||
systemctl daemon-reload
|
||||
|
||||
log_info "Removing BSPDS configuration..."
|
||||
rm -rf /etc/bspds
|
||||
|
||||
log_info "Dropping postgres database and user..."
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS pds;" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "DROP USER IF EXISTS bspds;" 2>/dev/null || true
|
||||
log_info "Removing minio bucket and resetting minio..."
|
||||
|
||||
log_info "Removing minio bucket..."
|
||||
if command -v mc &>/dev/null; then
|
||||
mc rb local/pds-blobs --force 2>/dev/null || true
|
||||
mc alias remove local 2>/dev/null || true
|
||||
@@ -48,19 +52,17 @@ nuke_installation() {
|
||||
systemctl stop minio 2>/dev/null || true
|
||||
rm -rf /var/lib/minio/data/.minio.sys 2>/dev/null || true
|
||||
rm -f /etc/default/minio 2>/dev/null || true
|
||||
|
||||
log_info "Removing nginx config..."
|
||||
rm -f /etc/nginx/sites-enabled/bspds
|
||||
rm -f /etc/nginx/sites-available/bspds
|
||||
systemctl reload nginx 2>/dev/null || true
|
||||
log_success "Previous installation nuked!"
|
||||
echo ""
|
||||
|
||||
log_success "Previous installation nuked"
|
||||
}
|
||||
|
||||
if [[ -f /etc/bspds/bspds.env ]] || [[ -d /opt/bspds ]] || [[ -f /usr/local/bin/bspds ]]; then
|
||||
echo -e "${YELLOW}"
|
||||
echo "╔═══════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ EXISTING INSTALLATION DETECTED ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
log_warn "Existing installation detected"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " 1) Nuke everything and start fresh (destroys database!)"
|
||||
@@ -68,21 +70,21 @@ if [[ -f /etc/bspds/bspds.env ]] || [[ -d /opt/bspds ]] || [[ -f /usr/local/bin/
|
||||
echo " 3) Exit"
|
||||
echo ""
|
||||
read -p "Choose an option [1/2/3]: " INSTALL_CHOICE
|
||||
|
||||
case "$INSTALL_CHOICE" in
|
||||
1)
|
||||
echo ""
|
||||
echo -e "${RED}WARNING: This will DELETE:${NC}"
|
||||
log_warn "This will DELETE:"
|
||||
echo " - PostgreSQL database 'pds' and all data"
|
||||
echo " - All BSPDS configuration and credentials"
|
||||
echo " - All source code in /opt/bspds"
|
||||
echo " - MinIO bucket 'pds-blobs' and all blobs"
|
||||
echo " - Mail queue contents"
|
||||
echo ""
|
||||
read -p "Type 'NUKE' to confirm destruction: " CONFIRM_NUKE
|
||||
read -p "Type 'NUKE' to confirm: " CONFIRM_NUKE
|
||||
if [[ "$CONFIRM_NUKE" == "NUKE" ]]; then
|
||||
nuke_installation
|
||||
else
|
||||
log_error "Nuke cancelled. Exiting."
|
||||
log_error "Nuke cancelled"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
@@ -90,92 +92,68 @@ if [[ -f /etc/bspds/bspds.env ]] || [[ -d /opt/bspds ]] || [[ -f /usr/local/bin/
|
||||
log_info "Continuing with existing installation..."
|
||||
;;
|
||||
3)
|
||||
log_info "Exiting."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Invalid option. Exiting."
|
||||
log_error "Invalid option"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
echo -e "${CYAN}"
|
||||
echo "╔═══════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ BSPDS Installation Script for Debian ║"
|
||||
echo "║ AT Protocol Personal Data Server in Rust ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
|
||||
echo ""
|
||||
log_info "BSPDS Installation Script for Debian"
|
||||
echo ""
|
||||
|
||||
get_public_ips() {
|
||||
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")
|
||||
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")
|
||||
IPV6=$(curl -6 -s --max-time 5 ifconfig.me 2>/dev/null || curl -6 -s --max-time 5 icanhazip.com 2>/dev/null || echo "")
|
||||
}
|
||||
|
||||
log_info "Detecting public IP addresses..."
|
||||
get_public_ips
|
||||
echo " IPv4: ${IPV4}"
|
||||
[[ -n "$IPV6" ]] && echo " IPv6: ${IPV6}"
|
||||
echo ""
|
||||
echo -e "${CYAN}Your server's public IPs:${NC}"
|
||||
echo -e " IPv4: ${GREEN}${IPV4}${NC}"
|
||||
echo -e " IPv6: ${GREEN}${IPV6}${NC}"
|
||||
echo ""
|
||||
|
||||
read -p "Enter your PDS domain (e.g., pds.example.com): " PDS_DOMAIN
|
||||
if [[ -z "$PDS_DOMAIN" ]]; then
|
||||
log_error "Domain cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
read -p "Enter your email for Let's Encrypt notifications: " CERTBOT_EMAIL
|
||||
|
||||
read -p "Enter your email for Let's Encrypt: " CERTBOT_EMAIL
|
||||
if [[ -z "$CERTBOT_EMAIL" ]]; then
|
||||
log_error "Email cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW}DNS RECORDS REQUIRED${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
log_info "DNS records required (create these now if you haven't):"
|
||||
echo ""
|
||||
echo "Before continuing, create these DNS records at your registrar:"
|
||||
echo ""
|
||||
echo -e "${GREEN}A Record:${NC}"
|
||||
echo " Name: ${PDS_DOMAIN}"
|
||||
echo " Type: A"
|
||||
echo " Value: ${IPV4}"
|
||||
echo ""
|
||||
if [[ "$IPV6" != "Not available" ]]; then
|
||||
echo -e "${GREEN}AAAA Record:${NC}"
|
||||
echo " Name: ${PDS_DOMAIN}"
|
||||
echo " Type: AAAA"
|
||||
echo " Value: ${IPV6}"
|
||||
echo ""
|
||||
fi
|
||||
echo -e "${GREEN}Wildcard A Record (for user handles):${NC}"
|
||||
echo " Name: *.${PDS_DOMAIN}"
|
||||
echo " Type: A"
|
||||
echo " Value: ${IPV4}"
|
||||
echo ""
|
||||
if [[ "$IPV6" != "Not available" ]]; then
|
||||
echo -e "${GREEN}Wildcard AAAA Record (for user handles):${NC}"
|
||||
echo " Name: *.${PDS_DOMAIN}"
|
||||
echo " Type: AAAA"
|
||||
echo " Value: ${IPV6}"
|
||||
echo ""
|
||||
fi
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo " ${PDS_DOMAIN} A ${IPV4}"
|
||||
[[ -n "$IPV6" ]] && echo " ${PDS_DOMAIN} AAAA ${IPV6}"
|
||||
echo " *.${PDS_DOMAIN} A ${IPV4} (for user handles)"
|
||||
[[ -n "$IPV6" ]] && echo " *.${PDS_DOMAIN} AAAA ${IPV6} (for user handles)"
|
||||
echo ""
|
||||
read -p "Have you created these DNS records? (y/N): " DNS_CONFIRMED
|
||||
if [[ ! "$DNS_CONFIRMED" =~ ^[Yy]$ ]]; then
|
||||
log_warn "Please create the DNS records and run this script again."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CREDENTIALS_FILE="/etc/bspds/.credentials"
|
||||
if [[ -f "$CREDENTIALS_FILE" ]]; then
|
||||
log_info "Loading existing credentials from previous installation..."
|
||||
log_info "Loading existing credentials..."
|
||||
source "$CREDENTIALS_FILE"
|
||||
log_success "Credentials loaded"
|
||||
else
|
||||
log_info "Generating secure secrets..."
|
||||
log_info "Generating secrets..."
|
||||
JWT_SECRET=$(openssl rand -base64 48)
|
||||
DPOP_SECRET=$(openssl rand -base64 48)
|
||||
MASTER_KEY=$(openssl rand -base64 48)
|
||||
DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
|
||||
MINIO_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
|
||||
|
||||
mkdir -p /etc/bspds
|
||||
cat > "$CREDENTIALS_FILE" << EOF
|
||||
JWT_SECRET="$JWT_SECRET"
|
||||
@@ -185,39 +163,35 @@ DB_PASSWORD="$DB_PASSWORD"
|
||||
MINIO_PASSWORD="$MINIO_PASSWORD"
|
||||
EOF
|
||||
chmod 600 "$CREDENTIALS_FILE"
|
||||
log_success "Secrets generated and saved"
|
||||
log_success "Secrets generated"
|
||||
fi
|
||||
|
||||
log_info "Checking swap space..."
|
||||
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
||||
TOTAL_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
|
||||
|
||||
if [[ $TOTAL_SWAP_KB -lt 2000000 ]]; then
|
||||
log_info "Adding swap space (needed for compilation)..."
|
||||
if [[ ! -f /swapfile ]]; then
|
||||
log_info "Adding swap space for compilation..."
|
||||
SWAP_SIZE="4G"
|
||||
if [[ $TOTAL_MEM_KB -lt 2000000 ]]; then
|
||||
SWAP_SIZE="4G"
|
||||
elif [[ $TOTAL_MEM_KB -lt 4000000 ]]; then
|
||||
SWAP_SIZE="2G"
|
||||
fi
|
||||
[[ $TOTAL_MEM_KB -ge 4000000 ]] && SWAP_SIZE="2G"
|
||||
fallocate -l $SWAP_SIZE /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=4096
|
||||
chmod 600 /swapfile
|
||||
mkswap /swapfile
|
||||
swapon /swapfile
|
||||
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||||
log_success "Swap space added ($SWAP_SIZE)"
|
||||
log_success "Swap added ($SWAP_SIZE)"
|
||||
else
|
||||
swapon /swapfile 2>/dev/null || true
|
||||
log_success "Existing swap enabled"
|
||||
fi
|
||||
else
|
||||
log_success "Sufficient swap already configured"
|
||||
fi
|
||||
|
||||
log_info "Updating system packages..."
|
||||
apt update && apt upgrade -y
|
||||
log_success "System updated"
|
||||
|
||||
log_info "Installing build dependencies..."
|
||||
apt install -y curl git build-essential pkg-config libssl-dev ca-certificates gnupg lsb-release unzip xxd
|
||||
log_success "Build dependencies installed"
|
||||
|
||||
log_info "Installing postgres..."
|
||||
apt install -y postgresql postgresql-contrib
|
||||
systemctl enable postgresql
|
||||
@@ -226,34 +200,34 @@ sudo -u postgres psql -c "CREATE USER bspds WITH PASSWORD '${DB_PASSWORD}';" 2>/
|
||||
sudo -u postgres psql -c "ALTER USER bspds WITH PASSWORD '${DB_PASSWORD}';"
|
||||
sudo -u postgres psql -c "CREATE DATABASE pds OWNER bspds;" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO bspds;"
|
||||
log_success "postgres installed and configured"
|
||||
log_success "postgres configured"
|
||||
|
||||
log_info "Installing valkey..."
|
||||
apt install -y valkey || {
|
||||
log_warn "valkey not in repos, trying redis..."
|
||||
apt install -y valkey 2>/dev/null || {
|
||||
log_warn "valkey not in repos, installing redis..."
|
||||
apt install -y redis-server
|
||||
systemctl enable redis-server
|
||||
systemctl start redis-server
|
||||
}
|
||||
systemctl enable valkey-server 2>/dev/null || true
|
||||
systemctl start valkey-server 2>/dev/null || true
|
||||
log_success "valkey/redis installed"
|
||||
|
||||
log_info "Installing minio..."
|
||||
if [[ ! -f /usr/local/bin/minio ]]; then
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
if [[ "$ARCH" == "amd64" ]]; then
|
||||
curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-amd64/minio
|
||||
elif [[ "$ARCH" == "arm64" ]]; then
|
||||
curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-arm64/minio
|
||||
else
|
||||
log_error "Unsupported architecture: $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
case "$ARCH" in
|
||||
amd64) curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-amd64/minio ;;
|
||||
arm64) curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-arm64/minio ;;
|
||||
*) log_error "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
chmod +x /tmp/minio
|
||||
mv /tmp/minio /usr/local/bin/
|
||||
fi
|
||||
|
||||
mkdir -p /var/lib/minio/data
|
||||
id -u minio-user &>/dev/null || useradd -r -s /sbin/nologin minio-user
|
||||
chown -R minio-user:minio-user /var/lib/minio
|
||||
|
||||
cat > /etc/default/minio << EOF
|
||||
MINIO_ROOT_USER=minioadmin
|
||||
MINIO_ROOT_PASSWORD=${MINIO_PASSWORD}
|
||||
@@ -261,10 +235,12 @@ MINIO_VOLUMES="/var/lib/minio/data"
|
||||
MINIO_OPTS="--console-address :9001"
|
||||
EOF
|
||||
chmod 600 /etc/default/minio
|
||||
|
||||
cat > /etc/systemd/system/minio.service << 'EOF'
|
||||
[Unit]
|
||||
Description=MinIO Object Storage
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=minio-user
|
||||
Group=minio-user
|
||||
@@ -272,30 +248,34 @@ EnvironmentFile=/etc/default/minio
|
||||
ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
|
||||
Restart=always
|
||||
LimitNOFILE=65536
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable minio
|
||||
systemctl start minio
|
||||
log_success "minio installed"
|
||||
log_info "Waiting for minio to start..."
|
||||
|
||||
log_info "Waiting for minio..."
|
||||
sleep 5
|
||||
log_info "Installing minio client and creating bucket..."
|
||||
|
||||
if [[ ! -f /usr/local/bin/mc ]]; then
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
if [[ "$ARCH" == "amd64" ]]; then
|
||||
curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||
elif [[ "$ARCH" == "arm64" ]]; then
|
||||
curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-arm64/mc
|
||||
fi
|
||||
case "$ARCH" in
|
||||
amd64) curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-amd64/mc ;;
|
||||
arm64) curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-arm64/mc ;;
|
||||
esac
|
||||
chmod +x /tmp/mc
|
||||
mv /tmp/mc /usr/local/bin/
|
||||
fi
|
||||
|
||||
mc alias remove local 2>/dev/null || true
|
||||
mc alias set local http://localhost:9000 minioadmin "${MINIO_PASSWORD}" --api S3v4
|
||||
mc mb local/pds-blobs --ignore-existing
|
||||
log_success "minio bucket created"
|
||||
|
||||
log_info "Installing rust..."
|
||||
if [[ -f "$HOME/.cargo/env" ]]; then
|
||||
source "$HOME/.cargo/env"
|
||||
@@ -304,47 +284,46 @@ if ! command -v rustc &>/dev/null; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
source "$HOME/.cargo/env"
|
||||
fi
|
||||
log_success "rust installed"
|
||||
|
||||
log_info "Installing deno..."
|
||||
export PATH="$HOME/.deno/bin:$PATH"
|
||||
if ! command -v deno &>/dev/null && [[ ! -f "$HOME/.deno/bin/deno" ]]; then
|
||||
curl -fsSL https://deno.land/install.sh | sh
|
||||
grep -q 'deno/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
|
||||
fi
|
||||
log_success "deno installed"
|
||||
|
||||
log_info "Cloning BSPDS..."
|
||||
if [[ ! -d /opt/bspds ]]; then
|
||||
git clone https://tangled.org/lewis.moe/bspds-sandbox /opt/bspds
|
||||
else
|
||||
log_warn "/opt/bspds already exists, pulling latest..."
|
||||
cd /opt/bspds && git pull
|
||||
fi
|
||||
cd /opt/bspds
|
||||
log_success "BSPDS cloned"
|
||||
|
||||
log_info "Building frontend..."
|
||||
cd /opt/bspds/frontend
|
||||
"$HOME/.deno/bin/deno" task build
|
||||
cd /opt/bspds
|
||||
"$HOME/.deno/bin/deno" task build --filter=frontend
|
||||
log_success "Frontend built"
|
||||
log_info "Building BSPDS (this may take a while)..."
|
||||
|
||||
log_info "Building BSPDS (this takes a while)..."
|
||||
source "$HOME/.cargo/env"
|
||||
NPROC=$(nproc)
|
||||
if [[ $TOTAL_MEM_KB -lt 4000000 ]]; then
|
||||
log_info "Low memory detected, limiting parallel jobs..."
|
||||
log_info "Low memory - limiting parallel jobs"
|
||||
CARGO_BUILD_JOBS=1 cargo build --release
|
||||
else
|
||||
cargo build --release
|
||||
fi
|
||||
log_success "BSPDS built"
|
||||
log_info "Installing sqlx-cli and running migrations..."
|
||||
|
||||
log_info "Running migrations..."
|
||||
cargo install sqlx-cli --no-default-features --features postgres
|
||||
export DATABASE_URL="postgres://bspds:${DB_PASSWORD}@localhost:5432/pds"
|
||||
"$HOME/.cargo/bin/sqlx" migrate run
|
||||
log_success "Migrations complete"
|
||||
log_info "Setting up mail trap for testing..."
|
||||
|
||||
log_info "Setting up mail trap..."
|
||||
mkdir -p /var/spool/bspds-mail
|
||||
chown root:root /var/spool/bspds-mail
|
||||
chmod 1777 /var/spool/bspds-mail
|
||||
|
||||
cat > /usr/local/bin/bspds-sendmail << 'SENDMAIL_EOF'
|
||||
#!/bin/bash
|
||||
MAIL_DIR="/var/spool/bspds-mail"
|
||||
@@ -359,140 +338,40 @@ mkdir -p "$MAIL_DIR"
|
||||
cat
|
||||
} > "$MAIL_FILE"
|
||||
chmod 644 "$MAIL_FILE"
|
||||
echo "Mail saved to: $MAIL_FILE" >&2
|
||||
exit 0
|
||||
SENDMAIL_EOF
|
||||
chmod +x /usr/local/bin/bspds-sendmail
|
||||
|
||||
cat > /usr/local/bin/bspds-mailq << 'MAILQ_EOF'
|
||||
#!/bin/bash
|
||||
MAIL_DIR="/var/spool/bspds-mail"
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
show_help() {
|
||||
echo "bspds-mailq - View captured emails from BSPDS mail trap"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " bspds-mailq List all captured emails"
|
||||
echo " bspds-mailq <number> View email by number (from list)"
|
||||
echo " bspds-mailq <filename> View email by filename"
|
||||
echo " bspds-mailq latest View the most recent email"
|
||||
echo " bspds-mailq clear Delete all captured emails"
|
||||
echo " bspds-mailq watch Watch for new emails (tail -f style)"
|
||||
echo " bspds-mailq count Show count of emails in queue"
|
||||
echo ""
|
||||
}
|
||||
list_emails() {
|
||||
if [[ ! -d "$MAIL_DIR" ]] || [[ -z "$(ls -A "$MAIL_DIR" 2>/dev/null)" ]]; then
|
||||
echo -e "${YELLOW}No emails in queue.${NC}"
|
||||
return
|
||||
fi
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} BSPDS Mail Queue${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
local i=1
|
||||
for f in $(ls -t "$MAIL_DIR"/*.eml 2>/dev/null); do
|
||||
local filename=$(basename "$f")
|
||||
local received=$(grep "^X-BSPDS-Received:" "$f" 2>/dev/null | cut -d' ' -f2-)
|
||||
local to=$(grep -i "^To:" "$f" 2>/dev/null | head -1 | cut -d' ' -f2-)
|
||||
local subject=$(grep -i "^Subject:" "$f" 2>/dev/null | head -1 | sed 's/^Subject: *//')
|
||||
echo -e "${BLUE}[$i]${NC} ${filename}"
|
||||
echo -e " To: ${GREEN}${to:-unknown}${NC}"
|
||||
echo -e " Subject: ${YELLOW}${subject:-<no subject>}${NC}"
|
||||
echo -e " Received: ${received:-unknown}"
|
||||
echo ""
|
||||
((i++))
|
||||
done
|
||||
echo -e "${CYAN}Total: $((i-1)) email(s)${NC}"
|
||||
}
|
||||
view_email() {
|
||||
local target="$1"
|
||||
local file=""
|
||||
if [[ "$target" == "latest" ]]; then
|
||||
file=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | head -1)
|
||||
elif [[ "$target" =~ ^[0-9]+$ ]]; then
|
||||
file=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | sed -n "${target}p")
|
||||
elif [[ -f "$MAIL_DIR/$target" ]]; then
|
||||
file="$MAIL_DIR/$target"
|
||||
elif [[ -f "$target" ]]; then
|
||||
file="$target"
|
||||
fi
|
||||
if [[ -z "$file" ]] || [[ ! -f "$file" ]]; then
|
||||
echo -e "${RED}Email not found: $target${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} $(basename "$file")${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
cat "$file"
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
}
|
||||
clear_queue() {
|
||||
local count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
|
||||
if [[ "$count" -eq 0 ]]; then
|
||||
echo -e "${YELLOW}Queue is already empty.${NC}"
|
||||
return
|
||||
fi
|
||||
rm -f "$MAIL_DIR"/*.eml
|
||||
echo -e "${GREEN}Cleared $count email(s) from queue.${NC}"
|
||||
}
|
||||
watch_queue() {
|
||||
echo -e "${CYAN}Watching for new emails... (Ctrl+C to stop)${NC}"
|
||||
echo ""
|
||||
local last_count=0
|
||||
while true; do
|
||||
local current_count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
|
||||
if [[ "$current_count" -gt "$last_count" ]]; then
|
||||
echo -e "${GREEN}[$(date +%H:%M:%S)] New email received!${NC}"
|
||||
view_email latest
|
||||
last_count=$current_count
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
count_queue() {
|
||||
local count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
|
||||
echo "$count"
|
||||
}
|
||||
case "${1:-}" in
|
||||
""|list)
|
||||
list_emails
|
||||
case "${1:-list}" in
|
||||
list)
|
||||
ls -lt "$MAIL_DIR"/*.eml 2>/dev/null | head -20 || echo "No emails"
|
||||
;;
|
||||
latest|[0-9]*)
|
||||
view_email "$1"
|
||||
latest)
|
||||
f=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | head -1)
|
||||
[[ -f "$f" ]] && cat "$f" || echo "No emails"
|
||||
;;
|
||||
clear)
|
||||
clear_queue
|
||||
;;
|
||||
watch)
|
||||
watch_queue
|
||||
rm -f "$MAIL_DIR"/*.eml
|
||||
echo "Cleared"
|
||||
;;
|
||||
count)
|
||||
count_queue
|
||||
ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
[0-9]*)
|
||||
f=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | sed -n "${1}p")
|
||||
[[ -f "$f" ]] && cat "$f" || echo "Not found"
|
||||
;;
|
||||
*)
|
||||
if [[ -f "$MAIL_DIR/$1" ]] || [[ -f "$1" ]]; then
|
||||
view_email "$1"
|
||||
else
|
||||
echo -e "${RED}Unknown command: $1${NC}"
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
[[ -f "$MAIL_DIR/$1" ]] && cat "$MAIL_DIR/$1" || echo "Usage: bspds-mailq [list|latest|clear|count|N]"
|
||||
;;
|
||||
esac
|
||||
MAILQ_EOF
|
||||
chmod +x /usr/local/bin/bspds-mailq
|
||||
log_success "Mail trap configured"
|
||||
|
||||
log_info "Creating BSPDS configuration..."
|
||||
mkdir -p /etc/bspds
|
||||
cat > /etc/bspds/bspds.env << EOF
|
||||
SERVER_HOST=127.0.0.1
|
||||
SERVER_PORT=3000
|
||||
@@ -518,19 +397,19 @@ MAIL_FROM_NAME=BSPDS
|
||||
SENDMAIL_PATH=/usr/local/bin/bspds-sendmail
|
||||
EOF
|
||||
chmod 600 /etc/bspds/bspds.env
|
||||
log_success "Configuration created"
|
||||
log_info "Creating BSPDS service user..."
|
||||
|
||||
log_info "Installing BSPDS..."
|
||||
id -u bspds &>/dev/null || useradd -r -s /sbin/nologin bspds
|
||||
cp /opt/bspds/target/release/bspds /usr/local/bin/
|
||||
mkdir -p /var/lib/bspds
|
||||
cp -r /opt/bspds/frontend/dist /var/lib/bspds/frontend
|
||||
chown -R bspds:bspds /var/lib/bspds
|
||||
log_success "BSPDS binary installed"
|
||||
log_info "Creating systemd service..."
|
||||
|
||||
cat > /etc/systemd/system/bspds.service << 'EOF'
|
||||
[Unit]
|
||||
Description=BSPDS - AT Protocol PDS
|
||||
After=network.target postgresql.service minio.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=bspds
|
||||
@@ -540,22 +419,28 @@ Environment=FRONTEND_DIR=/var/lib/bspds/frontend
|
||||
ExecStart=/usr/local/bin/bspds
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable bspds
|
||||
systemctl start bspds
|
||||
log_success "BSPDS service created and started"
|
||||
log_success "BSPDS service started"
|
||||
|
||||
log_info "Installing nginx..."
|
||||
apt install -y nginx certbot python3-certbot-nginx
|
||||
log_success "nginx installed"
|
||||
log_info "Configuring nginx..."
|
||||
apt install -y nginx
|
||||
cat > /etc/nginx/sites-available/bspds << EOF
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
@@ -571,72 +456,131 @@ server {
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
ln -sf /etc/nginx/sites-available/bspds /etc/nginx/sites-enabled/
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
log_success "nginx configured"
|
||||
log_info "Configuring firewall (ufw)..."
|
||||
|
||||
log_info "Configuring firewall..."
|
||||
apt install -y ufw
|
||||
ufw --force reset
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow ssh comment 'SSH'
|
||||
ufw allow 80/tcp comment 'HTTP'
|
||||
ufw allow 443/tcp comment 'HTTPS'
|
||||
ufw allow ssh
|
||||
ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw --force enable
|
||||
log_success "Firewall configured"
|
||||
log_info "Obtaining SSL certificate..."
|
||||
certbot --nginx -d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" --email "${CERTBOT_EMAIL}" --agree-tos --non-interactive || {
|
||||
log_warn "Wildcard cert failed (requires DNS challenge). Trying single domain..."
|
||||
certbot --nginx -d "${PDS_DOMAIN}" --email "${CERTBOT_EMAIL}" --agree-tos --non-interactive
|
||||
|
||||
echo ""
|
||||
log_info "Obtaining wildcard SSL certificate..."
|
||||
echo ""
|
||||
echo "User handles are served as subdomains (e.g., alice.${PDS_DOMAIN}),"
|
||||
echo "so you need a wildcard certificate. This requires DNS validation."
|
||||
echo ""
|
||||
echo "You'll need to add a TXT record to your DNS when prompted."
|
||||
echo ""
|
||||
read -p "Ready to proceed? (y/N): " CERT_READY
|
||||
|
||||
if [[ "$CERT_READY" =~ ^[Yy]$ ]]; then
|
||||
apt install -y certbot python3-certbot-nginx
|
||||
|
||||
log_info "Running certbot with DNS challenge..."
|
||||
echo ""
|
||||
echo "When prompted, add the TXT record to your DNS, wait a minute"
|
||||
echo "for propagation, then press Enter to continue."
|
||||
echo ""
|
||||
|
||||
if certbot certonly --manual --preferred-challenges dns \
|
||||
-d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" \
|
||||
--email "${CERTBOT_EMAIL}" --agree-tos; then
|
||||
|
||||
cat > /etc/nginx/sites-available/bspds << EOF
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://\$host\$request_uri;
|
||||
}
|
||||
}
|
||||
log_success "SSL certificate obtained"
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/${PDS_DOMAIN}/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/${PDS_DOMAIN}/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
client_max_body_size 100M;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
nginx -t && systemctl reload nginx
|
||||
log_success "Wildcard SSL certificate installed"
|
||||
|
||||
echo ""
|
||||
log_warn "Certificate renewal note:"
|
||||
echo "Manual DNS challenges don't auto-renew. Before expiry, run:"
|
||||
echo " certbot renew --manual"
|
||||
echo ""
|
||||
echo "For auto-renewal, consider using a DNS provider plugin:"
|
||||
echo " apt install python3-certbot-dns-cloudflare # or your provider"
|
||||
echo ""
|
||||
else
|
||||
log_warn "Wildcard cert failed. You can retry later with:"
|
||||
echo " certbot certonly --manual --preferred-challenges dns \\"
|
||||
echo " -d ${PDS_DOMAIN} -d '*.${PDS_DOMAIN}'"
|
||||
fi
|
||||
else
|
||||
log_warn "Skipping SSL. Your PDS is running on HTTP only."
|
||||
echo "To add SSL later, run:"
|
||||
echo " certbot certonly --manual --preferred-challenges dns \\"
|
||||
echo " -d ${PDS_DOMAIN} -d '*.${PDS_DOMAIN}'"
|
||||
fi
|
||||
|
||||
log_info "Verifying installation..."
|
||||
sleep 3
|
||||
if curl -s "http://localhost:3000/xrpc/_health" | grep -q "version"; then
|
||||
log_success "BSPDS is responding!"
|
||||
log_success "BSPDS is responding"
|
||||
else
|
||||
log_warn "BSPDS may still be starting up. Check: journalctl -u bspds -f"
|
||||
log_warn "BSPDS may still be starting. Check: journalctl -u bspds -f"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} INSTALLATION COMPLETE!${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||||
log_success "Installation complete"
|
||||
echo ""
|
||||
echo -e "Your PDS is now running at: ${GREEN}https://${PDS_DOMAIN}${NC}"
|
||||
echo "PDS: https://${PDS_DOMAIN}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}IMPORTANT: Save these credentials securely!${NC}"
|
||||
echo "Credentials (also in /etc/bspds/.credentials):"
|
||||
echo " DB password: ${DB_PASSWORD}"
|
||||
echo " MinIO password: ${MINIO_PASSWORD}"
|
||||
echo ""
|
||||
echo "Database password: ${DB_PASSWORD}"
|
||||
echo "MinIO password: ${MINIO_PASSWORD}"
|
||||
echo "Commands:"
|
||||
echo " journalctl -u bspds -f # logs"
|
||||
echo " systemctl restart bspds # restart"
|
||||
echo " bspds-mailq # view trapped emails"
|
||||
echo ""
|
||||
echo "Configuration file: /etc/bspds/bspds.env"
|
||||
echo ""
|
||||
echo -e "${CYAN}Useful commands:${NC}"
|
||||
echo " journalctl -u bspds -f # View BSPDS logs"
|
||||
echo " systemctl status bspds # Check BSPDS status"
|
||||
echo " systemctl restart bspds # Restart BSPDS"
|
||||
echo " curl https://${PDS_DOMAIN}/xrpc/_health # Health check"
|
||||
echo ""
|
||||
echo -e "${CYAN}Mail queue (for testing):${NC}"
|
||||
echo " bspds-mailq # List all captured emails"
|
||||
echo " bspds-mailq latest # View most recent email"
|
||||
echo " bspds-mailq 1 # View email #1 from list"
|
||||
echo " bspds-mailq watch # Watch for new emails live"
|
||||
echo " bspds-mailq clear # Clear all captured emails"
|
||||
echo ""
|
||||
echo " Emails are saved to: /var/spool/bspds-mail/"
|
||||
echo ""
|
||||
echo -e "${CYAN}DNS Records Summary:${NC}"
|
||||
echo ""
|
||||
echo " ${PDS_DOMAIN} A ${IPV4}"
|
||||
if [[ "$IPV6" != "Not available" ]]; then
|
||||
echo " ${PDS_DOMAIN} AAAA ${IPV6}"
|
||||
fi
|
||||
echo " *.${PDS_DOMAIN} A ${IPV4}"
|
||||
if [[ "$IPV6" != "Not available" ]]; then
|
||||
echo " *.${PDS_DOMAIN} AAAA ${IPV6}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${GREEN}Enjoy your new AT Protocol PDS!${NC}"
|
||||
|
||||
Reference in New Issue
Block a user