Note: While MediaRSS is primarily designed and tested for audio files (MP3, M4A, M4B, etc.), video files should also work. However, video support is not the primary focus and may have limitations (especially with regard to performance) with certain formats.
Node.js 24 (engines.node is >=24.12.0), npm, Remix 3 (remix@3.0.0,
remix/component, no React). Format with oxfmt, lint with oxlint. How to
work in the repo, including npm run validate, the data layer, and
npx remix db, is in docs/contributing.md. Installed
Remix docs live at node_modules/remix/INDEX.md.
The admin media pages list files from every MEDIA_PATHS root, with search,
sort, feed assignment, and per-file analytics. Uploading a file or writing tags
(the media editor or MCP update_media_metadata) rewrites the file on disk and
needs a read/write media mount. The Docker image includes ffmpeg for those
writes. Mount media read-only (:ro) if you only serve existing files.
The admin dashboard shows feed and media analytics:
- Feed analytics (per feed + per token):
- RSS fetches
- media requests
- download starts
- bytes served
- unique clients
- top media items
- top clients
- daily activity trends
- Media analytics (aggregated across all feed tokens):
- requests/download starts
- bytes served
- unique clients
- per-feed/per-token breakdowns
- top clients
- daily activity trends
- Time windows: switch between 7d / 30d / 90d directly in the UI
MediaRSS exposes a Model Context Protocol server at /mcp (Streamable HTTP
2026-07-28). Clients authenticate with OAuth (PKCE). Discovery is public:
/.well-known/oauth-authorization-server/.well-known/oauth-protected-resource/mcp
Consent is /admin/authorize, behind Cloudflare Access with the rest of admin.
Scopes are mcp:read and mcp:write. After you approve a client, it can
connect to /mcp with the issued token.
RSS subscribe URLs from MCP tools are /feed/{token}. The player widget is
/mcp/widget/{token}/... (same feed token). Agent conventions for the server
live in docs/mcp-server-best-practices.md.
- Docker
- A server or machine to host the application
- Basic understanding of Docker volumes for data persistence
- Media files organized in one or more directories on your host machine
- (Recommended) A domain name and Cloudflare account for secure remote access (see Securing the Admin Dashboard)
If you're running this on a Synology NAS, follow these specific instructions:
-
Install Docker from the Synology Package Center if you haven't already.
-
Create a shared folder for the database files:
- Open Control Panel → Shared Folder
- Create a new shared folder named
docker(or your preferred name) - Inside this folder, create a directory called
mediarss
-
Note your media locations:
- Synology typically stores media files in
/volume1/[shared-folder-name] - You can mount any number of media directories from your Synology
- Synology typically stores media files in
-
Open Docker in Synology DSM:
- Go to "Registry" and search for
kentcdodds/mediarss - Download the image
- Go to "Container" and launch using the image
- Go to "Registry" and search for
-
When setting up the container in the Synology Docker UI:
-
In the "Advanced Settings" → "Volume" tab:
- Add a volume mount for the database:
- Mount path:
/data - Local path:
/volume1/docker/mediarss
- Mount path:
- Add a volume mount for artwork (to persist uploaded feed images):
- Mount path:
/app/data/artwork - Local path:
/volume1/docker/mediarss/artwork
- Mount path:
- Add your media volume mounts (add as many as you need):
- Mount path:
/media/[your-name](e.g.,/media/shows,/media/personal, etc.) - Local path:
/volume1/[your-folder] - Check "Read-only" if you only need to serve existing files
- Uncheck "Read-only" to upload files or edit metadata (see Media library)
- Mount path:
- Add a volume mount for the database:
-
In the "Port Settings" tab:
- Local Port: 22050 (or your preferred port)
- Container Port: 22050 (or whatever you set PORT to in the env settings)
-
In the "Environment" tab:
- Add the standard environment variables as needed
- For multiple media paths, specify them in MEDIA_PATHS:
- Variable: MEDIA_PATHS
- Value:
shows:/media/shows,personal:/media/personal - Format:
name:path,name:path(comma-separated, each with a name and path)
-
The rest of the standard instructions apply for managing the container.
For the easiest setup, use the provided NAS setup script:
- Copy
scripts/nas-setup.shto your NAS - Edit the configuration section at the top of the script
- Run it:
chmod +x nas-setup.sh
./nas-setup.shThe script will pull the latest image, set up the container with your media directories, and configure automatic restarts.
- Pull the Docker image:
docker pull kentcdodds/mediarss- Create directories for persistent storage and ensure your media directories exist:
# Create directories for database and artwork storage
mkdir -p /path/to/your/data
mkdir -p /path/to/your/data/artwork
# Your media directories should already exist- Run the container:
docker run -d \
--name mediarss \
-p 22050:22050 \
-v /path/to/your/data:/data \
-v /path/to/your/data/artwork:/app/data/artwork \
-v /path/to/audiobooks:/media/audiobooks:ro \
-v /path/to/audio-series:/media/audio-series:ro \
-e MEDIA_PATHS=audiobooks:/media/audiobooks,audio-series:/media/audio-series \
kentcdodds/mediarssMedia volume permissions (read-only vs upload and metadata edits) are under Volume Mounts.
The application requires these volume mounts:
-
Database Volume (
/data):- Purpose: Stores SQLite databases
- Mount point:
/data - Example:
-v /path/to/your/data:/data
-
Artwork Volume (
/app/data/artwork):- Purpose: Stores uploaded feed artwork images
- Mount point:
/app/data/artwork - Example:
-v /path/to/your/data/artwork:/app/data/artwork - Important: Without this mount, uploaded artwork will be lost on container restart
-
Media Volumes (any number allowed):
- Purpose: Access to your audio files
- Mount point pattern:
/media/[your-name] - Examples:
-v /path/to/audiobooks:/media/audiobooks:ro-v /path/to/audio-series:/media/audio-series:ro
- Mount read-only (
:ro) if you only need to serve existing files - Mount read/write (no
:ro) to upload files or edit metadata; see Media library - Name the mount points anything that makes sense for your use case
You can organize your media directories however you prefer. Here's an example structure for audiobooks and audio series:
/media/audiobooks/
├── fiction/
│ ├── Harry Potter Series/
│ └── Lord of the Rings/
└── non-fiction/
├── Atomic Habits/
└── Deep Work/
/media/audio-series/
├── podcasts/
│ ├── show1/
│ └── show2/
└── courses/
├── course1/
└── course2/
The following environment variables can be configured:
PORT: Server port (default: 22050)NODE_ENV: Environment mode (production,development, ortest)DATABASE_PATH: Path to the main SQLite database (default: ./data/sqlite.db)CACHE_DATABASE_PATH: Path to the cache SQLite database (default: ./data/cache.db)MEDIA_PATHS: Named media roots in formatname:path,name:path- Example:
shows:/media/shows,personal:/media/personal,other:/media/other - Each media root has a name (used in URLs) and a path (filesystem location)
- Names must be unique - duplicate names are not allowed
- Names should be URL-safe (alphanumeric, hyphens, underscores)
- Example:
ANALYTICS_RETENTION_DAYS: Number of days to retain feed analytics events (default:180)- Older analytics events are pruned automatically on startup
RATE_LIMIT_ADMIN_READ: Admin GET/HEAD/OPTIONS requests per minute per IP (default:5000)RATE_LIMIT_ADMIN_WRITE: Admin write requests per minute per IP (default:30)RATE_LIMIT_MEDIA:/media/*enclosure requests and admin artwork / stream reads (/admin/api/artwork,/admin/api/media-stream, feed artwork GET) per minute per IP (default:300). Public/art/*usesRATE_LIMIT_DEFAULT.RATE_LIMIT_DEFAULT: Other requests (feeds,/art/*, MCP, OAuth, …) per minute per IP (default:1000)GITHUB_REPO: Repository URL used when linking to commits (default:https://github.com/kentcdodds/mediarss)COMMIT_SHA: Git SHA reported by/healthand/admin/version. The published image sets this at build time (SOURCE_COMMITis an accepted alias).ALLOWED_HOSTS: Optional comma-separated hosts allowed on/oauth/token. Unset means anyHostheader is accepted.MEDIA_ANALYTICS_MAX_SQLITE_VARIABLE_NUMBER: Optional cap on SQLite IN-clause bind variables when aggregating per-file analytics. Unset uses the compiled SQLiteMAX_VARIABLE_NUMBER(or999).
The Docker image sets DATABASE_PATH=/data/sqlite.db and
CACHE_DATABASE_PATH=/data/cache.db. The ./data/… defaults above apply to
local development.
MediaRSS routes fall into five groups:
- Public token routes (
/feed/*,/media/*,/art/*,/mcp/widget/*) — podcast apps and the MCP player widget. Access is the secret token in the URL. - Admin routes (
/admin,/admin/api/*) — the dashboard./redirects here./admin/healthis the same snapshot as/health. Protect these at the edge. - MCP (
/mcp) — OAuth bearer token for the Streamable HTTP server. Leave/mcpoff the Access policy so clients can connect after you approve them. - OAuth discovery and tokens (
/.well-known/*,/oauth/*) — public so clients can register and redeem codes. Consent itself is/admin/authorize. - Health (
/health) — public JSON snapshot of process, database, media roots, MCP, and OAuth. Not rate-limited unless?probe=is present (?probe=cimdlive-fetches Kody's client metadata).
Podcast apps need unauthenticated token URLs, so use Cloudflare Tunnel and
an Access policy on /admin rather than app-level login.
- Free HTTPS - Automatic SSL certificates, no configuration needed
- No port forwarding - Outbound-only connection, your router stays locked down
- Hidden IP - Your home IP address is never exposed
- Access policies - Add authentication at the edge (email OTP, SSO, etc.)
- Works with any DNS - You don't need to move your domain to Cloudflare
-
Create a free Cloudflare account and add your domain (you can use the free plan).
-
Create a tunnel in the Zero Trust dashboard:
- Go to Networks → Tunnels → Create a tunnel
- Name it (e.g., "mediarss")
- Choose Docker as the connector
- Copy the tunnel token
-
Run cloudflared alongside MediaRSS on your NAS/server:
docker run -d \ --name cloudflared \ --restart unless-stopped \ --network host \ cloudflare/cloudflared:latest \ tunnel run --token YOUR_TUNNEL_TOKEN
Note: The
--network hostflag is required so that cloudflared can reach MediaRSS vialocalhost. Without it,localhostwould refer to the cloudflared container itself, not your NAS. -
Configure the public hostname in the tunnel settings:
- Hostname:
media.yourdomain.com(a domain name you control) - Service:
http://localhost:22050(or your MediaRSS port)
- Hostname:
-
Add an Access policy to protect the admin dashboard:
- Go to Access → Applications → Add an application
- Choose "Self-hosted"
- Set the domain to
media.yourdomain.com - Set the path to
/admin(just the admin pages) - Add an authentication method (email OTP is easiest)
- Important: Don't protect
/feed/*,/media/*,/art/*,/mcp,/.well-known/*,/oauth/*, or/health
If your domain's DNS is managed elsewhere (not Cloudflare), you can still use Cloudflare Tunnel with a CNAME record:
- Complete steps 1-4 above
- Find your tunnel's hostname in the Cloudflare dashboard - it looks like:
a1b2c3d4-e5f6-7890-abcd-ef1234567890.cfargotunnel.com - At your DNS provider, create a CNAME record:
media.yourdomain.com CNAME <tunnel-uuid>.cfargotunnel.com
Note: This only works for subdomains (e.g., media.yourdomain.com), not the
root domain.
If you only access MediaRSS from your local network and don't need remote access, you can skip Cloudflare Tunnel entirely. The token-based URLs for feeds provide sufficient security for trusted networks.
Persistent files live on the /data and /app/data/artwork mounts (see
Volume Mounts):
/data/sqlite.db— main database/data/cache.db— media metadata cache (safe to delete; it rebuilds)/app/data/artwork— uploaded feed cover images
Schema migrations run at container start, before the app serves requests. Take a backup before upgrading. See the rollback runbook when a migration has to be reversed.
To backup your data, simply copy the files from your mounted data directory. For example:
# Stop the container before backup
docker stop mediarss
# Backup the databases and artwork
cp /path/to/your/data/sqlite.db /path/to/backup/sqlite.db
cp /path/to/your/data/cache.db /path/to/backup/cache.db
cp -r /path/to/your/data/artwork /path/to/backup/artwork
# Restart the container
docker start mediarssTo restore from backup:
# Stop the container
docker stop mediarss
# Restore the databases and artwork
cp /path/to/backup/sqlite.db /path/to/your/data/sqlite.db
cp /path/to/backup/cache.db /path/to/your/data/cache.db
cp -r /path/to/backup/artwork /path/to/your/data/artwork
# Restart the container
docker start mediarssTo upgrade to a new version:
# Pull the new image
docker pull kentcdodds/mediarss
# Stop the current container
docker stop mediarss
# Remove the old container
docker rm mediarss
# Run the new container (using the same data directory and media mount)
# Note: Remove :ro from media volumes to upload files or edit metadata
docker run -d \
--name mediarss \
-p 22050:22050 \
-v /path/to/your/data:/data \
-v /path/to/your/data/artwork:/app/data/artwork \
-v /path/to/audiobooks:/media/audiobooks:ro \
-v /path/to/audio-series:/media/audio-series:ro \
-e MEDIA_PATHS=audiobooks:/media/audiobooks,audio-series:/media/audio-series \
kentcdodds/mediarssYour data will be preserved as long as you use the same volume mount points.
Audiobookshelf is a fully featured audio media server. It doesn't support private RSS feeds and it's a little too much for what I want, so I built my own thing. But you might like it.





