Archipelago — open-source initial import
This commit is contained in:
@@ -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"]
|
||||
@@ -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.
|
||||
Executable
+35
@@ -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"
|
||||
@@ -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
|
||||
Executable
+57
@@ -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."
|
||||
Reference in New Issue
Block a user