HarborFM
Open source podcast creation tool designed as a modern replacement for Anchor.fm. Build episodes from segments: record or upload clips, pull in intros and bumpers from a library, trim and reorder, then export a single audio file and RSS feed.
Install / Use
/learn @LoganRickert/HarborFMREADME
HarborFM

Open source podcast creation tool designed as a modern replacement for Anchor.fm. Build episodes from segments: record or upload clips, pull in intros and bumpers from a library, trim and reorder, then export a single audio file and RSS feed.
The app has PWA, so you can add it to your home screen and connect to your server.
License: MIT
Home Page: https://harborfm.com/
Source: https://github.com/LoganRickert/harborfm
Demo Site: https://app.harborfm.com/
Swagger API Docs: https://harborfm.com/server/
Overview on Noted.lol https://noted.lol/harborfm/
Discord https://discord.gg/hSmstBzAJV
Table of contents
- Overview
- Deploy with Terraform
- WebRTC (group calls)
- Requirements
- Quick start (local)
- Docker
- Environment variables
- Running without Docker
- Features
- Embed
- Tech stack
- Project structure
- Scripts
- Export
- Local Testing
- Troubleshooting
- Backup and upgrading
- Single Sign-On (SSO)
Overview
HarborFM lets you assemble podcast episodes from building blocks. Create a show, add episodes, and for each episode add segments: recorded clips (uploaded per episode) or reusable assets from your library (intros, outros, bumpers). Trim, split, remove silence, and reorder. The app concatenates segments with ffmpeg and produces the final episode audio. Generate RSS feeds and deploy to S3-compatible storage (e.g. Cloudflare R2) so listeners can subscribe. Optional: transcripts via Whisper ASR, LLM helpers (Ollama or OpenAI) for copy suggestions, and public feed pages for discovery.
Quick Start
The app expects two writable directories: /data (SQLite DB, uploads, processed audio, RSS, artwork, library) and /secrets (JWT and encryption keys). You do not need to mount /secrets if you pass the secrets in through environment variables.
HARBORFM_SECRETS_KEY=$(openssl rand -base64 32)
JWT_SECRET=$(openssl rand -base64 32)
docker run --name harborfm -p 3001:3001 \
-v harborfm-data:/data \
-e HARBORFM_SECRETS_KEY="$HARBORFM_SECRETS_KEY" \
-e JWT_SECRET="$JWT_SECRET" \
ghcr.io/loganrickert/harborfm:latest
Use nginx+letsencrypt to provide a secure connection.
If you are using http, you need to set COOKIE_SECURE=false as an environment variable.
Deploy with Terraform
Use Terraform to provision a VM (AWS EC2 or Vultr) that runs HarborFM via user-data (PM2 + nginx, with optional WebRTC and Let's Encrypt).
AWS (EC2)
-
Install Terraform – see infrastructure/terraform/QUICKSTART.md (macOS, Debian, CentOS).
-
Configure AWS – set
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY(or useaws configure). -
Apply from the AWS Terraform directory:
cd infrastructure/terraform/aws cp terraform.tfvars.example terraform.tfvars # Edit terraform.tfvars: deploy_type, ami_id (Debian 12 for your region), domain, admin_email, admin_password, etc. ./run.sh init ./run.sh apply -
Use the url output to open the app; if you set
admin_emailandadmin_password, the admin is created on first boot.
Vultr
-
Install Terraform – see infrastructure/terraform/QUICKSTART.md.
-
Set
VULTR_API_KEYin.env(copy frominfrastructure/terraform/.env.example). -
Apply from the Vultr directory:
cd infrastructure/terraform/vultr cp terraform.tfvars.example terraform.tfvars # Edit terraform.tfvars: deploy_type, region, os_id, plan, domain, etc. ./run.sh init ./run.sh apply
Getting Vultr OS IDs
List available OS images:
curl -s -H "Authorization: Bearer $VULTR_API_KEY" https://api.vultr.com/v2/os | jq '.os[] | {id, name}'
Common mappings: Debian 11 477, Debian 12 2136, Debian 13 2625; Ubuntu 22 1743, Ubuntu 24 2285, Ubuntu 25 2657; CentOS 9 542, CentOS 10 2467. Vultr derives the os variable from os_id via infrastructure/terraform/vultr/scripts/os-from-id.sh.
Getting AWS AMI IDs
Look up a Debian 12 AMI for your region (owner 136693071363 is Debian):
aws ec2 describe-images --region us-east-2 --owners 136693071363 \
--filters "Name=name,Values=debian-12-*" "Name=state,Values=available" \
--query "sort_by(Images, &CreationDate)[-1].ImageId" --output text
Change us-east-2 to your region. The Terraform os variable (e.g. debian-12) must match the image.
Full variable reference, optional persistent data volume (survives destroy+apply), and multi-environment (dev/prod) details: infrastructure/terraform/README.md.
WebRTC (group calls)
Group calls use a separate webrtc-service (mediasoup). The main app talks to it over HTTP; browsers connect via WebSocket.
Enabling:
- Set
WEBRTC_ENABLED=1(ortrue) on the main app. - Configure
WEBRTC_SERVICE_URL(internal, e.g.http://webrtc:3002) andWEBRTC_PUBLIC_WS_URL(public, e.g.wss://example.com/webrtc-ws). Nginx/Caddy proxy/webrtc-ws/to the webrtc service.
Docker Compose: WebRTC runs under profile webrtc. Start with:
docker compose --profile nginx --profile webrtc up -d
(or caddy instead of nginx). Required .env: WEBRTC_ENABLED, WEBRTC_SERVICE_URL, WEBRTC_PUBLIC_WS_URL, WEBRTC_SERVICE_SECRET, RECORDING_CALLBACK_SECRET, and MEDIASOUP_ANNOUNCED_IP (when behind NAT).
PM2 / bare metal: Use ecosystem.config.cjs; it starts both harborfm and webrtc. Ensure the firewall allows UDP RTC_MIN_PORT–RTC_MAX_PORT (webrtc-service default 40000–40200; Docker uses 41000–41100).
Debugging:
- No "Record" or group-call UI: check
WEBRTC_ENABLEDandWEBRTC_SERVICE_URL/WEBRTC_PUBLIC_WS_URL. - Can't connect / no audio: verify firewall UDP ports; behind NAT, set
MEDIASOUP_ANNOUNCED_IPto the server's public IP. - Logs:
docker compose logs webrtcorpm2 logs webrtc.
Docker Compose Quick Start (Curl)
To run the full stack on a fresh machine (app, nginx, Let's Encrypt, Whisper, Fail2Ban) without cloning the repo:
curl -fsSL https://raw.githubusercontent.com/loganrickert/harborfm/main/install.sh | bash
The script downloads the compose file and configs, prompts for domain and cert email (unless non-interactive), then starts the stack. When not using Let's Encrypt, you can optionally use a self-signed certificate for HTTPS (browsers will show a warning). This script assumes you have docker and docker compose installed.
To auto-renew Let's Encrypt certificates, add a cron job (run crontab -e and add a line like the following, adjusting the path to your install directory):
0 3 * * * cd /path/to/harborfm-docker && docker compose run --rm --entrypoint certbot certbot renew
If you use the install.sh script, an update.sh script will also be added to the install directory. Run this script to pull the latest docker-compose files and renew the nginx certificate. Always run docker compose (and docker compose restart) from the install directory so volume paths such as nginx sites-enabled use the correct path from .env.
Adding additional domains (nginx)
If you use nginx and want to serve the same HarborFM app on extra domains or subdomains (e.g. demo.harborfm.com, podcast.example.com), use the included script from your install directory:
./nginx-add-domain.sh <domain>
# Example:
./nginx-add-domain.sh demo.harborfm.com
Before running:
- Your
.envmust haveREVERSE_PROXY=nginx,CERTBOT_EMAILset, andINSTALL_DIRset to the install directory’s absolute path. - DNS for the new domain must already point to this server (A/AAAA to the same host as your main domain).
The script will: add an nginx config for the domain under sites-enabled, reload nginx, run Let’s Encrypt (certbot) to obtain a certificate for that domain, then switch the config to HTTPS and reload again. Your primary domain (the one in DOMAIN in .env) is already served by the main nginx config-do not add it with this script or you’ll get duplicate server name warnings. Certificate renewal (e.g. cron with docker compose run --rm --entrypoint certbot certbot renew) renews all certs, including ones added this way.
Guide and Screenshots

When creating a new instance, you will need to navigate to the correct setup link. The link will be written to the console and is unique to every instance.
For example,
Open this URL to initialize the server (runs once):
/setup?id=oFwK--nBt8YloIVABKA4nOmYy_Kbx7PS

The initial setup will create an admin account. You will need to provide the admin email, a password, and you can enable or disable account registration and public feeds from here.
After you've finished the setup, you can sign into your new account.

Once signed in, you will see the dashboard which has a list of podcast shows.

For each show, you can configure the information on the show page.

From there you can view and create episodes on the episodes page.

The app provides the ability to 'build'
