Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bbd70281b | ||
|
|
395e26b524 |
@@ -25,6 +25,7 @@ This document lists all port assignments for Archipelago apps.
|
|||||||
| did-wallet | 8083 | TCP | Web UI | 18083 |
|
| did-wallet | 8083 | TCP | Web UI | 18083 |
|
||||||
| router | 8084, 5353, 1900 | TCP/UDP | Web UI, mDNS, SSDP | 18084, 15353, 11900 |
|
| router | 8084, 5353, 1900 | TCP/UDP | Web UI, mDNS, SSDP | 18084, 15353, 11900 |
|
||||||
| meshtastic | 4403, 1883 | TCP | HTTP API, MQTT | 14403, 11883 |
|
| meshtastic | 4403, 1883 | TCP | HTTP API, MQTT | 14403, 11883 |
|
||||||
|
| podsteadr | 8095, 1935, 8889, 8189, 8890, 8098 | TCP/UDP | Web UI/API/RSS, RTMP ingest, WebRTC/WHIP ingest, WebRTC ICE (udp), HLS playback, Blossom blobs | 18095, 11935, 18889, 18189, 18890, 18098 |
|
||||||
|
|
||||||
## Development Ports (Offset: +10000)
|
## Development Ports (Offset: +10000)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Containerized applications for the Archipelago Bitcoin Node OS. All apps run in
|
|||||||
- **did-wallet** — Web5 DID Wallet
|
- **did-wallet** — Web5 DID Wallet
|
||||||
|
|
||||||
### Self-Hosted Services
|
### Self-Hosted Services
|
||||||
|
- **podsteadr** — Nostr-native podcast publishing and livestreaming (RTMP/WebRTC ingest, HLS, RSS, Blossom media)
|
||||||
- **nextcloud** (v28), **jellyfin** (v10.8.13), **immich** (release), **photoprism** (v240915)
|
- **nextcloud** (v28), **jellyfin** (v10.8.13), **immich** (release), **photoprism** (v240915)
|
||||||
- **vaultwarden** (v1.30.0-alpine), **penpot** (v2.4)
|
- **vaultwarden** (v1.30.0-alpine), **penpot** (v2.4)
|
||||||
- **homeassistant** (v2024.1), **filebrowser** (v2.27.0), **searxng** (2024.11.17)
|
- **homeassistant** (v2024.1), **filebrowser** (v2.27.0), **searxng** (2024.11.17)
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
app:
|
||||||
|
id: podsteadr-blossom
|
||||||
|
name: podsteadr Blossom
|
||||||
|
version: "4"
|
||||||
|
description: Blossom (BUD-02) sha256-addressed media blob server backing podsteadr's episode uploads and covers.
|
||||||
|
category: media
|
||||||
|
|
||||||
|
# Hyphenated name matches the podsteadr repo's docker-compose container_name
|
||||||
|
# (podsteadr-blossom); alias `blossom` is the short hostname podsteadr's
|
||||||
|
# server reaches it by (BLOSSOM_URL_INTERNAL=http://blossom:3000).
|
||||||
|
container_name: podsteadr-blossom
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: ghcr.io/hzrd149/blossom-server:4
|
||||||
|
pull_policy: if-not-present
|
||||||
|
network: podsteadr-net
|
||||||
|
network_aliases: [blossom]
|
||||||
|
# Image runs as container-root (no USER directive) writing to a
|
||||||
|
# bind-mounted /app/data — CHOWN/DAC_OVERRIDE cover the fresh-bind-dir
|
||||||
|
# ownership gap the same way apps/botfights and apps/immich document.
|
||||||
|
# Unverified against a real install; check first-boot logs.
|
||||||
|
data_uid: "0:0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- storage: 20Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
cpu_limit: 1
|
||||||
|
memory_limit: 512Mi
|
||||||
|
disk_limit: 20Gi
|
||||||
|
|
||||||
|
security:
|
||||||
|
capabilities: [CHOWN, DAC_OVERRIDE, FOWNER]
|
||||||
|
readonly_root: false
|
||||||
|
no_new_privileges: true
|
||||||
|
network_policy: isolated
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- host: 8098
|
||||||
|
container: 3000
|
||||||
|
protocol: tcp
|
||||||
|
auth: none
|
||||||
|
auth_rationale: >-
|
||||||
|
Media blobs (episode audio/video, covers) must be publicly fetchable
|
||||||
|
by podcast clients as RSS enclosure URLs — that's the entire purpose
|
||||||
|
of this port. Uploads are separately gated by blossom's own BUD-02
|
||||||
|
signed-nostr-event auth (upload.requireAuth below), not a node
|
||||||
|
session; reads are intentionally public per the config's own header
|
||||||
|
comment.
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/podsteadr-blossom/data
|
||||||
|
target: /app/data
|
||||||
|
options: [rw]
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/podsteadr-blossom/config/config.yml
|
||||||
|
target: /app/config.yml
|
||||||
|
options: [ro]
|
||||||
|
|
||||||
|
environment: []
|
||||||
|
|
||||||
|
files:
|
||||||
|
- path: /var/lib/archipelago/podsteadr-blossom/config/config.yml
|
||||||
|
overwrite: true
|
||||||
|
content: |
|
||||||
|
# blossom-server (v4.x) configuration for podsteadr.
|
||||||
|
# Uploads require a signed nostr auth event (BUD-02, kind 24242);
|
||||||
|
# reads are public so podcast apps can fetch enclosures.
|
||||||
|
#
|
||||||
|
# NOTE (blossom-server 4.4.1 gotcha, do not rediscover): `rules:` MUST
|
||||||
|
# be nested under `storage:` — a top-level `rules:` key is silently
|
||||||
|
# ignored, the ruleset ends up empty, and every upload fails 401
|
||||||
|
# "Server dose not accept video/mp4 blobs" (typo is theirs). The
|
||||||
|
# GitHub master branch is a Deno rewrite with a different schema
|
||||||
|
# (storage.rules, BUD-11, range support); the `:4` image is the older
|
||||||
|
# node/koa codebase this config targets.
|
||||||
|
|
||||||
|
publicDomain: ""
|
||||||
|
|
||||||
|
databasePath: data/sqlite.db
|
||||||
|
|
||||||
|
dashboard:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
discovery:
|
||||||
|
nostr:
|
||||||
|
enabled: false
|
||||||
|
relays: []
|
||||||
|
upstream:
|
||||||
|
enabled: false
|
||||||
|
domains: []
|
||||||
|
|
||||||
|
storage:
|
||||||
|
backend: local
|
||||||
|
local:
|
||||||
|
dir: ./data/blobs
|
||||||
|
removeWhenNoOwners: false
|
||||||
|
# "expiration" is time since a blob was last accessed — unaccessed
|
||||||
|
# blobs get pruned after this. Podcast media should effectively
|
||||||
|
# never expire, so keep this long.
|
||||||
|
rules:
|
||||||
|
- type: "*"
|
||||||
|
expiration: 10 years
|
||||||
|
|
||||||
|
upload:
|
||||||
|
enabled: true
|
||||||
|
requireAuth: true
|
||||||
|
requirePubkeyInRule: false
|
||||||
|
|
||||||
|
list:
|
||||||
|
requireAuth: false
|
||||||
|
allowListOthers: true
|
||||||
|
|
||||||
|
tor:
|
||||||
|
enabled: false
|
||||||
|
proxy: ""
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
# No documented health endpoint; TCP liveness on the app port.
|
||||||
|
type: tcp
|
||||||
|
endpoint: localhost:3000
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
app:
|
||||||
|
id: podsteadr-mediamtx
|
||||||
|
name: podsteadr MediaMTX
|
||||||
|
version: "1.20.0"
|
||||||
|
description: MediaMTX ingest/output backend for podsteadr — RTMP + WebRTC/WHIP ingest, HLS playback, stream recording.
|
||||||
|
category: media
|
||||||
|
|
||||||
|
# Hyphenated name matches the podsteadr repo's docker-compose container_name
|
||||||
|
# (podsteadr-mediamtx); alias `mediamtx` is the short hostname podsteadr's
|
||||||
|
# server reaches it by (MEDIAMTX_API_URL=http://mediamtx:9997) and the one
|
||||||
|
# baked into mediamtx.yml's authHTTPAddress callback below.
|
||||||
|
container_name: podsteadr-mediamtx
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: docker.io/bluenviron/mediamtx:1.20.0
|
||||||
|
pull_policy: if-not-present
|
||||||
|
network: podsteadr-net
|
||||||
|
network_aliases: [mediamtx]
|
||||||
|
derived_env:
|
||||||
|
# Browsers need a reachable ICE host candidate for WebRTC/WHIP; without
|
||||||
|
# this, the offer only advertises container-internal addresses and
|
||||||
|
# publish/playback negotiation fails for anyone off-host.
|
||||||
|
- key: MTX_WEBRTCADDITIONALHOSTS
|
||||||
|
template: "{{HOST_MDNS}}"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- storage: 10Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
cpu_limit: 1
|
||||||
|
memory_limit: 512Mi
|
||||||
|
disk_limit: 10Gi
|
||||||
|
|
||||||
|
security:
|
||||||
|
# Stock mediamtx image runs as container-root (no USER directive) but
|
||||||
|
# only ever writes to the bind-mounted /recordings — CHOWN/DAC_OVERRIDE
|
||||||
|
# cover the fresh-bind-dir-ownership gap the same way apps/botfights and
|
||||||
|
# apps/immich document (root uid inside the container does not
|
||||||
|
# automatically bypass DAC checks once cap-drop ALL applies). Unverified
|
||||||
|
# against a real install; check first-boot logs on initial deploy.
|
||||||
|
capabilities: [CHOWN, DAC_OVERRIDE]
|
||||||
|
readonly_root: true
|
||||||
|
no_new_privileges: true
|
||||||
|
network_policy: isolated
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- host: 1935
|
||||||
|
container: 1935
|
||||||
|
protocol: tcp
|
||||||
|
auth: none
|
||||||
|
auth_rationale: >-
|
||||||
|
RTMP ingest (OBS). Not HTTP, so the node's session gate has no login
|
||||||
|
page to serve here; publish auth is delegated to podsteadr's own
|
||||||
|
HTTP auth webhook (authHTTPAddress below), which checks a per-stream
|
||||||
|
secret key never exposed in this port mapping.
|
||||||
|
- host: 8889
|
||||||
|
container: 8889
|
||||||
|
protocol: tcp
|
||||||
|
auth: none
|
||||||
|
auth_rationale: >-
|
||||||
|
WebRTC/WHIP ingest — browsers publish directly with a per-stream
|
||||||
|
bearer secret checked by podsteadr's auth webhook, the same
|
||||||
|
protocol-level auth as the RTMP port above.
|
||||||
|
- host: 8189
|
||||||
|
container: 8189
|
||||||
|
protocol: udp
|
||||||
|
auth: none
|
||||||
|
auth_rationale: >-
|
||||||
|
WebRTC ICE/UDP media transport. Raw UDP has no HTTP session concept
|
||||||
|
for the gate to enforce.
|
||||||
|
- host: 8890
|
||||||
|
container: 8888
|
||||||
|
protocol: tcp
|
||||||
|
auth: none
|
||||||
|
auth_rationale: >-
|
||||||
|
Public HLS playback URL, handed out to viewers and podcast/livestream
|
||||||
|
clients outside the node (zap.stream, third-party players). A login
|
||||||
|
page here would break every external viewer; playback is read-only.
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
# Shared with apps/podsteadr (mounted read-only there) so the app can
|
||||||
|
# list and remux finished recordings for one-click episode publishing.
|
||||||
|
source: /var/lib/archipelago/podsteadr/recordings
|
||||||
|
target: /recordings
|
||||||
|
options: [rw]
|
||||||
|
- type: bind
|
||||||
|
source: /var/lib/archipelago/podsteadr-mediamtx/config/mediamtx.yml
|
||||||
|
target: /mediamtx.yml
|
||||||
|
options: [ro]
|
||||||
|
|
||||||
|
environment: []
|
||||||
|
|
||||||
|
files:
|
||||||
|
- path: /var/lib/archipelago/podsteadr-mediamtx/config/mediamtx.yml
|
||||||
|
overwrite: true
|
||||||
|
content: |
|
||||||
|
# MediaMTX configuration for podsteadr.
|
||||||
|
# Ingest: RTMP (OBS) + WebRTC/WHIP (browser). Output: HLS. Publish auth is
|
||||||
|
# delegated to podsteadr via HTTP; stream status is polled from the API.
|
||||||
|
|
||||||
|
logLevel: info
|
||||||
|
|
||||||
|
api: yes
|
||||||
|
apiAddress: :9997
|
||||||
|
|
||||||
|
# ---- authentication ------------------------------------------------------
|
||||||
|
authMethod: http
|
||||||
|
authHTTPAddress: http://podsteadr-app:8095/api/mediamtx/auth
|
||||||
|
authHTTPExclude:
|
||||||
|
- action: api
|
||||||
|
- action: metrics
|
||||||
|
- action: pprof
|
||||||
|
|
||||||
|
# ---- protocols -----------------------------------------------------------
|
||||||
|
rtsp: no
|
||||||
|
srt: no
|
||||||
|
moq: no
|
||||||
|
|
||||||
|
rtmp: yes
|
||||||
|
rtmpAddress: :1935
|
||||||
|
|
||||||
|
hls: yes
|
||||||
|
hlsAddress: :8888
|
||||||
|
# Standard HLS, not lowLatency: LL-HLS's small per-part buffering window has very little
|
||||||
|
# tolerance for B-frame reordering (common in most OBS encoder presets), and a real test
|
||||||
|
# stream crashed the muxer twice in ~2 minutes with "too many reordered frames" / "unable to
|
||||||
|
# extract DTS" once frame timing got even slightly irregular. Standard HLS buffers a full
|
||||||
|
# segment before finalizing, which absorbs that jitter — a few extra seconds of latency
|
||||||
|
# instead of intermittent muxer crashes / viewer buffering.
|
||||||
|
hlsVariant: mpegts
|
||||||
|
hlsAlwaysRemux: yes
|
||||||
|
hlsAllowOrigins: ["*"]
|
||||||
|
|
||||||
|
webrtc: yes
|
||||||
|
webrtcAddress: :8889
|
||||||
|
webrtcLocalUDPAddress: :8189
|
||||||
|
webrtcAllowOrigins: ["*"]
|
||||||
|
|
||||||
|
# ---- recording -----------------------------------------------------------
|
||||||
|
pathDefaults:
|
||||||
|
record: yes
|
||||||
|
recordPath: /recordings/%path/%Y-%m-%d_%H-%M-%S-%f
|
||||||
|
recordFormat: fmp4
|
||||||
|
recordPartDuration: 1s
|
||||||
|
recordSegmentDuration: 1h
|
||||||
|
recordDeleteAfter: 168h
|
||||||
|
|
||||||
|
paths:
|
||||||
|
# Streams live at live/<streamId>; publish requires the stream secret,
|
||||||
|
# which podsteadr checks in the auth webhook.
|
||||||
|
"~^live/[A-Za-z0-9]+$": {}
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
# Stock mediamtx image has no shell, so an in-container HTTP probe of the
|
||||||
|
# API isn't meaningfully cheaper than TCP; RTMP liveness is enough (same
|
||||||
|
# polling-not-hooks rationale as podsteadr's own status poller, which
|
||||||
|
# exists precisely because runOn*-style shell hooks aren't available on
|
||||||
|
# this image).
|
||||||
|
type: tcp
|
||||||
|
endpoint: localhost:1935
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
@@ -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."
|
||||||
@@ -6,7 +6,41 @@
|
|||||||
//! no listener, so allowing them is inert.
|
//! no listener, so allowing them is inert.
|
||||||
|
|
||||||
pub const APP_LAUNCH_PORTS: &[u16] = &[
|
pub const APP_LAUNCH_PORTS: &[u16] = &[
|
||||||
2283, 2342, 3000, 3001, 3002, 4080, 5180, 7778, 8080, 8081, 8082, 8083, 8084, 8085, 8087, 8088,
|
2283,
|
||||||
8089, 8090, 8096, 8123, 8175, 8176, 8240, 8334, 8336, 8888, 8999, 9000, 9100, 10380, 11434,
|
2342,
|
||||||
18081, 18083, 23000, 32838, 50002,
|
3000,
|
||||||
|
3001,
|
||||||
|
3002,
|
||||||
|
4080,
|
||||||
|
5180,
|
||||||
|
7778,
|
||||||
|
8080,
|
||||||
|
8081,
|
||||||
|
8082,
|
||||||
|
8083,
|
||||||
|
8084,
|
||||||
|
8085,
|
||||||
|
8087,
|
||||||
|
8088,
|
||||||
|
8089,
|
||||||
|
8090,
|
||||||
|
8095,
|
||||||
|
8096,
|
||||||
|
8123,
|
||||||
|
8175,
|
||||||
|
8176,
|
||||||
|
8240,
|
||||||
|
8334,
|
||||||
|
8336,
|
||||||
|
8888,
|
||||||
|
8999,
|
||||||
|
9000,
|
||||||
|
9100,
|
||||||
|
10380,
|
||||||
|
11434,
|
||||||
|
18081,
|
||||||
|
18083,
|
||||||
|
23000,
|
||||||
|
32838,
|
||||||
|
50002,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1717,17 +1717,25 @@ app:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
exempt.sort();
|
exempt.sort();
|
||||||
// 25 as of the v1.7.123 port-policy round: bitcoin p2p (8333 ×2),
|
// 31 as of the podsteadr app-package round: the prior 25 (bitcoin p2p
|
||||||
// core-lightning 9736/9835, electrumx 50001, fedimint 8173/8174,
|
// (8333 ×2), core-lightning 9736/9835, electrumx 50001, fedimint
|
||||||
// fedimint-gateway 8176/9737, gitea ssh 2222, lightning-stack
|
// 8173/8174, fedimint-gateway 8176/9737, gitea ssh 2222,
|
||||||
// 8091/9738/10010, lnd 9735/10009/18080, netbird 3478/8086/8087,
|
// lightning-stack 8091/9738/10010, lnd 9735/10009/18080, netbird
|
||||||
// pine TLS 10381 + the three voice ports (10200/10300/10400 — the
|
// 3478/8086/8087, pine TLS 10381 + the three voice ports
|
||||||
// disclosed known gap), router SSDP/mDNS 1900/5353. Every one is a
|
// (10200/10300/10400 — the disclosed known gap), router SSDP/mDNS
|
||||||
// deliberate, rationale-carrying exemption; the release-gate test
|
// 1900/5353) plus 6 new ones: podsteadr 8095 (web UI/API/RSS —
|
||||||
// stage timed out that cycle, so the count here lagged at 17.
|
// third-party podcast clients and other podsteadr instances must
|
||||||
|
// fetch feeds/marketplace data with no node session; the app gates
|
||||||
|
// its own sensitive routes with NIP-98), podsteadr-blossom 8098
|
||||||
|
// (public blob reads for RSS enclosures; uploads are BUD-02
|
||||||
|
// signed-auth gated by blossom itself), podsteadr-mediamtx
|
||||||
|
// 1935/8189/8889/8890 (RTMP/ICE/WHIP ingest + HLS playback — none of
|
||||||
|
// these are HTTP-session-shaped, and publish is protocol-gated by a
|
||||||
|
// per-stream secret checked via podsteadr's own auth webhook). Every
|
||||||
|
// one is a deliberate, rationale-carrying exemption.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
exempt.len(),
|
exempt.len(),
|
||||||
25,
|
31,
|
||||||
"unauthenticated port set changed — review before updating this count: {exempt:?}"
|
"unauthenticated port set changed — review before updating this count: {exempt:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const GENERATED_APP_PORTS: Record<string, number> = {
|
|||||||
"nostr-rs-relay": 18081,
|
"nostr-rs-relay": 18081,
|
||||||
"photoprism": 2342,
|
"photoprism": 2342,
|
||||||
"pine": 10380,
|
"pine": 10380,
|
||||||
|
"podsteadr": 8095,
|
||||||
"portainer": 9000,
|
"portainer": 9000,
|
||||||
"router": 8084,
|
"router": 8084,
|
||||||
"searxng": 8888,
|
"searxng": 8888,
|
||||||
@@ -87,6 +88,9 @@ export const GENERATED_APP_TITLES: Record<string, string> = {
|
|||||||
"pine-openwakeword": "Pine Wake Word (openWakeWord)",
|
"pine-openwakeword": "Pine Wake Word (openWakeWord)",
|
||||||
"pine-piper": "Pine Piper (TTS)",
|
"pine-piper": "Pine Piper (TTS)",
|
||||||
"pine-whisper": "Pine Whisper (STT)",
|
"pine-whisper": "Pine Whisper (STT)",
|
||||||
|
"podsteadr": "podsteadr",
|
||||||
|
"podsteadr-blossom": "podsteadr Blossom",
|
||||||
|
"podsteadr-mediamtx": "podsteadr MediaMTX",
|
||||||
"portainer": "Portainer",
|
"portainer": "Portainer",
|
||||||
"router": "Mesh Router",
|
"router": "Mesh Router",
|
||||||
"searxng": "SearXNG",
|
"searxng": "SearXNG",
|
||||||
|
|||||||
Reference in New Issue
Block a user