Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:49 +00:00
commit b33b9af85a
1963 changed files with 449763 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
# Vendored copy of the podsteadr repo's own Dockerfile (source lives outside
# this tree — http://146.59.87.168:3000/ssmithx/podsteadr). Re-sync by hand if
# the upstream Dockerfile changes; build with build-from-prototype.sh, which
# passes the podsteadr repo root as build context (this Dockerfile expects
# frontend/ and server/ subdirectories at the context root, not this apps/
# directory).
#
# ---- frontend ----
FROM node:22-bookworm-slim AS frontend-build
WORKDIR /build/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# ---- server ----
FROM node:22-bookworm-slim AS server-build
WORKDIR /build/server
COPY server/package*.json ./
RUN npm ci
COPY server/ ./
RUN npm run build && npm prune --omit=dev
# ---- runtime ----
FROM node:22-bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=server-build /build/server/node_modules ./node_modules
COPY --from=server-build /build/server/package.json ./package.json
COPY --from=server-build /build/server/dist ./dist
COPY --from=frontend-build /build/frontend/dist ./public
# Named volumes inherit ownership from the image path: keep /data writable by node
RUN mkdir -p /data && chown node:node /data
USER node
ENV NODE_ENV=production \
PORT=8095 \
DATA_DIR=/data \
STATIC_DIR=/app/public
EXPOSE 8095
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -fsS http://localhost:8095/api/health || exit 1
CMD ["node", "dist/index.js"]
+85
View File
@@ -0,0 +1,85 @@
# podsteadr — Nostr-native Podcasting & Livestreaming
Self-hosted, nostr-native podcast publishing and livestreaming. Log in with a
NIP-07 nostr identity (no passwords, no email), upload an mp4 to publish an
RSS 2.0 feed with Podcasting 2.0 lightning payment info, or go live via OBS
(RTMP) or the browser (WebRTC/WHIP) — the stream is announced on nostr as a
NIP-53 live event and viewers watch over HLS.
This is a three-container stack:
| App | Manifest | Role |
|---|---|---|
| `podsteadr` | `apps/podsteadr/manifest.yml` | Fastify API + built Vue UI + RSS feeds |
| `podsteadr-mediamtx` | `apps/podsteadr-mediamtx/manifest.yml` | RTMP/WHIP ingest, HLS output, recording |
| `podsteadr-blossom` | `apps/podsteadr-blossom/manifest.yml` | BUD-02 sha256-addressed media blobs |
All three join a dedicated `podsteadr-net` bridge network and resolve each
other by short DNS aliases (`podsteadr-app`, `mediamtx`, `blossom`).
## Building the Image
The app image is built from the **podsteadr** repo, source of truth at
`http://146.59.87.168:3000/ssmithx/podsteadr`.
### Option 1: Use the build script
```bash
# From archy repo root
./apps/podsteadr/build-from-prototype.sh
```
### Option 2: Build from source directory
```bash
cd ~/podsteadr
podman build -t localhost/podsteadr:1.0.0 -f ~/archy/apps/podsteadr/Dockerfile .
```
### Publishing to the shared registry
```bash
./apps/podsteadr/push-to-registry.sh 1.0.0
```
Then update `apps/podsteadr/manifest.yml`'s `container.image` to the pushed
tag so other nodes pull instead of building locally.
## Ports
See `apps/PORTS.md`. Summary: 8095 (web UI/API/RSS), 1935 (RTMP), 8889
(WebRTC/WHIP), 8189/udp (WebRTC ICE), 8890 (HLS), 8098 (Blossom).
All of podsteadr's ports are `auth: none` — this is a public podcast/livestream
server, not a private personal app; RSS feeds, HLS playback, and blob reads
must stay reachable by third-party clients with no Archipelago session, and
the app enforces its own NIP-98 signed-request auth for sensitive routes and
per-stream secret keys for RTMP/WHIP publish. See the `auth_rationale` on each
port mapping.
## Nostr Identity
podsteadr's frontend vendors a copy of Archipelago's `nostr-provider.js` shim
and references it directly from `index.html` (its Fastify server isn't the
nginx-served SPA shape the platform auto-patches — see "Nostr Signer Bridge"
in `docs/app-developer-guide.md`). `apps/podsteadr/manifest.yml` declares a
`post_install` hook that re-copies the canonical
`/opt/archipelago/web-ui/nostr-provider.js` over the vendored copy on every
install/reinstall, so it doesn't go stale across OTA releases.
## Data
- `/var/lib/archipelago/podsteadr` — SQLite DB, server's own nostr key,
covers, and (read-only here) shared stream recordings.
- `/var/lib/archipelago/podsteadr/recordings` — stream recordings (writable
by `podsteadr-mediamtx`, read-only for `podsteadr`), 7-day retention.
- `/var/lib/archipelago/podsteadr-blossom/data` — media blobs.
## Known gotchas
See the podsteadr repo's `docs/STATUS.md` for the full list (blossom v4
config `rules:` nesting, no HTTP range support in blossom 4.x, split-horizon
blossom URL, MediaMTX has no shell so status is polled not hooked, standard
vs. low-latency HLS). The blossom and mediamtx config files embedded in
`apps/podsteadr-blossom/manifest.yml` / `apps/podsteadr-mediamtx/manifest.yml`
already carry the load-bearing ones inline as comments.
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# Build the podsteadr container image from the podsteadr repo.
# Usage: ./build-from-prototype.sh [path-to-podsteadr-repo]
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_REPO="$HOME/podsteadr"
REPO_DIR="${1:-$DEFAULT_REPO}"
IMAGE_TAG="localhost/podsteadr:1.0.0"
if [ ! -d "$REPO_DIR" ]; then
echo "podsteadr repo not found at: $REPO_DIR"
echo " Set path: $0 /path/to/podsteadr"
exit 1
fi
if [ ! -f "$REPO_DIR/server/package.json" ] || [ ! -f "$REPO_DIR/frontend/package.json" ]; then
echo "No server/package.json or frontend/package.json found in $REPO_DIR — is this the right directory?"
exit 1
fi
# Determine container runtime
RUNTIME="podman"
if ! command -v podman >/dev/null 2>&1; then
RUNTIME="docker"
fi
echo "Building podsteadr from $REPO_DIR using $SCRIPT_DIR/Dockerfile"
$RUNTIME build -t "$IMAGE_TAG" -f "$SCRIPT_DIR/Dockerfile" "$REPO_DIR"
echo "Built $IMAGE_TAG"
echo ""
echo "You can now install podsteadr from the App Store in Archipelago."
echo "Or run directly: $RUNTIME run -d --name podsteadr-app -p 8095:8095 $IMAGE_TAG"
+143
View File
@@ -0,0 +1,143 @@
app:
id: podsteadr
name: podsteadr
version: "1.0.0"
description: Self-hosted, nostr-native podcast publishing and livestreaming. Log in with Nostr, upload episodes or go live via OBS/WebRTC, publish to RSS with Podcasting 2.0 lightning payments.
category: media
# Container/DNS-alias name deliberately NOT "podsteadr" — on a host whose own
# hostname happens to be "podsteadr", the host's own /etc/hosts self-hostname
# entry (127.0.1.1, e.g. from cloud-init) shadows the container network's DNS
# alias for other containers looking up "podsteadr", and mediamtx's auth-webhook
# callback resolves to the host's loopback instead of this container — every
# RTMP publish gets rejected with "connection refused" (observed on
# podsteadr.atobitcoin.io, 2026-07-30; see docker-compose.yml in the podsteadr
# repo for the original writeup). Carried forward unchanged into the manifest.
container_name: podsteadr-app
container:
# Built locally from the podsteadr repo (source lives outside this tree —
# see apps/podsteadr/README.md + build-from-prototype.sh), same pattern as
# apps/indeedhub. Not yet pushed to the shared registry; push-to-registry.sh
# is there for when fleet-wide install is needed.
image: localhost/podsteadr:1.0.0
pull_policy: if-not-present
network: podsteadr-net
network_aliases: [podsteadr-app]
derived_env:
- key: PUBLIC_URL
template: "http://{{HOST_MDNS}}:8095"
- key: MEDIAMTX_RTMP_PUBLIC
template: "rtmp://{{HOST_MDNS}}:1935"
- key: MEDIAMTX_WHIP_PUBLIC
template: "http://{{HOST_MDNS}}:8889"
- key: MEDIAMTX_HLS_PUBLIC
template: "http://{{HOST_MDNS}}:8890"
- key: BLOSSOM_URL_DEFAULT
template: "http://{{HOST_MDNS}}:8098"
# node:22-bookworm-slim's built-in `node` user is uid:gid 1000:1000. The
# image's own Dockerfile chowns /data to node:node, but that only affects
# the image layer — the actual runtime mount is the bind volume below, so
# the host directory needs the same ownership or the read-only-root,
# non-root `node` process can't open the SQLite DB (unverified against a
# real node install; flagging per this repo's convention of documenting
# bind-mount ownership assumptions, e.g. apps/botfights/manifest.yml).
data_uid: "1000:1000"
dependencies:
- app_id: podsteadr-mediamtx
- app_id: podsteadr-blossom
- storage: 2Gi
resources:
cpu_limit: 2
memory_limit: 1Gi
disk_limit: 2Gi
security:
capabilities: []
readonly_root: true
no_new_privileges: true
network_policy: isolated
ports:
- host: 8095
container: 8095
protocol: tcp
auth: none
auth_rationale: >-
podsteadr is a public podcast/livestream server: RSS feeds and the
marketplace/catalog API must stay fetchable by third-party podcast
clients, crawlers, and other podsteadr instances with no Archipelago
session, and the app already gates its own sensitive routes with
NIP-98 signed-request auth (see server/src/plugins/nostr-auth.ts in
the podsteadr repo). Putting the node's session gate in front would
block every external RSS/API consumer without adding real protection.
volumes:
- type: bind
source: /var/lib/archipelago/podsteadr
target: /data
options: [rw]
# Shares podsteadr-mediamtx's recordings directory (rw there, ro here) so
# the app can list/remux finished recordings for one-click episode
# publishing without granting it write access to live segments.
- type: bind
source: /var/lib/archipelago/podsteadr/recordings
target: /recordings
options: [ro]
environment:
- NODE_ENV=production
- PORT=8095
- DATA_DIR=/data
- RECORDINGS_DIR=/recordings
- MEDIAMTX_API_URL=http://mediamtx:9997
- BLOSSOM_URL_INTERNAL=http://blossom:3000
- NOSTR_RELAYS=wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band
- CASHU_MINT_URL_DEFAULT=https://mint.minibits.cash/Bitcoin
# podsteadr's Fastify server (fastify-static) isn't the nginx-served SPA
# shape the platform auto-patches for NIP-07 injection (see "Nostr Signer
# Bridge" in docs/app-developer-guide.md) — its frontend already
# self-references /nostr-provider.js from index.html and vendors a copy at
# build time (podsteadr commit 133558d). That vendored copy goes stale
# across archy OTA releases, so re-copy the canonical host script over it
# on every install/reinstall instead of trusting the baked-in one.
hooks:
post_install:
- copy_from_host:
src: "web-ui/nostr-provider.js"
dest: /app/public/nostr-provider.js
health_check:
type: http
endpoint: http://localhost:8095
path: /api/health
interval: 30s
timeout: 5s
retries: 3
interfaces:
main:
name: Web UI
description: Podcast dashboard, upload/live wizard, and stream management
type: ui
port: 8095
protocol: http
path: /
metadata:
author: podsteadr
icon: /assets/img/app-icons/podsteadr.png
repo: http://146.59.87.168:3000/ssmithx/podsteadr
license: MIT
tags:
- nostr
- podcast
- livestream
- media
- rss
- lightning
launch:
open_in_new_tab: false
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# Build and push the podsteadr container image to a registry.
# Usage: ./push-to-registry.sh [version]
#
# Environment variables:
# REGISTRY - Registry host (default: 146.59.87.168:3000, same as indeedhub/botfights)
# NAMESPACE - Registry namespace (default: lfg2025)
# RUNTIME - Container runtime (default: podman)
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="${PODSTEADR_REPO:-$HOME/podsteadr}"
VERSION="${1:-1.0.0}"
REGISTRY="${REGISTRY:-146.59.87.168:3000}"
NAMESPACE="${NAMESPACE:-lfg2025}"
IMAGE_NAME="podsteadr"
RUNTIME="${RUNTIME:-podman}"
FULL_TAG="${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${VERSION}"
if [ ! -d "$REPO_DIR" ]; then
echo "podsteadr repo not found at: $REPO_DIR"
echo "Set PODSTEADR_REPO=/path/to/podsteadr"
exit 1
fi
echo "=== podsteadr Container Registry Push ==="
echo "Source: $REPO_DIR"
echo "Image: $FULL_TAG"
echo "Runtime: $RUNTIME"
echo ""
echo "[1/3] Building image..."
$RUNTIME build --platform linux/amd64 \
-t "$FULL_TAG" \
-t "localhost/${IMAGE_NAME}:${VERSION}" \
-f "$SCRIPT_DIR/Dockerfile" \
"$REPO_DIR"
echo "[2/3] Pushing to registry..."
if ! $RUNTIME login --get-login "$REGISTRY" >/dev/null 2>&1; then
echo ""
echo "Not logged in to $REGISTRY."
echo "Run: $RUNTIME login $REGISTRY"
exit 1
fi
$RUNTIME push "$FULL_TAG"
echo ""
echo "[3/3] Done!"
echo ""
echo "Image pushed: $FULL_TAG"
echo ""
echo "Update apps/podsteadr/manifest.yml's container.image to $FULL_TAG so"
echo "nodes pull it instead of building locally."