A private, self-hosted web app for your guitar chord sheets.
- Transpose to any key, saved per song, with every chord diagram following
- Chord diagrams for every chord in the sheet — tap one for its fingerings from a database of 2,000+ guitar voicings, draw your own when none fits, and pin the shape this song actually uses
- Metronome with a step grid you can edit by hand — kick, snare, hats, toms, rim, clap, ride, cowbell and shaker — time-signature presets from 2/4 to 12/8 shuffle, grooves from half-time to bossa nova and swing, tempo saved per song, and a playhead showing where you are in the bar
- Play mode — hands-free auto-scroll at a speed you set per song, the screen kept awake, and a fullscreen stage mode with keyboard shortcuts
- Installable on a phone or desktop as a PWA, with a dark mode that suits a dim stage
- Fuzzy search, tags, favourites, capo and BPM, YouTube tutorial embeds, Markdown export, print
- Sign in by emailed link or six-digit code — no passwords to manage
One container, one file on disk, no external services.
- Next.js
- SQLite via better-sqlite3
- Tailwind CSS + shadcn/ui on Radix primitives, with Lucide icons and next-themes
- Better Auth
- chords-db + react-chords
- Fuse.js
You need Docker and an .env file:
cp .env.example .env
# put a secret in it
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"Then:
mkdir -p data
docker compose up -dIt listens on port 3000. The database is a single SQLite file at data/chordy.db.
The data directory must be writable by the container, which runs as uid 1000. If the log
says SQLITE_CANTOPEN, that is what happened:
sudo chown -R 1000:1000 datadocker run -d --name chordy -p 3000:3000 \
-v "$PWD/data:/app/data" \
--env-file .env \
ghcr.io/meceware/chordy:latest| Variable | Needed | Notes |
|---|---|---|
AUTH_SECRET |
yes | Any long random string. Changing it signs everyone out. |
SITE_URL |
yes | Public URL, no trailing slash. Sign-in links are built from it. |
DATABASE_PATH |
no | Defaults to /app/data/chordy.db in the container. |
EMAIL_SERVER_HOST |
no | SMTP host. Without it, sign-in emails are printed to the container log — fine for a single user, since docker logs chordy shows the code. |
EMAIL_SERVER_PORT |
no | 587 by default. Use 465 for implicit TLS. |
EMAIL_SERVER_USER, EMAIL_SERVER_PASSWORD |
no | Leave empty for a relay that authenticates by IP. |
EMAIL_FROM |
no | Must be an address your SMTP server may send as. |
Anyone who can reach the login page can create an account, so put it behind something — a VPN, or Cloudflare Access, or a reverse proxy with auth — if it is on the open internet.
docker build -t chordy .
docker run -d --name chordy -p 3000:3000 -v "$PWD/data:/app/data" --env-file .env chordyMulti-arch images for amd64 and arm64 are published to ghcr.io on every push to main and on
every v* tag.
npm ci
cp .env.example .env # add AUTH_SECRET
npm run migrate
npm run seed # optional: six demo songs
npm run devSign-in emails go to the terminal. Node 24 — nvm use picks it up from .nvmrc.
Use npm run backup rather than copying chordy.db. From the host, against a running container:
docker exec chordy node scripts/backup.jsA backup is not a backup until you have restored it somewhere and opened the app against it.
