No description
  • TypeScript 57.2%
  • Go 27%
  • Shell 7.8%
  • Dockerfile 7.5%
  • JavaScript 0.5%
Find a file
2026-05-03 17:14:42 +02:00
scripts_and_tools do exclude DS_Store 2026-05-03 17:14:42 +02:00
upload-form 🚧 2026-04-27 13:57:05 +02:00
upload-handler 🚧 2026-04-27 13:57:05 +02:00
.gitignore fairly solid stuff, ready for merge soon 2026-04-27 16:27:04 +02:00
Dockerfile fairly solid stuff, ready for merge soon 2026-04-27 16:27:04 +02:00
Dockerfile.vanilla introduce and explain vanilla, for those who care 2026-04-27 17:07:03 +02:00
entrypoint.sh WIP /upload 2026-04-27 08:53:54 +02:00
fly.toml WIP /upload 2026-04-27 08:53:54 +02:00
fly.vanilla.toml introduce and explain vanilla, for those who care 2026-04-27 17:07:03 +02:00
navidrome.toml WIP /upload 2026-04-27 08:53:54 +02:00
nginx.conf 🚧 2026-04-27 13:57:05 +02:00
README.md introduce and explain vanilla, for those who care 2026-04-27 17:07:03 +02:00
supervisord.conf WIP /upload 2026-04-27 08:53:54 +02:00
UPLOAD_PLAN.md 🚧 2026-04-27 09:42:31 +02:00

navidrome stack on Fly.io

Personal music streaming server running Navidrome on Fly.io.

Two flavors: This repo defaults to the full stack (Dockerfile + fly.toml). For vanilla Navidrome-only, use Dockerfile.vanilla + fly.vanilla.toml instead (tested at v1.0.0 tag — deluan/navidrome:latest, no nginx, no upload UI).

Architecture

flowchart LR
    subgraph Fly["Fly Proxy (SSL termination)"]
        direction TB
        proxy["*:80 → :443"]
    end

    subgraph Container["Container (single Fly Machine)"]
        direction TB
        nginx["nginx :8080"]

        subgraph Services[" "]
            direction LR
            nginx --> navidrome["navidrome :4533"]
            nginx --> upload_handler["upload-handler :3000 (Go)"]
            nginx -.-> static["static files<br>/var/www/upload<br>(Next.js build)"]
        end
    end

    subgraph Volume["Fly Volume /data"]
        direction TB
        db["navidrome.db"]
        music["music/<br>Library 1 (manual)"]
        uploads["music/uploads/<br>Library 2 (uploaded)"]
    end

    proxy --> nginx
    static -.-> nginx
    navidrome --> db
    navidrome --> music
    navidrome --> uploads
    upload_handler --> uploads

Services managed by supervisord:

  • nginx — reverse proxy, routes traffic (port 8080)
  • navidrome — music server (port 4533)
  • upload-handler — Go binary receiving uploads (port 3000)

Setup

App and volume were created manually before first deploy:

fly apps create juliannymark-navidrome --org personal
fly volumes create navidrome_data --app juliannymark-navidrome --region ams --size 1
fly deploy

Volume

A single 1GB volume is mounted at /data. Both the Navidrome database and the music library live here:

graph TD
    volume["/data (Fly Volume)"]

    volume --> navidrome_db["navidrome.db"]
    volume --> navidrome_toml["navidrome.toml"]
    volume --> transcoding["transcoding/"]
    volume --> music["music/<br>Library 1: manually curated"]
    volume --> uploads["music/uploads/<br>Library 2: uploaded (7-day staging)"]

    music --- m1["ALBUM_1/"]
    music --- m2["ALBUM_2/"]
    uploads --- u1["UUID_xxx/"]

    style music stroke:#90EE90
    style uploads stroke:#FFE4B5
Path Contents
/data Navidrome database & cache
/data/music Manually curated music library
/data/music/uploads Uploaded music (7-day staging)

Library Separation

Navidrome is configured with two libraries:

  • Library 1/data/music — manually curated, permanent
  • Library 2/data/music/uploads — uploaded via web UI, auto-deleted after 7 days if not processed

The volume auto-extends by 1GB whenever usage hits 80%, up to a maximum of 100GB (configured via auto_extend_* in fly.toml). You can also extend manually:

fly volumes list --app juliannymark-navidrome
fly volumes extend <volume-id> -s <new-size-gb> --app juliannymark-navidrome

Fly takes daily snapshots retained for 5 days. Check them with:

fly volumes snapshots list <volume-id> --app juliannymark-navidrome

Uploading music

Web UI

Open https://juliannymark-navidrome.fly.dev/upload/ in your browser. You'll be prompted for the same credentials as Navidrome itself.

The upload UI:

  • Accepts FLAC, MP3, M4A, OGG, WAV, AIFF, ZIP
  • Shows current storage usage and limits
  • Max file size: 1GB
  • Files are grouped together if uploaded simultaneously (optional)

Important: Uploaded files land in /data/music/uploads (staging area) and are auto-deleted after 7 days if not processed. After uploading, poke the Navidrome instance maintainer to process/organize your files into the main library.

Quick single file or folder (no proxy needed)

fly sftp put --app juliannymark-navidrome -R ~/Music/Radiohead /data/music/Radiohead

SSH/rsync (for large transfers or power users)

All SSH approaches go through an fly proxy tunnel.

./scripts_and_tools/fly-connect.sh

This script checks if your SSH keys are older than 20 hours and re-issues them automatically before starting the fly proxy.

Manual SSH/rsync setup (alternatives)

Step 1 — issue SSH credentials (once per 24h)

mkdir -p ~/.fly/ssh
fly ssh issue personal ~/.fly/ssh/navidrome --overwrite

This writes a private key and certificate to ~/.fly/ssh/navidrome (valid 24 hours). Re-run it when it expires.

Step 2 — open the tunnel

# keep this running in a dedicated terminal
fly proxy 2222:22 --app juliannymark-navidrome

SSH config alias

To make things easier (and the upload scripts kinda expect it) you can add this to ~/.ssh/config once — it keeps the key and port wiring in one place so all tools below just use fly-navidrome:

Host fly-navidrome
User                  root
Hostname              127.0.0.1
Port                  2222
IdentityFile          ~/.fly/ssh/navidrome
IdentitiesOnly        yes
StrictHostKeyChecking no
UserKnownHostsFile    /dev/null

rsync helper scripts

./scripts_and_tools/rsync-to-cloud.sh   # upload: ~/Music/navidrome/ → /data/music/
./scripts_and_tools/rsync-from-cloud.sh # download: /data/music/ → ~/Music/navidrome/

Only transfers new or changed files, so re-running after adding more music is fast.

Upload processing

Uploaded files sit in /data/music/uploads for 7 days before automatic deletion. To make them permanent:

  1. Navidrome picks up new files on its next scan (automatic, or trigger from Settings → Library in web UI)
  2. The instance maintainer needs to move/copy files from the Uploads library into the main library
  3. Once organized, files survive the 7-day cleanup

Upload flow

sequenceDiagram
    participant Friend
    participant nginx
    participant upload_handler as upload-handler
    participant navidrome

    Friend->>nginx: GET /upload/
    nginx-->>Friend: upload form HTML

    Friend->>nginx: POST /upload-api (multipart)
    nginx->>upload_handler: POST /upload-api
    upload_handler->>uploads: save file to /data/music/uploads/groupID/

    Note over upload_handler: 7-day cleanup runs on every boot

    loop every scan
        navidrome->>uploads: detect new files
        navidrome->>navidrome: appear in Uploads library
    end

Streaming

sequenceDiagram
    participant User
    participant nginx
    participant navidrome

    User->>nginx: GET /
    nginx->>navidrome: GET /
    navidrome-->>nginx: web UI
    nginx-->>User: Navidrome UI

    User->>nginx: GET /rest/ (Subsonic API)
    nginx->>navidrome: GET /rest/
    navidrome-->>nginx: JSON response
    nginx-->>User: Subsonic response

Deploy

fly deploy

Logs

fly logs --app juliannymark-navidrome

Service-specific logs (inside the container):

# nginx
fly ssh issue personal ~/.fly/ssh/navidrome --app juliannymark-navidrome
# then: cat /var/log/nginx/stdout.log

# navidrome
fly ssh console --app juliannymark-navidrome
# then: cat /var/log/navidrome/stdout.log

# upload-handler
fly ssh console --app juliannymark-navidrome
# then: cat /var/log/upload-handler/stdout.log

Auto-stop

The machine stops when idle and starts automatically on the next request. To disable this (always-on), set auto_stop_machines = "off" in fly.toml and redeploy.

Development

To run the upload-form locally:

cd upload-form
pnpm install
NEXT_PUBLIC_API_BASE=http://localhost:3000 pnpm dev

Use docker-compose.dev.yml in scripts_and_tools/ for local testing of the full stack.