- gRPC service with MusicBrainz provider - PostgreSQL schema with migrations - Service layer with database-first caching - Repository pattern for data access - YAML configuration support - Research documentation for 17 music metadata projects
24 KiB
Melodee: Deployment Analysis
Deployment Strategy Overview
Melodee provides Docker-based deployment with multi-stage builds, Docker Compose orchestration, and automatic database migrations. The deployment architecture prioritizes ease of setup for self-hosted environments while supporting advanced configurations for production deployments.
Key deployment features:
- Docker multi-stage build: Optimized image size and security
- Docker Compose: Single-command deployment with PostgreSQL
- Automatic migrations: Database schema updates on container startup
- 12 persistent volumes: Data persistence across container restarts
- Raspberry Pi support: ARM64 compatibility for low-power hardware
- Podman compatibility: Rootless container runtime support
Docker Architecture
Multi-Stage Dockerfile
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy project files
COPY ["Melodee.Web/Melodee.Web.csproj", "Melodee.Web/"]
COPY ["Melodee.Data/Melodee.Data.csproj", "Melodee.Data/"]
COPY ["Melodee.Core/Melodee.Core.csproj", "Melodee.Core/"]
# Restore dependencies
RUN dotnet restore "Melodee.Web/Melodee.Web.csproj"
# Copy source code
COPY . .
# Build application
WORKDIR "/src/Melodee.Web"
RUN dotnet build "Melodee.Web.csproj" -c Release -o /app/build
# Publish application
RUN dotnet publish "Melodee.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
# Install FFmpeg for transcoding
RUN apt-get update && \
apt-get install -y --no-install-recommends ffmpeg && \
rm -rf /var/lib/apt/lists/*
# Copy published application
COPY --from=build /app/publish .
# Copy entrypoint script
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
# Expose port
EXPOSE 5000
# Set entrypoint
ENTRYPOINT ["./entrypoint.sh"]
Multi-Stage Benefits:
- Smaller image size: Runtime image excludes SDK (saves ~500 MB)
- Faster deployments: Smaller images transfer and start faster
- Security: No build tools in production image
- Layer caching: Dependencies cached separately from source code
Image Size Comparison:
- Single-stage (with SDK): ~1.2 GB
- Multi-stage (runtime only): ~700 MB
- Savings: ~500 MB (42% reduction)
Entrypoint Script
#!/bin/bash
set -e
echo "Melodee v1.8.0 starting..."
# Wait for PostgreSQL to be ready
echo "Waiting for PostgreSQL..."
until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$POSTGRES_HOST" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c '\q' 2>/dev/null; do
echo "PostgreSQL is unavailable - sleeping"
sleep 2
done
echo "PostgreSQL is ready"
# Run database migrations
echo "Applying database migrations..."
dotnet ef database update --project /app/Melodee.Data.dll --no-build
if [ $? -ne 0 ]; then
echo "Migration failed, exiting..."
exit 1
fi
echo "Migrations applied successfully"
# Start application
echo "Starting Melodee..."
exec dotnet Melodee.Web.dll
Entrypoint Responsibilities:
- Database readiness check: Waits for PostgreSQL before starting
- Automatic migrations: Applies schema changes on startup
- Error handling: Exits if migrations fail
- Process replacement:
execreplaces shell with .NET process for proper signal handling
Signal Handling:
The exec command is critical for graceful shutdown. Without it:
- Docker sends SIGTERM to shell process
- Shell doesn't forward signal to .NET process
- .NET process killed with SIGKILL after timeout
- No graceful shutdown (connections dropped, jobs interrupted)
With exec:
- Docker sends SIGTERM directly to .NET process
- .NET process handles shutdown gracefully
- Connections closed cleanly
- Background jobs complete or checkpoint
Docker Compose Configuration
version: '3.8'
services:
melodee:
image: melodee:1.8.0
container_name: melodee
restart: unless-stopped
ports:
- "5000:5000"
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:5000
- ConnectionStrings__DefaultConnection=Host=postgres;Database=melodee;Username=melodee;Password=${POSTGRES_PASSWORD}
- MusicBrainz__CachePath=/data/mb-cache.db
- Library__Path=/music
- Spotify__ClientId=${SPOTIFY_CLIENT_ID}
- Spotify__ClientSecret=${SPOTIFY_CLIENT_SECRET}
- LastFm__ApiKey=${LASTFM_API_KEY}
- LastFm__SharedSecret=${LASTFM_SHARED_SECRET}
- Google__ClientId=${GOOGLE_CLIENT_ID}
- Google__ClientSecret=${GOOGLE_CLIENT_SECRET}
- Brave__ApiKey=${BRAVE_API_KEY}
volumes:
- music:/music
- data:/data
- logs:/var/log/melodee
- config:/app/config
- cache:/app/cache
- album-art:/app/album-art
- transcoding:/app/transcoding
depends_on:
- postgres
networks:
- melodee-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
postgres:
image: postgres:17
container_name: melodee-postgres
restart: unless-stopped
environment:
- POSTGRES_DB=melodee
- POSTGRES_USER=melodee
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
- postgres-backups:/backups
networks:
- melodee-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U melodee"]
interval: 10s
timeout: 5s
retries: 5
volumes:
music:
driver: local
driver_opts:
type: none
o: bind
device: /path/to/music/library
data:
driver: local
logs:
driver: local
config:
driver: local
cache:
driver: local
album-art:
driver: local
transcoding:
driver: local
postgres-data:
driver: local
postgres-backups:
driver: local
networks:
melodee-network:
driver: bridge
Volume Breakdown:
| Volume | Purpose | Size | Backup Priority |
|---|---|---|---|
music |
User's music library | Varies (100GB-10TB) | Critical (user data) |
data |
MusicBrainz cache, app data | 2-5 GB | Medium (rebuildable) |
logs |
Application logs | 1-10 GB | Low (rotated) |
config |
User settings, API keys | <1 MB | Critical (secrets) |
cache |
Metadata cache | 100 MB-1 GB | Low (rebuildable) |
album-art |
Album cover images | 1-10 GB | Medium (re-downloadable) |
transcoding |
Temporary transcoded files | 1-5 GB | None (temporary) |
postgres-data |
PostgreSQL database | 1-10 GB | Critical (user data) |
postgres-backups |
Database backups | 5-50 GB | Critical (disaster recovery) |
Environment Variables:
| Variable | Purpose | Required | Default |
|---|---|---|---|
ASPNETCORE_ENVIRONMENT |
Runtime environment | No | Production |
ASPNETCORE_URLS |
Listening URLs | No | http://+:5000 |
ConnectionStrings__DefaultConnection |
PostgreSQL connection | Yes | - |
MusicBrainz__CachePath |
SQLite cache location | No | /data/mb-cache.db |
Library__Path |
Music library path | Yes | - |
Spotify__ClientId |
Spotify API credentials | No | - |
Spotify__ClientSecret |
Spotify API credentials | No | - |
LastFm__ApiKey |
Last.fm API credentials | No | - |
LastFm__SharedSecret |
Last.fm API credentials | No | - |
Google__ClientId |
Google OAuth credentials | No | - |
Google__ClientSecret |
Google OAuth credentials | No | - |
Brave__ApiKey |
Brave Search API key | No | - |
Health Checks:
- Melodee: HTTP GET to
/healthendpoint every 30 seconds - PostgreSQL:
pg_isreadycommand every 10 seconds
Health checks enable:
- Automatic restarts: Container restarts if unhealthy
- Load balancer integration: Remove unhealthy instances from rotation
- Monitoring alerts: Trigger notifications on health check failures
Environment File (.env)
# PostgreSQL
POSTGRES_PASSWORD=your-secure-password
# Spotify (optional)
SPOTIFY_CLIENT_ID=your-spotify-client-id
SPOTIFY_CLIENT_SECRET=your-spotify-client-secret
# Last.fm (optional)
LASTFM_API_KEY=your-lastfm-api-key
LASTFM_SHARED_SECRET=your-lastfm-shared-secret
# Google OAuth (optional)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Brave Search (optional)
BRAVE_API_KEY=your-brave-api-key
Security Considerations:
.envfile should be in.gitignore- Use strong passwords (20+ characters, mixed case, numbers, symbols)
- Rotate API keys periodically
- Restrict file permissions:
chmod 600 .env
Deployment Scenarios
Single-Server Deployment
Hardware Requirements:
- CPU: 2+ cores (4+ recommended)
- RAM: 4 GB minimum (8 GB recommended)
- Storage: 50 GB minimum (varies with library size)
- Network: 100 Mbps+ for streaming
Deployment Steps:
- Install Docker and Docker Compose:
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y docker.io docker-compose
# Enable Docker service
sudo systemctl enable docker
sudo systemctl start docker
- Clone repository or create docker-compose.yml:
mkdir melodee
cd melodee
# Create docker-compose.yml and .env files
- Configure environment variables:
nano .env
# Set POSTGRES_PASSWORD and optional API keys
- Update music library path:
# Edit docker-compose.yml
# Change device: /path/to/music/library to actual path
- Start services:
docker-compose up -d
- Verify deployment:
docker-compose ps
docker-compose logs -f melodee
curl http://localhost:5000/health
- Access web interface:
http://localhost:5000
Raspberry Pi Deployment
Hardware Requirements:
- Model: Raspberry Pi 4 (4GB+ RAM recommended)
- Storage: 64 GB+ microSD or USB SSD
- OS: Raspberry Pi OS 64-bit or Ubuntu Server ARM64
ARM64 Image Build:
# Use ARM64 base images
FROM mcr.microsoft.com/dotnet/sdk:10.0-arm64v8 AS build
# ... build stage ...
FROM mcr.microsoft.com/dotnet/aspnet:10.0-arm64v8 AS runtime
# ... runtime stage ...
Performance Optimizations:
- Use SSD instead of microSD: 10x faster I/O
- Disable transcoding: Use direct streaming when possible
- Limit concurrent jobs: Reduce background job parallelism
- Increase swap: Add 2-4 GB swap for memory-intensive operations
Deployment Steps:
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Add user to docker group
sudo usermod -aG docker $USER
# Install Docker Compose
sudo apt-get install -y docker-compose
# Deploy Melodee
docker-compose up -d
Resource Limits:
services:
melodee:
# ... other config ...
deploy:
resources:
limits:
cpus: '3'
memory: 3G
reservations:
cpus: '1'
memory: 1G
Reverse Proxy Deployment
Nginx Configuration:
upstream melodee {
server localhost:5000;
}
server {
listen 80;
server_name music.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name music.example.com;
# SSL certificates
ssl_certificate /etc/letsencrypt/live/music.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/music.example.com/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Proxy settings
location / {
proxy_pass http://melodee;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts for streaming
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# Increase max upload size for album art
client_max_body_size 50M;
}
Traefik Configuration (Docker labels):
services:
melodee:
# ... other config ...
labels:
- "traefik.enable=true"
- "traefik.http.routers.melodee.rule=Host(`music.example.com`)"
- "traefik.http.routers.melodee.entrypoints=websecure"
- "traefik.http.routers.melodee.tls.certresolver=letsencrypt"
- "traefik.http.services.melodee.loadbalancer.server.port=5000"
High Availability Deployment
Architecture:
┌─────────────┐
│ Load Balancer│
│ (HAProxy) │
└──────┬───────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Melodee 1│ │Melodee 2│ │Melodee 3│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└──────────────────┼──────────────────┘
│
┌──────▼───────┐
│ PostgreSQL │
│ Primary │
└──────┬───────┘
│
┌──────▼───────┐
│ PostgreSQL │
│ Replica │
└──────────────┘
Challenges:
- Blazor Server state: SignalR connections tied to specific server
- Session affinity: Load balancer must route user to same server
- Shared storage: Music library and album art must be accessible to all instances
Solutions:
1. Redis Backplane for SignalR:
services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration.EndPoints.Add("redis:6379");
});
2. HAProxy Sticky Sessions:
backend melodee
balance roundrobin
cookie SERVERID insert indirect nocache
server melodee1 melodee1:5000 check cookie melodee1
server melodee2 melodee2:5000 check cookie melodee2
server melodee3 melodee3:5000 check cookie melodee3
3. NFS for Shared Storage:
volumes:
music:
driver: local
driver_opts:
type: nfs
o: addr=nfs-server,rw
device: ":/music"
album-art:
driver: local
driver_opts:
type: nfs
o: addr=nfs-server,rw
device: ":/album-art"
4. PostgreSQL Replication:
services:
postgres-primary:
image: postgres:17
environment:
- POSTGRES_REPLICATION_MODE=master
- POSTGRES_REPLICATION_USER=replicator
- POSTGRES_REPLICATION_PASSWORD=replicator-password
volumes:
- postgres-primary-data:/var/lib/postgresql/data
postgres-replica:
image: postgres:17
environment:
- POSTGRES_REPLICATION_MODE=slave
- POSTGRES_MASTER_HOST=postgres-primary
- POSTGRES_REPLICATION_USER=replicator
- POSTGRES_REPLICATION_PASSWORD=replicator-password
volumes:
- postgres-replica-data:/var/lib/postgresql/data
Podman Deployment
Podman is a daemonless, rootless container runtime compatible with Docker.
Advantages:
- Rootless: Runs without root privileges
- Daemonless: No background daemon process
- Systemd integration: Native systemd service generation
Deployment Steps:
- Install Podman:
# Ubuntu/Debian
sudo apt-get install -y podman podman-compose
# Fedora
sudo dnf install -y podman podman-compose
- Convert Docker Compose to Podman:
# Podman Compose uses same syntax
podman-compose up -d
- Generate systemd service:
# Generate service file for melodee container
podman generate systemd --new --name melodee > ~/.config/systemd/user/melodee.service
# Enable service
systemctl --user enable melodee.service
systemctl --user start melodee.service
Rootless Considerations:
- Port binding: Ports <1024 require root or
sysctl net.ipv4.ip_unprivileged_port_start=80 - Volume permissions: Ensure user has read/write access to volume paths
- Resource limits: Rootless containers have lower default limits
Backup and Recovery
Database Backup
Automated Daily Backups:
#!/bin/bash
BACKUP_DIR="/backups/postgres"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/melodee_$TIMESTAMP.sql.gz"
# Create backup
docker exec melodee-postgres pg_dump -U melodee melodee | gzip > $BACKUP_FILE
# Verify backup
if [ $? -eq 0 ]; then
echo "Backup successful: $BACKUP_FILE"
else
echo "Backup failed"
exit 1
fi
# Retain last 30 days
find $BACKUP_DIR -name "melodee_*.sql.gz" -mtime +30 -delete
# Upload to S3 (optional)
aws s3 cp $BACKUP_FILE s3://melodee-backups/postgres/
Cron Schedule:
0 2 * * * /usr/local/bin/backup-melodee.sh
Restore from Backup:
# Stop Melodee
docker-compose stop melodee
# Restore database
gunzip -c /backups/postgres/melodee_20250428_020000.sql.gz | \
docker exec -i melodee-postgres psql -U melodee melodee
# Start Melodee
docker-compose start melodee
Volume Backup
Backup Script:
#!/bin/bash
BACKUP_DIR="/backups/volumes"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Backup config volume (contains API keys)
docker run --rm \
-v melodee_config:/data \
-v $BACKUP_DIR:/backup \
alpine tar czf /backup/config_$TIMESTAMP.tar.gz -C /data .
# Backup data volume (MusicBrainz cache)
docker run --rm \
-v melodee_data:/data \
-v $BACKUP_DIR:/backup \
alpine tar czf /backup/data_$TIMESTAMP.tar.gz -C /data .
# Backup album-art volume
docker run --rm \
-v melodee_album-art:/data \
-v $BACKUP_DIR:/backup \
alpine tar czf /backup/album-art_$TIMESTAMP.tar.gz -C /data .
Restore Volumes:
# Restore config volume
docker run --rm \
-v melodee_config:/data \
-v $BACKUP_DIR:/backup \
alpine tar xzf /backup/config_20250428_020000.tar.gz -C /data
# Restore data volume
docker run --rm \
-v melodee_data:/data \
-v $BACKUP_DIR:/backup \
alpine tar xzf /backup/data_20250428_020000.tar.gz -C /data
Disaster Recovery
Full System Recovery:
- Install Docker and Docker Compose on new server
- Restore docker-compose.yml and .env files
- Create volumes:
docker volume create melodee_config
docker volume create melodee_data
docker volume create melodee_album-art
docker volume create melodee_postgres-data
- Restore volume data from backups
- Restore PostgreSQL database from backup
- Start services:
docker-compose up -d
- Verify health:
docker-compose ps
curl http://localhost:5000/health
Recovery Time Objective (RTO): 1-2 hours
Recovery Point Objective (RPO): 24 hours (daily backups)
Monitoring and Logging
Prometheus Metrics
Metrics Endpoint:
app.UseEndpoints(endpoints =>
{
endpoints.MapMetrics("/metrics");
});
Prometheus Configuration:
scrape_configs:
- job_name: 'melodee'
static_configs:
- targets: ['melodee:5000']
metrics_path: '/metrics'
scrape_interval: 15s
Key Metrics:
http_requests_total: Total HTTP requestshttp_request_duration_seconds: Request latencydotnet_gc_collections_total: Garbage collection countprocess_cpu_seconds_total: CPU usageprocess_resident_memory_bytes: Memory usagemelodee_scrobbles_total: Total scrobbles submittedmelodee_library_tracks_total: Total tracks in library
Grafana Dashboard
Dashboard Panels:
- Request Rate: Requests per second
- Response Time: P50, P95, P99 latencies
- Error Rate: 4xx and 5xx responses
- CPU Usage: Process CPU percentage
- Memory Usage: Resident memory
- Database Connections: Active connections
- Scrobble Rate: Scrobbles per hour
- Library Size: Total tracks, albums, artists
Log Aggregation
Serilog to Elasticsearch:
Log.Logger = new LoggerConfiguration()
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://elasticsearch:9200"))
{
AutoRegisterTemplate = true,
IndexFormat = "melodee-logs-{0:yyyy.MM.dd}"
})
.CreateLogger();
Kibana Queries:
# Errors in last hour
level:Error AND @timestamp:[now-1h TO now]
# Slow requests (>1s)
http.request.duration:>1000
# Failed scrobbles
message:"scrobble failed"
Security Hardening
HTTPS Configuration
Let's Encrypt with Certbot:
# Install Certbot
sudo apt-get install -y certbot
# Obtain certificate
sudo certbot certonly --standalone -d music.example.com
# Configure Nginx with certificate (see Reverse Proxy section)
Certificate Renewal:
0 0 1 * * certbot renew --quiet && systemctl reload nginx
Firewall Configuration
UFW (Ubuntu):
# Allow SSH
sudo ufw allow 22/tcp
# Allow HTTP/HTTPS (if using reverse proxy)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow Melodee (if direct access)
sudo ufw allow 5000/tcp
# Enable firewall
sudo ufw enable
Secret Management
Docker Secrets (Swarm mode):
services:
melodee:
secrets:
- postgres_password
- spotify_client_secret
environment:
- ConnectionStrings__DefaultConnection=Host=postgres;Database=melodee;Username=melodee;Password_FILE=/run/secrets/postgres_password
secrets:
postgres_password:
file: ./secrets/postgres_password.txt
spotify_client_secret:
file: ./secrets/spotify_client_secret.txt
Vault Integration:
var vaultClient = new VaultClient(new VaultClientSettings("http://vault:8200", "vault-token"));
var secret = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync("melodee/postgres");
var password = secret.Data.Data["password"].ToString();
Performance Tuning
PostgreSQL Optimization
-- Increase shared buffers (25% of RAM)
ALTER SYSTEM SET shared_buffers = '2GB';
-- Increase work memory for complex queries
ALTER SYSTEM SET work_mem = '64MB';
-- Increase maintenance work memory for VACUUM
ALTER SYSTEM SET maintenance_work_mem = '512MB';
-- Optimize for SSD
ALTER SYSTEM SET random_page_cost = 1.1;
-- Enable query planning statistics
ALTER SYSTEM SET track_activity_query_size = 2048;
-- Reload configuration
SELECT pg_reload_conf();
.NET Runtime Optimization
Environment Variables:
environment:
- DOTNET_GCServer=1 # Server GC mode
- DOTNET_GCConcurrent=1 # Concurrent GC
- DOTNET_GCRetainVM=1 # Retain virtual memory
- DOTNET_ThreadPool_MinThreads=50 # Minimum thread pool size
- DOTNET_ThreadPool_MaxThreads=500 # Maximum thread pool size
Caching Configuration
Redis Cache:
services:
redis:
image: redis:7
command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
Application Configuration:
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "redis:6379";
options.InstanceName = "melodee:";
});
Conclusion
Melodee's deployment architecture demonstrates production-ready containerization with Docker multi-stage builds, automatic migrations, and comprehensive volume management. The 12 persistent volumes ensure data persistence, while health checks and logging enable robust monitoring.
Key strengths:
- Easy deployment: Single-command Docker Compose setup
- Automatic migrations: Database schema updates on startup
- Raspberry Pi support: ARM64 compatibility for low-power deployments
- Podman compatibility: Rootless container runtime support
Key challenges:
- Horizontal scaling: Blazor Server requires sticky sessions and Redis backplane
- Backup complexity: 12 volumes require coordinated backup strategy
- Secret management: API keys in environment variables (consider Vault)
The architecture positions Melodee for both simple self-hosted deployments and advanced production configurations with high availability and monitoring.