fix(docs): install docs better

Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
Lewis
2026-05-26 09:49:59 +03:00
committed by Tangled
parent e6eee18ace
commit 44cb016762
20 changed files with 443 additions and 507 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ RUN apk add --no-cache ca-certificates
COPY --from=builder /tmp/tranquil-pds /usr/local/bin/tranquil-pds
COPY --from=frontend /app/dist /var/lib/tranquil-pds/frontend
WORKDIR /app
ENV SERVER_HOST=0.0.0.0
ENV SERVER_HOST=[::]
ENV SERVER_PORT=3000
EXPOSE 3000
CMD ["tranquil-pds"]
+12 -8
View File
@@ -6,7 +6,7 @@ A Personal Data Server for the AT Protocol.
We came together to make this PDS to enable and empower our users to better host their data on this shared protocol. All of our decisions as a project are guided by their usefulness to the community: PDS hosters and end-users both.
Comparatively: Bluesky the company created a "reference PDS" that we can self-host quite easily, and that's great, but Bluesky has an incentive to make software for themselvess first & foremost, then secondly their software can be useful for us self-hosters. In contrast, Tranquil is not from a company, and will never be.
Comparatively: Bluesky the company created a "reference PDS" that we can self-host quite easily, and that's great, but Bluesky has an incentive to make software for themselves first & foremost, then secondly their software can be useful for us self-hosters. In contrast, Tranquil is not from a company, and will never be.
## What's different about Tranquil PDS
@@ -20,7 +20,7 @@ It is a superset of the reference PDS, including:
- account delegation: letting others manage an account with configurable permission levels
- a built-in web UI for account management, repo browsing, and admin
Unlike the ref PDS, Tranquil itself is compiled to a single binary with no nodeJS runtime. However, at time of writing, Tranquil requires postgres running separately.
Unlike the ref PDS, Tranquil is a single binary with no nodejs runtime. That said, at time of writing, Tranquil does require postgres running separately.
## Quick Start
@@ -46,24 +46,28 @@ just test
just lint
```
Nix users can enter a devshell with `nix develop`, or `direnv allow` to auto-enter via the bundled `.envrc`. Pre-built artifacts (including the devshell) are available from our [binary cache](docs/install-nix.md#binary-cache).
Nix users can enter a devshell with `nix develop`, or `direnv allow` to auto-enter via the bundled `.envrc`. Pre-built artifacts including the devshell are available from our [binary cache](docs/2_INSTALL_NIX.md#binary-cache).
## Production Deployment
### Quick Deploy (Docker/Podman Compose)
Edit `config.toml` with your values. Generate secrets with `openssl rand -base64 48`.
`docker-compose.prod.yaml` pulls the prebuilt image `atcr.io/tranquil.farm/tranquil-pds:latest`. Sign in to the registry first with `podman login atcr.io`. The Containers guide covers building from source.
```bash
cp example.toml config.toml
```
Edit `config.toml` with your values and generate secrets with `openssl rand -base64 48`. Set the postgres password to match `docker-compose.prod.yaml`. nginx needs a TLS certificate before it starts, so follow the wildcard cert steps in the [Containers guide](docs/2_INSTALL_CONTAINERS.md).
```bash
podman-compose -f docker-compose.prod.yaml up -d
```
### Installation Guides
- [Nix](docs/install-nix.md)
- [Containers](docs/install-containers.md)
- [Kubernetes](docs/install-kubernetes.md)
- [Nix](docs/2_INSTALL_NIX.md)
- [Containers](docs/2_INSTALL_CONTAINERS.md)
## Community
@@ -76,7 +80,7 @@ We currently don't have a shared space to chat and organize Tranquil things, but
- [@oyster.cafe](https://tangled.org/did:plc:3fwecdnvtcscjnrx2p4n7alz)
- [@nel.pet](https://tangled.org/did:plc:h5wsnqetncv6lu2weom35lg2)
### Amazing contributers
### Amazing contributors
- [@isabelroses.com](https://tangled.org/did:plc:qxichs7jsycphrsmbujwqbfb)
- [@quilling.dev](https://tangled.org/did:plc:jrtgsidnmxaen4offglr5lsh)
@@ -48,7 +48,7 @@ http {
client_max_body_size 10G;
location /xrpc/ {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
@@ -62,18 +62,8 @@ http {
proxy_request_buffering off;
}
location = /oauth-client-metadata.json {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types application/json;
sub_filter '__PDS_HOSTNAME__' $host;
}
location /oauth/ {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -84,7 +74,7 @@ http {
}
location /.well-known/ {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -93,7 +83,7 @@ http {
}
location /webhook/ {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -102,7 +92,7 @@ http {
}
location = /metrics {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -111,25 +101,25 @@ http {
}
location = /health {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /robots.txt {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /logo {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location ~ ^/u/[^/]+/did\.json$ {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -138,7 +128,7 @@ http {
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
+43
View File
@@ -0,0 +1,43 @@
#!/sbin/openrc-run
name="tranquil-pds-app"
description="Tranquil PDS app"
: "${TRANQUIL_IMAGE:=atcr.io/tranquil.farm/tranquil-pds:latest}"
: "${TRANQUIL_DATA:=/srv/tranquil-pds}"
: "${TRANQUIL_CONFIG:=/srv/tranquil-pds/config/config.toml}"
depend() {
need tranquil-pds-db
}
start() {
ebegin "Starting ${name}"
podman container exists tranquil-pds-app && podman rm -f tranquil-pds-app
podman run -d --name tranquil-pds-app \
--pod tranquil-pds \
-e "SERVER_HOST=[::]" \
-e SERVER_PORT=3000 \
-v "${TRANQUIL_CONFIG}:/etc/tranquil-pds/config.toml:ro,Z" \
-v "${TRANQUIL_DATA}/blobs:/var/lib/tranquil-pds/blobs:Z" \
-v "${TRANQUIL_DATA}/store:/var/lib/tranquil-pds/store:Z" \
"${TRANQUIL_IMAGE}"
local rc=$?
[ "${rc}" -eq 0 ] || { eend "${rc}"; return "${rc}"; }
ebegin "Waiting for tranquil-pds to accept connections"
local waited=0
while [ "${waited}" -lt 60 ]; do
podman exec tranquil-pds-app wget -q --spider http://localhost:3000/xrpc/_health && break
sleep 1
waited=$((waited + 1))
done
[ "${waited}" -lt 60 ]
eend $?
}
stop() {
ebegin "Stopping ${name}"
podman rm -f tranquil-pds-app
eend $?
}
+41
View File
@@ -0,0 +1,41 @@
#!/sbin/openrc-run
name="tranquil-pds-db"
description="Tranquil PDS postgres"
: "${TRANQUIL_DB_IMAGE:=docker.io/library/postgres:18-alpine}"
: "${TRANQUIL_DATA:=/srv/tranquil-pds}"
depend() {
need tranquil-pds-pod
}
start() {
ebegin "Starting ${name}"
podman container exists tranquil-pds-db && podman rm -f tranquil-pds-db
podman run -d --name tranquil-pds-db \
--pod tranquil-pds \
-e POSTGRES_USER=tranquil_pds \
-e POSTGRES_DB=pds \
--secret tranquil-pds-db-password,type=env,target=POSTGRES_PASSWORD \
-v "${TRANQUIL_DATA}/postgres:/var/lib/postgresql:Z" \
"${TRANQUIL_DB_IMAGE}"
local rc=$?
[ "${rc}" -eq 0 ] || { eend "${rc}"; return "${rc}"; }
ebegin "Waiting for postgres to accept connections"
local waited=0
while [ "${waited}" -lt 30 ]; do
podman exec tranquil-pds-db pg_isready -U tranquil_pds -d pds >/dev/null 2>&1 && break
sleep 1
waited=$((waited + 1))
done
[ "${waited}" -lt 30 ]
eend $?
}
stop() {
ebegin "Stopping ${name}"
podman rm -f tranquil-pds-db
eend $?
}
+30
View File
@@ -0,0 +1,30 @@
#!/sbin/openrc-run
name="tranquil-pds-nginx"
description="Tranquil PDS nginx reverse proxy"
: "${TRANQUIL_NGINX_IMAGE:=docker.io/library/nginx:1.29-alpine}"
: "${TRANQUIL_DATA:=/srv/tranquil-pds}"
: "${TRANQUIL_CONFIG_DIR:=/srv/tranquil-pds/config}"
depend() {
need tranquil-pds-app
}
start() {
ebegin "Starting ${name}"
podman container exists tranquil-pds-nginx && podman rm -f tranquil-pds-nginx
podman run -d --name tranquil-pds-nginx \
--pod tranquil-pds \
-v "${TRANQUIL_CONFIG_DIR}/nginx.conf:/etc/nginx/nginx.conf:ro,Z" \
-v "${TRANQUIL_DATA}/certs:/etc/nginx/certs:ro,Z" \
-v "${TRANQUIL_DATA}/acme:/var/www/acme:ro,Z" \
"${TRANQUIL_NGINX_IMAGE}"
eend $?
}
stop() {
ebegin "Stopping ${name}"
podman rm -f tranquil-pds-nginx
eend $?
}
+24
View File
@@ -0,0 +1,24 @@
#!/sbin/openrc-run
name="tranquil-pds-pod"
description="Tranquil PDS podman pod"
depend() {
need net
after firewall
}
start() {
ebegin "Creating ${name}"
podman pod exists tranquil-pds || \
podman pod create --name tranquil-pds \
--publish 80:80 \
--publish 443:443
eend $?
}
stop() {
ebegin "Removing ${name}"
podman pod rm -f tranquil-pds
eend $?
}
+2 -2
View File
@@ -3,9 +3,9 @@ Description=Tranquil PDS AT Protocol PDS
After=tranquil-pds-db.service
[Container]
ContainerName=tranquil-pds-app
Image=localhost/tranquil-pds:latest
Image=atcr.io/tranquil.farm/tranquil-pds:latest
Pod=tranquil-pds.pod
Environment=SERVER_HOST=0.0.0.0
Environment=SERVER_HOST=[::]
Environment=SERVER_PORT=3000
Volume=/srv/tranquil-pds/config/config.toml:/etc/tranquil-pds/config.toml:ro,Z
Volume=/srv/tranquil-pds/blobs:/var/lib/tranquil-pds/blobs:Z
@@ -1,21 +0,0 @@
[Unit]
Description=Tranquil PDS frontend
After=tranquil-pds-app.service
[Container]
ContainerName=tranquil-pds-frontend
Image=localhost/tranquil-pds-frontend:latest
Pod=tranquil-pds.pod
Volume=/opt/tranquil-pds/frontend/nginx-quadlet.conf:/etc/nginx/conf.d/default.conf:ro,Z
HealthCmd=wget -q --spider http://localhost:8080/
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3
HealthStartPeriod=5s
[Service]
Restart=always
RestartSec=10
[Install]
WantedBy=default.target
+2 -2
View File
@@ -1,9 +1,9 @@
[Unit]
Description=Tranquil PDS nginx reverse proxy
After=tranquil-pds-app.service tranquil-pds-frontend.service
After=tranquil-pds-app.service
[Container]
ContainerName=tranquil-pds-nginx
Image=docker.io/library/nginx:1.28-alpine
Image=docker.io/library/nginx:1.29-alpine
Pod=tranquil-pds.pod
Volume=/srv/tranquil-pds/config/nginx.conf:/etc/nginx/nginx.conf:ro,Z
Volume=/srv/tranquil-pds/certs:/etc/nginx/certs:ro,Z
+2 -5
View File
@@ -1,12 +1,9 @@
services:
tranquil-pds:
build:
context: .
dockerfile: Dockerfile
image: tranquil-pds:latest
image: atcr.io/tranquil.farm/tranquil-pds:latest
restart: unless-stopped
environment:
SERVER_HOST: "0.0.0.0"
SERVER_HOST: "[::]"
volumes:
- ./config.toml:/etc/tranquil-pds/config.toml:ro
# In memory of @olaren.dev's blobs when lewis forgot to update /tranquil to /tranquil-pds :(
+1 -12
View File
@@ -29,7 +29,7 @@ services:
cargo watch -x "run -p tranquil-server --features native-tls-roots -- --config /app/config.dev.toml"
'
environment:
SERVER_HOST: "0.0.0.0"
SERVER_HOST: "[::]"
SQLX_OFFLINE: "true"
PLC_DIRECTORY_URL: "http://plc:2582"
volumes:
@@ -46,17 +46,6 @@ services:
plc:
condition: service_started
frontend:
profiles: [prod]
build:
context: ./frontend
dockerfile: Dockerfile
image: tranquil-pds-frontend
ports:
- "8080:80"
depends_on:
- app
frontend-dev:
profiles: [dev]
image: node:24-alpine
+213 -294
View File
@@ -2,17 +2,27 @@
This guide covers deploying Tranquil PDS using containers with podman.
- **Debian 13+**: Uses systemd quadlets (modern, declarative container management)
- **Alpine 3.23+**: Uses OpenRC service script with podman-compose
- **Debian 13+**: Uses systemd quadlets
- **Alpine 3.23+**: Uses per-container OpenRC init scripts
## Prerequisites
- A server :p
- Disk space for blobs (depends on usage; plan for ~1GB per active user as a baseline)
- Disk space for blobs, around* 1GB per active user as a baseline
- A domain name pointing to your server's IP
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
- A **wildcard TLS certificate** for `*.pds.example.com`, since user handles are served as subdomains
- Root/sudo/doas access
> 🦪 Lewis
>
> * "around" here meaning "at absolute least!"
## Reverse proxy
The bundled pod ships nginx as its reverse proxy, and this guide uses it throughout. nginx is just the default. Swap in whatever you prefer. Tranquil serves its API and web UI on a single port, so any reverse proxy works once it forwards to the app on `[::1]:3000`.
Caddy is one good option. It grabs & renews TLS certificates automatically, including the wildcard this setup needs (if you're lucky, which Lewis is not), so the manual certbot steps later become unnecessary. We plan to soon also have the ability to terminate TLS directly in-Tranquil so that there's no reverse proxy needed.
## Quickstart (docker/podman compose)
If you just want to get running quickly:
@@ -23,66 +33,55 @@ cp example.toml config.toml
Edit `config.toml` with your values. Generate secrets with `openssl rand -base64 48`.
Build and start:
`docker-compose.prod.yaml` pulls the prebuilt image from atcr.io. Sign in to the registry first with `podman login atcr.io`.
nginx will not start without a certificate, so create a temporary self-signed one, bring the stack up, then swap in a real wildcard cert:
```sh
podman build -t tranquil-pds:latest .
podman build -t tranquil-pds-frontend:latest ./frontend
mkdir -p certs
openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
-keyout certs/privkey.pem -out certs/fullchain.pem \
-subj "/CN=pds.example.com"
podman-compose -f docker-compose.prod.yaml up -d
```
Get initial certificate (after DNS is configured):
To build the image from source instead, run `podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .` before bringing the stack up.
User handles are subdomains, so the real certificate must be a wildcard for `*.pds.example.com`, which requires DNS-01 validation. Follow the DNS cert steps in the Wildcard TLS certificate section below, then:
```sh
podman-compose -f docker-compose.prod.yaml run --rm certbot certonly \
--webroot -w /var/www/acme -d pds.example.com -d '*.pds.example.com'
ln -sf live/pds.example.com/fullchain.pem certs/fullchain.pem
ln -sf live/pds.example.com/privkey.pem certs/privkey.pem
podman-compose -f docker-compose.prod.yaml restart nginx
```
The end!!!
## Standalone container without compose
Or wait, you want more? Perhaps a deployment that comes back on server restart?
If you already have postgres running on the host, you can run just the app container.
For production setups with proper service management, continue to either the Debian or Alpine section below.
## Standalone containers (no compose)
If you already have postgres running on the host, you can run just the app containers.
Build the images:
Pull the image. atcr.io requires authentication, so sign in first:
```sh
podman build -t tranquil-pds:latest .
podman build -t tranquil-pds-frontend:latest ./frontend
podman login atcr.io
podman pull atcr.io/tranquil.farm/tranquil-pds:latest
```
Run the backend with host networking (so it can access postgres on localhost) and mount the blob storage:
Run with host networking so it can reach postgres on localhost, and mount config + storage:
```sh
podman run -d --name tranquil-pds \
--network=host \
-v /etc/tranquil-pds/config.toml:/etc/tranquil-pds/config.toml:ro,Z \
-v /var/lib/tranquil-pds:/var/lib/tranquil-pds:Z \
tranquil-pds:latest
atcr.io/tranquil.farm/tranquil-pds:latest
```
Run the frontend with port mapping (the container's nginx listens on port 80):
```sh
podman run -d --name tranquil-pds-frontend \
-p 8080:80 \
tranquil-pds-frontend:latest
```
To build from source instead, run `podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .` and use that tag.
Then configure your host nginx to proxy to both containers. Replace the static file `try_files` directives with proxy passes:
Then point your reverse proxy at the app on port 3000 for every route. With nginx that looks like:
```nginx
# API routes to backend
location /xrpc/ {
proxy_pass http://127.0.0.1:3000;
# ... (see Debian guide for full proxy headers)
proxy_pass http://[::1]:3000;
# full proxy headers are in deploy/nginx/nginx-pod.conf
}
# Static routes to frontend container
location / {
proxy_pass http://127.0.0.1:8080;
proxy_pass http://[::1]:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -91,7 +90,98 @@ location / {
}
```
See the Debian with systemd quadlets section below for the full nginx config with all API routes.
With Caddy the equivalent is a one-line `reverse_proxy [::1]:3000`, and it handles TLS for you. See `deploy/nginx/nginx-pod.conf` in the repo for the full nginx config with all routes.
The end!!!
Or wait, you want more? Perhaps a deployment that comes back on server restart?
---
# Common setup
Both service-managed deployments share the steps below. Do these first, then jump to the Debian or Alpine section for the init-specific stuff, and finish in the Wildcard TLS certificate section.
## Install podman
**Debian:**
```bash
apt update
apt install -y podman
```
**Alpine:**
```sh
apk update
apk add podman fuse-overlayfs
rc-update add cgroups
rc-service cgroups start
```
## Create the directory structure
```sh
mkdir -p /srv/tranquil-pds/{postgres,blobs,store,certs,acme,config}
```
## Clone the repo and pull the image
The repo provides the quadlet, OpenRC, and nginx files. The image comes prebuilt from atcr.io. Sign in before pulling:
```sh
cd /opt
git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
podman login atcr.io
podman pull atcr.io/tranquil.farm/tranquil-pds:latest
```
To build from source instead, tag it with the same name so the service uses it:
```sh
cd tranquil-pds && podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .
```
## Create a configuration file
```sh
cp /opt/tranquil-pds/example.toml /srv/tranquil-pds/config/config.toml
chmod 600 /srv/tranquil-pds/config/config.toml
```
Edit `/srv/tranquil-pds/config/config.toml` and fill in your values. Generate secrets with:
```sh
openssl rand -base64 48
```
> 🦪 Lewis
>
> Every config option can also be set via an environment variable
> named in the comments in `example.toml`. Environment variables always take
> precedence over the config file.
## Create the nginx config and database secret
The bundled pod runs nginx as its proxy. The app and nginx share the pod's network namespace, so the proxy reaches the app on `[::1]:3000`:
```sh
cp /opt/tranquil-pds/deploy/nginx/nginx-pod.conf /srv/tranquil-pds/config/nginx.conf
```
Create the podman secret for the database password. Use the same password in the `database.url` of your `config.toml`:
```sh
echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password -
```
## Create a temporary TLS certificate
The nginx that comes out of the box will not start without a certificate. The real wildcard cert needs DNS-01 validation and is set up once the stack is running, so for now let's add a self-signed placeholder so the services can start:
```sh
openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
-keyout /srv/tranquil-pds/certs/privkey.pem \
-out /srv/tranquil-pds/certs/fullchain.pem \
-subj "/CN=pds.example.com"
```
If your proxy obtains its own certificates, like Caddy does, you can skip this step!
The app runs migrations itself on first boot btw, so there is no separate migration step if you're looking for one.
---
@@ -99,117 +189,32 @@ See the Debian with systemd quadlets section below for the full nginx config wit
Quadlets are a nice way to run podman containers under systemd.
## Install podman
```bash
apt update
apt install -y podman
```
## Create the directory structure
```bash
mkdir -p /etc/containers/systemd
mkdir -p /srv/tranquil-pds/{postgres,blobs,store,certs,acme,config}
```
## Create a configuration file
```bash
cp /opt/tranquil-pds/example.toml /srv/tranquil-pds/config/config.toml
chmod 600 /srv/tranquil-pds/config/config.toml
```
Edit `/srv/tranquil-pds/config/config.toml` and fill in your values. Generate secrets with:
```bash
openssl rand -base64 48
```
> **Note:** Every config option can also be set via environment variables
> (see comments in `example.toml`). Environment variables always take
> precedence over the config file.
## Install quadlet definitions
Copy the quadlet files from the repository:
```bash
mkdir -p /etc/containers/systemd
cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds.pod /etc/containers/systemd/
cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-db.container /etc/containers/systemd/
cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-app.container /etc/containers/systemd/
cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-frontend.container /etc/containers/systemd/
cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-nginx.container /etc/containers/systemd/
```
Optional quadlets for valkey and minio are also available in `deploy/quadlets/` if you need them.
## Create nginx configuration
```bash
cp /opt/tranquil-pds/nginx.conf /srv/tranquil-pds/config/nginx.conf
```
## Clone and build images
```bash
cd /opt
git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
cd tranquil-pds
podman build -t tranquil-pds:latest .
podman build -t tranquil-pds-frontend:latest ./frontend
```
## Create podman secrets
```bash
echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password -
```
## Start services and initialize
## Start services
```bash
systemctl daemon-reload
systemctl start tranquil-pds-db
sleep 10
```
## Obtain a wildcard SSL cert
User handles are served as subdomains (eg. `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/tranquil-pds/certs/privkey.pem \
-out /srv/tranquil-pds/certs/fullchain.pem \
-subj "/CN=pds.example.com"
systemctl start tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx
```
Get a wildcard certificate using DNS validation:
```bash
podman run --rm -it \
-v /srv/tranquil-pds/certs:/etc/letsencrypt:Z \
docker.io/certbot/certbot:v5.2.2 certonly \
--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 (eg. cloudflare, route53).
Link certificates and restart:
```bash
ln -sf /srv/tranquil-pds/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/certs/fullchain.pem
ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem
systemctl restart tranquil-pds-nginx
systemctl start tranquil-pds-app tranquil-pds-nginx
```
## Enable all services
```bash
systemctl enable tranquil-pds-db tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx
systemctl enable tranquil-pds-db tranquil-pds-app tranquil-pds-nginx
```
## Configure firewall if you're into that sort of thing
@@ -222,165 +227,38 @@ ufw allow 443/tcp
ufw enable
```
## Cert renewal
Add to root's crontab (`crontab -e`):
```
0 0 * * * podman run --rm -v /srv/tranquil-pds/certs:/etc/letsencrypt:Z -v /srv/tranquil-pds/acme:/var/www/acme:Z docker.io/certbot/certbot:v5.2.2 renew --quiet && systemctl reload tranquil-pds-nginx
```
Now finish in the Wildcard TLS certificate section below.
---
# Alpine with OpenRC
Alpine uses OpenRC, not systemd. So instead of quadlets we'll use podman-compose with an OpenRC service wrapper.
Alpine uses OpenRC, not systemd, bless its soul. So instead of quadlets we use a set of OpenRC init scripts, one per container.
## Install podman
## Install the OpenRC services
Copy the init scripts. They run the pod, postgres, the app, and nginx as separate services ordered with `depend()`:
```sh
apk update
apk add podman podman-compose fuse-overlayfs cni-plugins
rc-update add cgroups
rc-service cgroups start
cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-pod /etc/init.d/
cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-db /etc/init.d/
cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-app /etc/init.d/
cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-nginx /etc/init.d/
chmod +x /etc/init.d/tranquil-pds-pod /etc/init.d/tranquil-pds-db /etc/init.d/tranquil-pds-app /etc/init.d/tranquil-pds-nginx
```
Enable podman socket for compose:
The scripts default to `/srv/tranquil-pds` for data and `/srv/tranquil-pds/config/config.toml` for config. Override via `/etc/conf.d/tranquil-pds-app` and friends if your paths differ.
## Start services
Starting the nginx service pulls in the pod, postgres, and app through its dependencies:
```sh
rc-update add podman
rc-service podman start
rc-service tranquil-pds-nginx start
```
## Create the directory structure
## Enable services at boot time
```sh
mkdir -p /srv/tranquil-pds/{data,config}
mkdir -p /srv/tranquil-pds/data/{postgres,blobs,certs,acme}
```
## Clone the repo and build images
```sh
cd /opt
git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
cd tranquil-pds
podman build -t tranquil-pds:latest .
podman build -t tranquil-pds-frontend:latest ./frontend
```
## Create a configuration file
```sh
cp /opt/tranquil-pds/example.toml /srv/tranquil-pds/config/config.toml
chmod 600 /srv/tranquil-pds/config/config.toml
```
Edit `/srv/tranquil-pds/config/config.toml` and fill in your values. Generate secrets with:
```sh
openssl rand -base64 48
```
> **Note:** Every config option can also be set via environment variables
> (see comments in `example.toml`). Environment variables always take
> precedence over the config file.
## Set up compose and nginx
Copy the production compose and nginx configs:
```sh
cp /opt/tranquil-pds/docker-compose.prod.yaml /srv/tranquil-pds/docker-compose.yml
cp /opt/tranquil-pds/nginx.conf /srv/tranquil-pds/config/nginx.conf
```
Edit `/srv/tranquil-pds/docker-compose.yml` to adjust paths if needed:
- Update volume mounts to use `/srv/tranquil-pds/data/` paths
- Update nginx config path to `/srv/tranquil-pds/config/nginx.conf`
Edit `/srv/tranquil-pds/config/nginx.conf` to update cert paths:
- Change `/etc/nginx/certs/live/${PDS_HOSTNAME}/` to `/etc/nginx/certs/`
## Create OpenRC service
```sh
cat > /etc/init.d/tranquil-pds << 'EOF'
#!/sbin/openrc-run
name="tranquil-pds"
description="Tranquil PDS AT Protocol PDS"
command="/usr/bin/podman-compose"
command_args="-f /srv/tranquil-pds/docker-compose.yml up"
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"
directory="/srv/tranquil-pds"
depend() {
need net podman
after firewall
}
start_pre() {
checkpath -d /srv/tranquil-pds
}
stop() {
ebegin "Stopping ${name}"
cd /srv/tranquil-pds
podman-compose -f /srv/tranquil-pds/docker-compose.yml down
eend $?
}
EOF
chmod +x /etc/init.d/tranquil-pds
```
## Initialize services
Start services:
```sh
rc-service tranquil-pds start
sleep 15
```
Run migrations:
```sh
apk add rustup
rustup-init -y
source ~/.cargo/env
cargo install sqlx-cli --no-default-features --features postgres
DB_IP=$(podman inspect tranquil-pds-db-1 --format '{{.NetworkSettings.Networks.tranquil-pds_default.IPAddress}}')
DATABASE_URL="postgres://tranquil_pds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations
```
## Obtain wildcard SSL cert
User handles are served as subdomains (eg. `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/tranquil-pds/data/certs/privkey.pem \
-out /srv/tranquil-pds/data/certs/fullchain.pem \
-subj "/CN=pds.example.com"
rc-service tranquil-pds restart
```
Get a wildcard certificate using DNS validation:
```sh
podman run --rm -it \
-v /srv/tranquil-pds/data/certs:/etc/letsencrypt \
docker.io/certbot/certbot:v5.2.2 certonly \
--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/tranquil-pds/data/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/data/certs/fullchain.pem
ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/data/certs/privkey.pem
rc-service tranquil-pds restart
```
## Enable service at boot time
```sh
rc-update add tranquil-pds
rc-update add tranquil-pds-pod tranquil-pds-db tranquil-pds-app tranquil-pds-nginx
```
## Configure firewall if you're into that sort of thing
@@ -405,11 +283,58 @@ rc-update add ip6tables
/etc/init.d/ip6tables save
```
Now finish in the Wildcard TLS certificate section below.
---
# Wildcard TLS certificate
This section sets up the real certificate for the bundled nginx.
With the stack running behind the temporary self-signed certificate, swap in a real wildcard cert. User handles are served as subdomains like `nel.pds.example.com`, so the certificate must cover `*.pds.example.com`, which requires DNS-01 validation.
Get a wildcard certificate using DNS validation:
```sh
podman run --rm -it \
-v /srv/tranquil-pds/certs:/etc/letsencrypt:Z \
docker.io/certbot/certbot:v5.2.2 certonly \
--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 or something.
Link the certificates into place:
```sh
ln -sf /srv/tranquil-pds/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/certs/fullchain.pem
ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem
```
Restart nginx to load them:
**Debian:**
```bash
systemctl restart tranquil-pds-nginx
```
**Alpine:**
```sh
rc-service tranquil-pds-nginx restart
```
## Cert renewal
Add to root's crontab (`crontab -e`):
Manual mode doesn't auto-renew, so add a renewal job to root's crontab with `crontab -e`.
**Debian:**
```
0 0 * * * podman run --rm -v /srv/tranquil-pds/data/certs:/etc/letsencrypt -v /srv/tranquil-pds/data/acme:/var/www/acme docker.io/certbot/certbot:v5.2.2 renew --quiet && rc-service tranquil-pds restart
0 0 * * * podman run --rm -v /srv/tranquil-pds/certs:/etc/letsencrypt:Z -v /srv/tranquil-pds/acme:/var/www/acme:Z docker.io/certbot/certbot:v5.2.2 renew --quiet && systemctl reload tranquil-pds-nginx
```
**Alpine:**
```
0 0 * * * podman run --rm -v /srv/tranquil-pds/certs:/etc/letsencrypt -v /srv/tranquil-pds/acme:/var/www/acme docker.io/certbot/certbot:v5.2.2 renew --quiet && rc-service tranquil-pds-nginx restart
```
---
@@ -429,55 +354,49 @@ curl -s https://pds.example.com/.well-known/atproto-did
```bash
journalctl -u tranquil-pds-app -f
podman logs -f tranquil-pds-app
podman logs -f tranquil-pds-frontend
```
**Alpine:**
```sh
podman-compose -f /srv/tranquil-pds/docker-compose.yml logs -f
podman logs -f tranquil-pds-tranquil-pds-1
podman logs -f tranquil-pds-frontend-1
rc-service tranquil-pds-app status
podman logs -f tranquil-pds-app
```
## Update Tranquil PDS
Pull the latest image:
```sh
cd /opt/tranquil-pds
git pull
podman build -t tranquil-pds:latest .
podman build -t tranquil-pds-frontend:latest ./frontend
podman login atcr.io
podman pull atcr.io/tranquil.farm/tranquil-pds:latest
```
Debian:
```bash
systemctl restart tranquil-pds-app tranquil-pds-frontend
systemctl restart tranquil-pds-app
```
Alpine:
```sh
rc-service tranquil-pds restart
rc-service tranquil-pds-app restart
```
To update a source-built deployment, `git pull` in the repo and rebuild with `podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .` before restarting.
## Backup database
**Debian:**
```bash
podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql
```
**Alpine:**
```sh
podman exec tranquil-pds-db-1 pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql
podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql
```
## Custom homepage
The frontend container serves `homepage.html` as the landing page. To customize it, either:
If a `homepage.html` exists in the app's frontend directory it is served at `/`. The account dashboard stays at `/app/`. The directory defaults to `/var/lib/tranquil-pds/frontend` and is set by `FRONTEND_DIR`. Mount your own into the app container:
1. Build a custom frontend image with your own `homepage.html`
2. Mount a custom `homepage.html` into the frontend container
```sh
-v /srv/tranquil-pds/homepage.html:/var/lib/tranquil-pds/frontend/homepage.html:ro,Z
```
Example custom homepage:
For ex:
```html
<!DOCTYPE html>
<html>
-43
View File
@@ -1,43 +0,0 @@
# Tranquil PDS 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)
- a PersistentVolume for blob storage
- 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:
- A TOML config file mounted at `/etc/tranquil-pds/config.toml` (or passed via `--config`)
- `DATABASE_URL` - postgres connection string
- `BLOB_STORAGE_PATH` - path to blob storage (mount a PV here)
- `PDS_HOSTNAME` - your PDS hostname (without protocol)
- `JWT_SECRET`, `DPOP_SECRET`, `MASTER_KEY` - generate with `openssl rand -base64 48`
- `CRAWLERS` - typically `https://bsky.network`
and more, check the example.toml for all options. Environment variables can override any TOML value.
You can also point to a config file via the `TRANQUIL_PDS_CONFIG` env var.
Health check: `GET /xrpc/_health`
## Custom homepage
Mount a ConfigMap with your `homepage.html` into the container's frontend directory and it becomes your landing page. Go nuts with it. Account dashboard is at `/app/` so you won't break anything.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: pds-homepage
data:
homepage.html: |
<!DOCTYPE html>
<html>
<head><title>Welcome to my PDS</title></head>
<body>
<h1>Welcome to my little evil secret lab!!!</h1>
<p><a href="/app/">Sign in</a></p>
</body>
</html>
```
+2 -9
View File
@@ -81,14 +81,7 @@ See [example.toml](https://tangled.org/tranquil.farm/tranquil-pds/blob/main/exam
### Secrets
Additionally, you will need to provide the following environment variables.
```env
# use `openssl rand -base64 32` to generate these.
JWT_SECRET=<secret_here>
DPOP_SECRET=<secret_here>
MASTER_KEY=<secret_here>
```
Secrets must not live in the nix store. Provide the `jwt_secret`, `dpop_secret`, and `master_key` values through `environmentFiles` instead of `settings`. example.toml documents the matching environment variable name for each. Generate each with `openssl rand -base64 48`.
The simplest (least secure and least reproducible) option is to provide these secrets in a `.env` file using the `environmentFiles` option as shown above.
@@ -224,7 +217,7 @@ The log entry shold look something like this:
## Binary cache
Pre-built artifacts from the flake — the package, frontend, and devshell — are published to [tranquil.cachix.org](https://tranquil.cachix.org). To pull from it instead of building locally, add to your NixOS config:
The flake publishes its package, frontend, and devshell to [tranquil.cachix.org](https://tranquil.cachix.org). To pull from it instead of building locally, add to your NixOS config:
```nix
nix.settings = {
+57
View File
@@ -0,0 +1,57 @@
# Tranquil PDS deployment using its own embedded DB
Welcome, brave one.
So you're interested in leaving relational databases behind? Raw performance? Or... perhaps you simply want to run less services on your machine?
Tranquil's embedded DB is experimental.
Risk of total data loss.
## What's the difference?
tranquil-store replaces the entire repository layer. When it is selected the server opens no postgres connection at all. The postgres service, its password secret, and the `database.url` value are all unused. Blob storage is however untouched: filesystem or S3 applies exactly as in the base guide.
2 settings select and place the store:
- `repo_backend` under `[storage]`, environment variable `REPO_BACKEND`. Set it to `"tranquil-store"`. The default is `"postgres"`.
- `data_dir` under `[tranquil_store]`, environment variable `TRANQUIL_STORE_DATA_DIR`. This is optional. It defaults to `/var/lib/tranquil-pds/store`.
So the minimum config delta is one line:
```toml
[storage]
repo_backend = "tranquil-store"
```
## That being said, here are the facts:
- At time of writing, there's no way to transfer an existing Tranquil instance from PG-backed to embedded or vice-versa. If you have an instance and you want to move to embedded, you'll have to spin it up as a new instance and migrate as you would normally.
- You will absolutely want to take backups of all users' CAR files daily of not more frequently. As usual, you *really* should have rotation keys separately stored aside somewhere in case the DB explodes in an unrecoverable way.
## Installing: a patch on the existing guides
The procedure is the one in [2_INSTALL_CONTAINERS.md](2_INSTALL_CONTAINERS.md) or [2_INSTALL_NIX.md](2_INSTALL_NIX.md). Follow your chosen guide top to bottom and apply the deltas below, otherwise exactly the same!
### Containers
Both base guides assume postgres of course, and the units couple the app to it. Dropping the database means uncoupling that out too.
Shared, regardless of init system:
1. In `config.toml`, leave `database.url` unset and add the `[storage]` block shown above.
2. Skip the database secret. No need to create `tranquil-pds-db-password`.
3. The app unit already mounts the `store` directory, so `data_dir` needs no extra setup. The `postgres` directory in the guide's `mkdir` goes unused.
4. Backup section: `pg_dump` does not apply. Back up the `data_dir` instead, which holds the metastore, eventlog, and blockstore. CAR files and rotation keys still belong in your own backup as mentioned above.
**Debian (quadlets):** Do not copy `tranquil-pds-db.container`. Drop `tranquil-pds-db` from the `systemctl start` and `systemctl enable` commands. The `After=tranquil-pds-db.service` line in `tranquil-pds-app.container` becomes a no-op with the database gone. Remove it if you want.
**Alpine (OpenRC):** Do not copy the `tranquil-pds-db` init script. The app script hard-depends on it via `need tranquil-pds-db`, so edit `tranquil-pds-app`'s `depend()` to read `need tranquil-pds-pod` instead. Without this the app needs a service that no longer exists and refuses to start. Drop `tranquil-pds-db` from the `rc-update add` command too.
### Nix
1. Set `services.tranquil-pds.database.createLocally = false`. This removes the local postgres service and the automatic `database.url`.
2. Set `services.tranquil-pds.settings.storage.repo_backend = "tranquil-store";`.
3. Leave `data_dir` at its default. It sits under the service state directory and needs no extra work. If you relocate it, ensure the service user can write there.
That's it!!
Please report anything wrong to us immediately, so that we can make our DB better, faster, stronger!
-12
View File
@@ -1,12 +0,0 @@
FROM node:24-alpine AS builder
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY . ./
RUN pnpm build
FROM nginx:1.29-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
-38
View File
@@ -1,38 +0,0 @@
server {
listen 8080;
listen [::]:8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
location = /oauth-client-metadata.json {
default_type application/json;
sub_filter_once off;
sub_filter_types application/json;
sub_filter '__FRONTEND_HOSTNAME__' $host;
try_files /oauth-client-metadata.json =404;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location = / {
try_files /homepage.html /index.html;
}
location /app/ {
try_files $uri $uri/ /index.html;
}
location / {
try_files $uri $uri/ /index.html;
}
}
-38
View File
@@ -1,38 +0,0 @@
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
location = /oauth-client-metadata.json {
default_type application/json;
sub_filter_once off;
sub_filter_types application/json;
sub_filter '__FRONTEND_HOSTNAME__' $host;
try_files /oauth-client-metadata.json =404;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location = / {
try_files /homepage.html /index.html;
}
location /app/ {
try_files $uri $uri/ /index.html;
}
location / {
try_files $uri $uri/ /index.html;
}
}
+3 -2
View File
@@ -159,8 +159,9 @@ podman-down:
podman-logs:
podman compose logs -f
container-build:
podman build -t tranquil-pds:latest .
podman build -t tranquil-pds-frontend:latest ./frontend
podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .
container-pull:
podman pull atcr.io/tranquil.farm/tranquil-pds:latest
frontend-dev:
. ~/.deno/env && cd frontend && deno task dev