The Problem: Protecting Your Media Server

Running a Jellyfin media server is fantastic until that dreaded moment when something goes wrong. Whether it’s a hardware failure, corrupted database, or an accidental configuration change, losing your carefully curated Jellyfin setup can be devastating. Your custom libraries, user preferences, watch history, and metadata all gone in an instant.

After experiencing this firsthand (and spending hours reconfiguring everything), I decided to build a comprehensive backup solution that would automatically protect my Jellyfin installation. The result? The Jellyfin MinIO Backup Plugin a robust, automated backup system that leverages MinIO object storage for reliable, scalable backups.

Why MinIO for Backups?

Before diving into the technical implementation, let’s discuss why MinIO makes an excellent backup target:

  • S3 Compatibility: Works with AWS S3, Google Cloud Storage, and any S3-compatible service
  • Self-Hosted: Keep complete control over your backup data
  • Scalability: Easily scale storage as your backup needs grow
  • Reliability: Built for enterprise-grade reliability and data integrity
  • Cost-Effective: More affordable than cloud storage for large datasets

Architecture Overview

The plugin is built as a native Jellyfin plugin using .NET 8 and follows Jellyfin’s plugin architecture patterns. Here’s what it does:

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Jellyfin      │    │  MinIO Backup    │    │   MinIO/S3      │
│   Server        │───▶│    Plugin        │───▶│   Storage       │
│                 │    │                  │    │                 │
│ • Config        │    │ • Scheduled      │    │ • Compressed    │
│ • Database      │    │   Tasks          │    │   Backups       │
│ • Plugins       │    │ • Retention      │    │ • Automatic     │
│ • Metadata      │    │   Management     │    │   Cleanup       │
└─────────────────┘    └──────────────────┘    └─────────────────┘

Key Features and Implementation

1. Selective Backup Strategy

One size doesn’t fit all when it comes to backups. The plugin provides granular control over what gets backed up:

public class PluginConfiguration : BasePluginConfiguration
{
    public bool BackupConfig { get; set; } = true;     // Essential
    public bool BackupPlugins { get; set; } = true;   // Essential  
    public bool BackupData { get; set; } = true;      // Essential
    public bool BackupLog { get; set; } = false;      // Optional (can be large)
    public bool BackupMetadata { get; set; } = false; // Optional (very large)
    public bool BackupRoot { get; set; } = false;     // Optional
}

Essential folders (Config, Plugins, Data) contain your core Jellyfin setup and should always be backed up. Optional folders like metadata can consume significant storage space—you might want to backup these less frequently or exclude them entirely if you can regenerate metadata.

2. Database Consistency with SQLite Checkpointing

Jellyfin uses SQLite for its database, often in WAL (Write-Ahead Logging) mode. Simply copying database files while Jellyfin is running can result in inconsistent backups. The plugin handles this properly:

private async Task CheckpointDatabase(string dbPath)
{
    try
    {
        using var connection = new SqliteConnection($"Data Source={dbPath}");
        await connection.OpenAsync();
        using var command = connection.CreateCommand();
        command.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);";
        await command.ExecuteNonQueryAsync();
    }
    catch (Exception ex)
    {
        _logger.LogWarning(ex, $"Could not perform database checkpoint for {dbPath}");
    }
}

This ensures that all pending WAL transactions are written to the main database file before backup, guaranteeing a consistent state.

3. Smart Retention Management

Nobody wants infinite backup storage costs. The plugin includes intelligent cleanup based on configurable retention periods:

public async Task CleanupOldBackups()
{
    var cutoffDate = DateTime.UtcNow.AddDays(-_config.RetentionDays);
    
    // List all backup objects
    var listArgs = new ListObjectsArgs()
        .WithBucket(_config.BucketName)
        .WithPrefix("backups/")
        .WithRecursive(true);

    // Parse timestamps from filenames and delete old backups
    var itemsToDelete = backups
        .Where(item => ExtractTimestampFromFilename(item.Key) < cutoffDate)
        .ToList();
        
    // Delete old backups
    foreach (var item in itemsToDelete)
    {
        await _minioClient.RemoveObjectAsync(
            new RemoveObjectArgs()
                .WithBucket(_config.BucketName)
                .WithObject(item.Key));
    }
}

The cleanup process parses timestamps from backup filenames (e.g., full_backup_20250623_020000.zip) and removes backups older than your configured retention period.

Web UI Configuration

The plugin provides a clean, intuitive web interface that integrates seamlessly with Jellyfin’s dashboard:

Jellyfin Plugin Configuration

The configuration page allows you to:

  • Configure MinIO connection settings
  • Select which folders to backup
  • Set retention policies
  • Define exclude patterns for files you don’t want backed up
<div class="checkboxContainer checkboxContainer-withDescription">
    <label class="emby-checkbox-label">
        <input id="BackupConfig" name="BackupConfig" type="checkbox" is="emby-checkbox" />
        <span>Config folder (essential)</span>
    </label>
    <div class="fieldDescription">Configuration files, system settings</div>
</div>

Scheduled Tasks: Set It and Forget It

The plugin registers three scheduled tasks with Jellyfin:

1. Daily Backup (2 AM)

Regular incremental-style backups respecting your folder selection preferences.

2. Weekly Full Backup (Sunday 3 AM)

Comprehensive backup of ALL folders, regardless of configuration—your safety net.

3. Daily Cleanup (4 AM)

Automatic removal of old backups based on retention policy.

public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
    return new[]
    {
        new TaskTriggerInfo
        {
            Type = TaskTriggerInfo.TriggerDaily,
            TimeOfDayTicks = TimeSpan.FromHours(2).Ticks // 2 AM
        }
    };
}

Kubernetes-First Deployment

Since my Jellyfin installation runs on Kubernetes, I included a deployment script that automates plugin installation:

#!/bin/bash
# deploy-plugin.sh

echo "Looking for Jellyfin pods..."

# Auto-detect Jellyfin pod
if kubectl get pods -n $NAMESPACE -l app=jellyfin &>/dev/null; then
    POD_NAME=$(kubectl get pods -n $NAMESPACE -l app=jellyfin -o jsonpath='{.items[0].metadata.name}')
elif kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=jellyfin &>/dev/null; then
    POD_NAME=$(kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=jellyfin -o jsonpath='{.items[0].metadata.name}')
fi

# Deploy plugin files
kubectl cp bin/Release/net8.0/Jellyfin.Plugin.MinioBackup.dll $NAMESPACE/$POD_NAME:$PLUGIN_DIR/
kubectl cp bin/Release/net8.0/manifest.json $NAMESPACE/$POD_NAME:$PLUGIN_DIR/

# Restart pod to load plugin
kubectl delete pod $POD_NAME -n $NAMESPACE

The script automatically detects your Jellyfin pod, copies the plugin files, and restarts the pod to load the new plugin.

Installation and Getting Started

Prerequisites

  • Jellyfin Server 10.10.0 or higher
  • MinIO server or S3-compatible storage
  • .NET 8.0 runtime

Quick Start

  1. Build the plugin:
git clone https://github.com/k3s-me/jellyfin-plugin-minio-backup
cd jellyfin-plugin-minio-backup
dotnet build --configuration Release
  1. Deploy to Kubernetes:
chmod +x deploy-plugin.sh
./deploy-plugin.sh
  1. Configure in Jellyfin:

    • Navigate to Dashboard → Plugins → MinIO Backup
    • Enter your MinIO credentials and settings
    • Select backup folders and retention policy
    • Save configuration
  2. Test the backup:

    • Go to Dashboard → Scheduled Tasks
    • Find “MinIO Backup” and click “Run Now”
    • Check your MinIO bucket for the backup file
Jellyfin Scheduled Tasks

Advanced Configuration

Exclude Patterns

Use glob patterns to exclude specific files or directories:

transcodes/*     # Exclude transcoding cache
cache/*          # Exclude general cache
*.tmp            # Exclude temporary files
*.log            # Exclude log files (if not backing up logs)

SSL Considerations

For self-hosted MinIO with self-signed certificates, the plugin automatically bypasses SSL validation:

var httpClientHandler = new HttpClientHandler()
{
    ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) =>
    {
        _logger.LogInformation("SSL Certificate validation bypassed for MinIO connection");
        return true; // Accept all certificates
    }
};

Backup Strategy Recommendations

Based on real-world usage, here’s my recommended backup strategy:

Daily Backups (Essential folders only):

  • Config: ✅ Always
  • Plugins: ✅ Always
  • Data: ✅ Always
  • Logs: ❌ Usually not needed
  • Metadata: ❌ Can be regenerated
  • Root: ❌ Usually empty

Weekly Full Backup:

  • All folders including metadata
  • Provides comprehensive restore point

Retention:

  • 30 days for daily backups (balance between safety and storage cost)
  • 90 days for weekly full backups

Real-World Performance

In my testing with a typical Jellyfin installation:

  • Essential folders backup: ~50MB, completes in 30 seconds
  • Full backup with metadata: ~2GB, completes in 5-10 minutes
  • Storage efficiency: 10:1 compression ratio typical for config/database files

Troubleshooting Common Issues

Plugin Not Loading

# Check Jellyfin logs
kubectl logs jellyfin-pod -n jellyfin

# Verify plugin files
kubectl exec jellyfin-pod -n jellyfin -- ls -la /config/plugins/Jellyfin.Plugin.MinioBackup/

MinIO Connection Issues

  • Verify endpoint includes port (e.g., minio.example.com:9000)
  • Check bucket permissions (should allow read/write/delete)
  • For internal networks, ensure SSL settings match your MinIO configuration

Large Backup Sizes

  • Review exclude patterns
  • Consider excluding metadata folder for daily backups
  • Monitor metadata folder size—it can grow very large with extensive libraries

Future Enhancements

The plugin provides a solid foundation, but there’s room for improvement:

  • Backup verification: Automated restore testing
  • Multi-destination: Backup to multiple locations simultaneously
  • Encryption: Client-side encryption for sensitive data

Source Code and Contributing

The complete source code is available on GitHub: jellyfin-minio-backup-plugin

Key files to explore:

Contributions welcome! Whether it’s bug fixes, feature enhancements, or documentation improvements, pull requests are appreciated.

Conclusion

Data loss is preventable, but only if you have reliable backups in place. The Jellyfin MinIO Backup Plugin provides an automated, configurable solution that scales from small home servers to large Kubernetes deployments.

The plugin demonstrates several important patterns for Jellyfin plugin development:

  • Proper integration with Jellyfin’s plugin architecture
  • Database consistency considerations
  • Kubernetes-friendly deployment
  • Clean web UI integration

Whether you’re protecting a small home media server or a large multi-user installation, automated backups should be part of your infrastructure. With this plugin, you can sleep better knowing your Jellyfin set-up is protected.


Tags: #Jellyfin #MinIO #Backup #Kubernetes #DotNet #SelfHosted #MediaServer #DataProtection