A self-hosted web application for cataloguing your personal vinyl, CD and cassette collection. PHP 8.1 · MySQL 8 · Bootstrap 5 · Vanilla JS — no frameworks, no dependencies to install.
- Full CRUD for albums, artists, genres and labels
- Cover art — automatic retrieval via MusicBrainz / Last.fm / Discogs, or manual upload
- Tracklist — automatic from MusicBrainz / Last.fm / Discogs or manual entry
- Audio player — upload MP3/FLAC files and play them directly in the browser via an HTML5 sticky player
- YouTube integration — automatically search and cache YouTube videos for tracks, with in-page preview via lightbox
- Playlists — create playlists, drag-and-drop reorder (SortableJS)
- Advanced search — by artist, title, format, genre, year, label
- Artist page — all albums linked to a single artist
- Dashboard — collection statistics (total by format, recent additions)
- Dark mode support
- Relocatable audio folder — store audio files outside the web root via the Settings page
- Bulk MP3 upload with automatic track matching (3-pass: track number + Levenshtein similarity)
- Docker and Docker Compose installed
- Ports
8080(or your chosenAPP_PORT) and3306available
git clone https://github.com/recycledesign9/grizzly-music-archive.git
cd grizzly-music-archiveCopy the example environment file:
cp .env.example .envOpen .env and configure the application URL and exposed port according to where you are running Grizzly Music Archive.
For a standard installation, you usually only need to change:
BASE_URL=http://localhost:8080
APP_PORT=8080If you access the app from multiple hostnames, through a reverse proxy, or both locally and publicly, also configure:
ALLOWED_HOSTS=localhost,localhost:8080,127.0.0.1,127.0.0.1:8080| Scenario | BASE_URL |
APP_PORT |
ALLOWED_HOSTS |
|---|---|---|---|
| Local machine (Mac / Windows / Linux) | http://localhost:8080 |
8080 |
localhost,localhost:8080,127.0.0.1,127.0.0.1:8080 |
| Home or LAN server (IP address) | http://192.168.1.x:8080 |
8080 |
192.168.1.x:8080 |
| Server with domain name | https://music.yourdomain.com |
8080 |
music.yourdomain.com |
| Reverse proxy + local access | https://music.yourdomain.com |
8080 |
music.yourdomain.com,192.168.1.x:8080,music.local:8080 |
BASE_URL: canonical public URL of the application, with no trailing slash. It is used as the fallback URL when the current request host is not explicitly allowed.
APP_PORT: port Docker exposes on your host machine. Change it if8080is already in use, for exampleAPP_PORT=8082.
ALLOWED_HOSTS: comma-separated list of hostnames or host:port combinations that are allowed to serve the app dynamically. This is useful when the same installation is reachable from more than one address, for example localhost, a LAN IP, a local hostname, or a reverse proxy domain.
Never edit
docker-compose.ymlfor local settings. Put your installation-specific values in.env.
Example — LAN server on port 9082:
BASE_URL=http://192.168.1.x:9082
APP_PORT=9082
ALLOWED_HOSTS=192.168.1.x:9082Example — local machine, default setup:
BASE_URL=http://localhost:8080
APP_PORT=8080
ALLOWED_HOSTS=localhost,localhost:8080,127.0.0.1,127.0.0.1:8080Example — reverse proxy with public domain and LAN access:
BASE_URL=https://music.yourdomain.com
APP_PORT=8080
ALLOWED_HOSTS=music.yourdomain.com,192.168.1.x:8080,grizzly.local:8080When Grizzly Music Archive is behind a reverse proxy such as Nginx Proxy Manager, SSL should normally terminate at the proxy. In that case, the app container can continue to run over HTTP internally.
⚠️ Access control notice Grizzly Music Archive is designed as a self-hosted application for personal use, meant to run inside your own network or private infrastructure. The application itself has no built-in authentication: anyone who can reach the URL has full read/write access to the archive (adding, editing and deleting albums, uploading files, and consuming external API quota).The reverse proxy scenarios above refer to environment configuration and controlled remote access (e.g. VPN, LAN hostnames, or a proxy restricted at the network level) — not to exposing the app as an open public service.
If your setup makes the app reachable from the internet, protect it at the proxy or network level, for example with Basic Authentication / Access Lists in Nginx Proxy Manager, an IP allow-list, or a VPN (WireGuard, Tailscale). Do not expose an unprotected instance on a public domain.
docker compose up -dDocker will:
- Build the PHP + Apache image
- Start MySQL and wait until it is healthy
- Automatically import the schema (
docker/db/01_schema.sql) and demo data (docker/db/02_seed.sql) - Serve the app at the address configured in
BASE_URL
First startup takes ~30–60 s while MySQL initialises. The app container waits for the database health check before starting.
Open your browser at the BASE_URL you set in .env, for example:
http://localhost:8080
The archive starts pre-loaded with 12 demo albums (Beatles, Pink Floyd, Radiohead, Nirvana…).
To start completely empty, comment out the seed line in docker-compose.yml:
# - ./docker/db/02_seed.sql:/docker-entrypoint-initdb.d/02_seed.sql:roThen run:
docker compose down -v
docker compose up -dThis deletes the existing database volume and rebuilds the database from scratch.
docker compose down # stop (data is preserved in volumes)
docker compose down -v # stop AND delete all data (full reset)
docker compose up -d # start againNo API keys are required. The app works fully out of the box thanks to MusicBrainz, a free and open music database that provides automatic cover art and tracklist retrieval with no registration or key needed.
Simply add an album, click Recupera automaticamente and Grizzly will fetch cover and tracklist from MusicBrainz automatically.
Last.fm and Discogs are used as additional fallback sources when MusicBrainz does not find a match. YouTube integration requires a key to enable track search and in-page preview.
| Feature | Service | Variable | Notes |
|---|---|---|---|
| Cover art + tracklist | MusicBrainz | — | ✅ No key required — works out of the box |
| Cover art + tracklist | Last.fm | LASTFM_API_KEY |
Optional fallback — get key |
| Cover art | Discogs | DISCOGS_TOKEN |
Optional fallback — get token |
| YouTube integration | YouTube Data API v3 | YOUTUBE_API_KEY |
Required for YouTube preview — get key |
To enable optional services, add keys to your .env file:
LASTFM_API_KEY=your_key_here
DISCOGS_TOKEN=your_token_here
YOUTUBE_API_KEY=your_key_hereThen restart the app container:
docker compose restart appIf you run Grizzly Music Archive on a home server accessed via a raw private IP address, for example:
http://192.168.1.x:8082
YouTube may refuse to embed certain videos and show a "Video unavailable" message. This can happen because some embeds do not behave reliably with raw private IP addresses as origins.
A practical fix is to map a local hostname to your server IP on every device that will use the app.
On macOS / Linux:
sudo nano /etc/hostsAdd a line like this:
192.168.1.x grizzly.local
On Windows:
Open Notepad as Administrator, then open:
C:\Windows\System32\drivers\etc\hosts
Add:
192.168.1.x grizzly.local
Then configure .env on the server:
BASE_URL=http://grizzly.local:8082
APP_PORT=8082
ALLOWED_HOSTS=grizzly.local,grizzly.local:8082,192.168.1.x:8082Restart:
docker compose down
docker compose up -dYou can use any local hostname you like, for example grizzly.local, grizzly.archive, or archive.home. The hostname must resolve to the server IP on every client that uses the app, and it should be included in ALLOWED_HOSTS.
Requirements: PHP 8.1+, MySQL 8.0 or MariaDB 10.6+, Apache with mod_rewrite.
# 1. Import the database
mysql -u root -p -e "CREATE DATABASE grizzly_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p grizzly_db < docker/db/01_schema.sql
mysql -u root -p grizzly_db < docker/db/02_seed.sql
# 2. Configure environment
cp .env.example .env
# Edit .env:
# DB_HOST=localhost
# DB_PORT=3306 (or 8889 for MAMP)
# BASE_URL=http://localhost:8888/grizzly-music-archive (adjust to your setup)
# ALLOWED_HOSTS=localhost,localhost:8888
# 3. Set Apache DocumentRoot to the project root directory
# 4. Enable mod_rewrite and AllowOverride AllFor MAMP users: set DB_PORT=8889 and adjust BASE_URL to match your MAMP virtual host.
grizzly-music-archive/
├── docker/
│ ├── apache/
│ │ └── vhost.conf < Apache virtual host configuration
│ └── db/
│ ├── 01_schema.sql < Database structure
│ └── 02_seed.sql < Demo data (safe to publish)
├── public/
│ ├── uploads/
│ │ ├── covers/ < Cover images (gitignored)
│ │ └── audio/ < Audio files (gitignored)
│ ├── css/
│ ├── js/
│ └── img/
├── config/
│ ├── config.php < Env-driven configuration
│ └── database.php < PDO singleton
├── app/
│ ├── controllers/ < AlbumController, ArtistController, …
│ ├── models/ < Album, Artist, Track, …
│ └── services/ < AlbumMetadataService, MediaPathResolver, …
├── views/ < PHP view templates
├── api/ < YouTube track API endpoint
├── docs/ < Project images and assets
├── Dockerfile
├── docker-compose.yml
├── .env.example < Template — copy to .env
├── .gitignore
└── README.md
| Table | Description |
|---|---|
artists |
Artist / band records |
albums |
Album metadata (title, year, condition, cover, MBID…) |
formats |
Vinile / CD / Musicassetta / Digital |
genres |
Genre taxonomy |
labels |
Record labels |
tracks |
Tracklists with duration and cached YouTube ID |
audio_files |
Uploaded audio files linked to albums/tracks |
playlists |
User-created playlists |
playlist_tracks |
Many-to-many: playlists ↔ tracks (with position) |
settings |
Key-value app settings (e.g. custom audio path) |
All configuration is via environment variables. See .env.example for the full list.
| Variable | Default | Description |
|---|---|---|
BASE_URL |
http://localhost:8080 |
Canonical public URL, with no trailing slash |
APP_PORT |
8080 |
Host port for the web server |
ALLOWED_HOSTS |
localhost,localhost:8080,127.0.0.1,127.0.0.1:8080 |
Comma-separated list of allowed hostnames or host:port combinations |
DB_NAME |
grizzly_db |
Database name |
DB_USER |
grizzly |
Database user |
DB_PASS |
grizzly_secret |
Database password |
DB_ROOT_PASS |
root_secret_change_me |
MySQL root password |
DEBUG |
false |
Show PHP errors (true only for development) |
LASTFM_API_KEY |
(empty) | Last.fm API key |
DISCOGS_TOKEN |
(empty) | Discogs personal access token |
YOUTUBE_API_KEY |
(empty) | YouTube Data API v3 key |
- No built-in authentication — the app is intended for personal, self-hosted use inside a trusted network. If it is reachable from outside your LAN, restrict access at the reverse proxy or network level (Basic Auth / Access Lists, IP allow-list, VPN). See the Access control notice in the installation section.
- Never commit
.envto version control - Set
DEBUG=falsein any non-local environment - The
public/uploads/directory is served by Apache; audio files outside the web root (configurable via Settings) are streamed through PHP with strict path validation - All database queries use PDO prepared statements
- File uploads are validated by MIME type and extension server-side
Pull requests are welcome. For major changes please open an issue first.
- Fork the repo
- Create your branch:
git checkout -b feature/my-feature - Commit your changes:
git commit -m 'Add my feature' - Push:
git push origin feature/my-feature - Open a Pull Request
Grizzly Music Archive is designed to work as a clean desktop archive and as a responsive mobile catalogue. The interface includes dashboard statistics, album browsing, artist profiles, discography lookup, track playback and dark mode support.
Dashboard
Collection statistics, recently added albums, top artists and playlist overview.
Album detail and player
Album metadata, tracklist, MP3/FLAC upload, YouTube lookup and sticky HTML5 audio player.
| Archive view | Artist profile |
|---|---|
![]() |
![]() |
| Searchable album table with cover, artist, format, year, genre and actions. | Artist biography, local albums, format statistics and official discography. |
| Mobile dashboard | Mobile artist page |
|---|---|
![]() |
![]() |
| Responsive dashboard with compact cards, recent albums and sticky player. | Mobile artist archive with official discography and cover thumbnails. |
MIT — see LICENSE for details.






