Oussama GHAIEB

Tips, tricks, and code snippets for developers

Laravel MinIO Integration: Step-by-Step S3 Alternative for Local Development

Last updated: August 2025 | Reading time: 8 minutes

Table of Contents

  1. What is MinIO?
  2. Why Use MinIO with Laravel?
  3. Prerequisites
  4. Installing and Setting Up MinIO
  5. Configuring Laravel for MinIO
  6. Basic File Operations
  7. Advanced MinIO Features
  8. Best Practices and Security
  9. Troubleshooting Common Issues
  10. Conclusion

What is MinIO?

MinIO is a high-performance, S3-compatible object storage solution that's perfect for modern application development. Unlike traditional file storage systems, MinIO provides distributed object storage that scales horizontally, making it ideal for Laravel applications that need reliable, fast file storage.

Key benefits of MinIO:

  • S3-compatible API: Works seamlessly with existing AWS S3 integrations
  • High performance: Optimized for speed and throughput
  • Self-hosted: Complete control over your data and infrastructure
  • Kubernetes-native: Perfect for containerized Laravel deployments
  • Cost-effective: No vendor lock-in or per-request charges

Why Use MinIO with Laravel?

Laravel developers choose MinIO for several compelling reasons:

Performance Advantages:

  • Lightning-fast file uploads and downloads
  • Optimized for concurrent operations
  • Minimal latency compared to cloud alternatives

Development Benefits:

  • Local development environment that mirrors production
  • No internet dependency for file operations during development
  • Consistent S3-compatible interface across all environments

Cost and Control:

  • Eliminate expensive cloud storage bills
  • Complete data sovereignty
  • Predictable infrastructure costs

Prerequisites

Before integrating MinIO with Laravel, ensure you have:

  • Laravel 9.0+ (though MinIO works with earlier versions)
  • PHP 8.0+ with required extensions
  • Composer for dependency management
  • Docker (recommended for MinIO setup)
  • Basic understanding of Laravel's filesystem configuration

Installing and Setting Up MinIO

The fastest way to get MinIO running is with Docker:

# Create a MinIO container
docker run -d \
  --name minio-server \
  -p 9000:9000 \
  -p 9001:9001 \
  -e "MINIO_ROOT_USER=minioadmin" \
  -e "MINIO_ROOT_PASSWORD=minioadmin123" \
  -v /path/to/data:/data \
  minio/minio server /data --console-address ":9001"

Method 2: Binary Installation

For production environments, download the MinIO binary:

# Linux/macOS
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/

# Start MinIO server
export MINIO_ROOT_USER=minioadmin
export MINIO_ROOT_PASSWORD=minioadmin123
minio server /data --console-address ":9001"

Verifying Installation

Once MinIO is running, access the web console at http://localhost:9001 and log in with your credentials.

Configuring Laravel for MinIO

Step 1: Install Required Packages

Laravel's built-in S3 filesystem driver works perfectly with MinIO:

composer require league/flysystem-aws-s3-v3 "^3.0"

Step 2: Environment Configuration

Add MinIO configuration to your .env file:

# MinIO Configuration
MINIO_ENDPOINT=http://localhost:9000
MINIO_KEY=minioadmin
MINIO_SECRET=minioadmin123
MINIO_REGION=us-east-1
MINIO_BUCKET=laravel-app
MINIO_USE_PATH_STYLE_ENDPOINT=true

Step 3: Filesystem Configuration

Update config/filesystems.php to include MinIO disk configuration:

return [
    // ... existing configuration

    'disks' => [
        // ... existing disks

        'minio' => [
            'driver' => 's3',
            'key' => env('MINIO_KEY'),
            'secret' => env('MINIO_SECRET'),
            'region' => env('MINIO_REGION'),
            'bucket' => env('MINIO_BUCKET'),
            'endpoint' => env('MINIO_ENDPOINT'),
            'use_path_style_endpoint' => env('MINIO_USE_PATH_STYLE_ENDPOINT', true),
            'throw' => false,
        ],
    ],
];

Step 4: Create MinIO Service Provider (Optional)

For better organization, create a dedicated service provider:

php artisan make:provider MinIOServiceProvider
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use Aws\S3\S3Client;

class MinIOServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Storage::extend('minio', function ($app, $config) {
            $client = new S3Client([
                'version' => 'latest',
                'region' => $config['region'],
                'endpoint' => $config['endpoint'],
                'use_path_style_endpoint' => $config['use_path_style_endpoint'],
                'credentials' => [
                    'key' => $config['key'],
                    'secret' => $config['secret'],
                ],
            ]);

            return new Filesystem(new AwsS3V3Adapter($client, $config['bucket']));
        });
    }
}

Basic File Operations

File Upload

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
    public function uploadFile(Request $request)
    {
        $request->validate([
            'file' => 'required|file|max:10240', // 10MB max
        ]);

        $file = $request->file('file');

        $disk = Storage::disk('minio');

        /** @var \Aws\S3\S3Client $s3Client */
        $s3Client = $disk->getClient(); // Available since Laravel 9+
        $bucket = config('filesystems.disks.minio.bucket');

        // Check if bucket exists
        try {
            $s3Client->headBucket(['Bucket' => $bucket]);
        } catch (\Aws\S3\Exception\S3Exception $e) {
            return response()->json([
                'message' => "Bucket '{$bucket}' does not exist or is not accessible",
                'error' => $e->getAwsErrorMessage(),
            ], 400);
        }
        
        // Upload to MinIO
        $path = Storage::disk('minio')->putFile('uploads', $file);
        
        // Generate public URL
        $url = Storage::disk('minio')->url($path);
        
        return response()->json([
            'message' => 'File uploaded successfully',
            'path' => $path,
            'url' => $url
        ]);
    }
}
# resources/views/upload.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Upload File</title>
    <link rel="preload" as="style" href="https://oussama.ghaieb.com/build/assets/app-PCyPMbGl.css" /><link rel="stylesheet" href="https://oussama.ghaieb.com/build/assets/app-PCyPMbGl.css" data-navigate-track="reload" /> 
</head>
<body class="flex items-center justify-center min-h-screen bg-gray-100">
    <div class="w-full max-w-md p-6 bg-white rounded-2xl shadow-lg">
        <h1 class="text-xl font-bold mb-4">Upload a File</h1>

        @if($errors->any())
            <div class="mb-4 p-3 bg-red-100 text-red-700 rounded-lg">
                <ul class="list-disc pl-5">
                    @foreach($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif

        <form method="POST" action="{{ route('file.upload') }}" enctype="multipart/form-data" class="space-y-4">
            @csrf

            <div>
                <label for="file" class="block text-sm font-medium text-gray-700">Choose a file</label>
                <input type="file" name="file" id="file"
                       class="mt-1 block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer focus:outline-none focus:ring focus:ring-indigo-200 focus:border-indigo-500">
            </div>

            <button type="submit"
                class="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg shadow hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-400">
                Upload
            </button>
        </form>
    </div>
</body>
</html>

// Route to display the upload form
Route::get('/upload', function () {
    return view('upload'); // resources/views/upload.blade.php
})->name('file.form');
Route::post('/upload', [FileController::class, 'uploadFile'])->name('file.upload');

File Download

public function downloadFile($filename)
{
    if (!Storage::disk('minio')->exists($filename)) {
        abort(404, 'File not found');
    }
    
    return Storage::disk('minio')->download($filename);
}

File Listing and Management

public function listFiles()
{
    $files = Storage::disk('minio')->files('uploads');
    
    $fileDetails = collect($files)->map(function ($file) {
        return [
            'name' => basename($file),
            'path' => $file,
            'size' => Storage::disk('minio')->size($file),
            'last_modified' => Storage::disk('minio')->lastModified($file),
            'url' => Storage::disk('minio')->url($file),
        ];
    });
    
    return response()->json($fileDetails);
}

public function deleteFile($filename)
{
    if (Storage::disk('minio')->exists($filename)) {
        Storage::disk('minio')->delete($filename);
        return response()->json(['message' => 'File deleted successfully']);
    }
    
    return response()->json(['error' => 'File not found'], 404);
}

Advanced MinIO Features

Pre-signed URLs for Secure Access

use Aws\S3\S3Client;

public function generatePresignedUrl($filename)
{
    $client = new S3Client([
        'version' => 'latest',
        'region' => config('filesystems.disks.minio.region'),
        'endpoint' => config('filesystems.disks.minio.endpoint'),
        'use_path_style_endpoint' => true,
        'credentials' => [
            'key' => config('filesystems.disks.minio.key'),
            'secret' => config('filesystems.disks.minio.secret'),
        ],
    ]);
    
    $command = $client->getCommand('GetObject', [
        'Bucket' => config('filesystems.disks.minio.bucket'),
        'Key' => $filename,
    ]);
    
    $request = $client->createPresignedRequest($command, '+1 hour');
    
    return (string) $request->getUri();
}

Multipart Upload for Large Files

public function uploadLargeFile(Request $request)
{
    $request->validate([
        'file' => 'required|file|max:1048576', // 1GB max
    ]);

    $file = $request->file('file');
    
    // For files larger than 100MB, use multipart upload
    if ($file->getSize() > 100 * 1024 * 1024) {
        $stream = fopen($file->getPathname(), 'r');
        
        $path = Storage::disk('minio')->putStream(
            'large-uploads/' . $file->getClientOriginalName(),
            $stream
        );
        
        fclose($stream);
    } else {
        $path = Storage::disk('minio')->putFile('uploads', $file);
    }
    
    return response()->json([
        'message' => 'File uploaded successfully',
        'path' => $path
    ]);
}

Image Processing with MinIO

use Intervention\Image\Facades\Image;

public function uploadAndProcessImage(Request $request)
{
    $request->validate([
        'image' => 'required|image|max:5120', // 5MB max
    ]);

    $image = $request->file('image');
    
    // Create thumbnail
    $thumbnail = Image::make($image)->resize(300, 300, function ($constraint) {
        $constraint->aspectRatio();
    })->encode('jpg', 80);
    
    // Upload original
    $originalPath = Storage::disk('minio')->putFile('images/original', $image);
    
    // Upload thumbnail
    $thumbnailPath = 'images/thumbnails/' . pathinfo($originalPath, PATHINFO_FILENAME) . '_thumb.jpg';
    Storage::disk('minio')->put($thumbnailPath, $thumbnail);
    
    return response()->json([
        'original' => Storage::disk('minio')->url($originalPath),
        'thumbnail' => Storage::disk('minio')->url($thumbnailPath)
    ]);
}

Best Practices and Security

1. Environment-Based Configuration

Always use environment variables for sensitive configuration:

// config/minio.php
return [
    'endpoint' => env('MINIO_ENDPOINT'),
    'key' => env('MINIO_KEY'),
    'secret' => env('MINIO_SECRET'),
    'region' => env('MINIO_REGION', 'us-east-1'),
    'bucket' => env('MINIO_BUCKET'),
    'use_path_style_endpoint' => env('MINIO_USE_PATH_STYLE_ENDPOINT', true),
];

2. Bucket Policies and Access Control

Configure appropriate bucket policies in MinIO console:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::laravel-app/public/*"
        }
    ]
}

3. File Validation and Security

Always validate uploaded files:

public function secureFileUpload(Request $request)
{
    $request->validate([
        'file' => [
            'required',
            'file',
            'max:10240', // 10MB
            'mimes:jpg,jpeg,png,pdf,docx',
            function ($attribute, $value, $fail) {
                // Additional security checks
                $forbiddenExtensions = ['exe', 'bat', 'sh', 'php'];
                $extension = strtolower($value->getClientOriginalExtension());
                
                if (in_array($extension, $forbiddenExtensions)) {
                    $fail('File type not allowed.');
                }
            }
        ]
    ]);
    
    // Continue with upload...
}

4. Monitoring and Logging

Implement comprehensive logging:

use Illuminate\Support\Facades\Log;

public function uploadWithLogging(Request $request)
{
    try {
        $file = $request->file('file');
        
        Log::info('File upload started', [
            'filename' => $file->getClientOriginalName(),
            'size' => $file->getSize(),
            'user_id' => auth()->id()
        ]);
        
        $path = Storage::disk('minio')->putFile('uploads', $file);
        
        Log::info('File uploaded successfully', [
            'path' => $path,
            'user_id' => auth()->id()
        ]);
        
        return response()->json(['path' => $path]);
        
    } catch (\Exception $e) {
        Log::error('File upload failed', [
            'error' => $e->getMessage(),
            'user_id' => auth()->id()
        ]);
        
        return response()->json(['error' => 'Upload failed'], 500);
    }
}

Troubleshooting Common Issues

Connection Issues

Problem: "Connection refused" errors Solution:

  • Verify MinIO server is running
  • Check endpoint URL in configuration
  • Ensure firewall allows connections on MinIO ports

SSL/TLS Issues

Problem: SSL certificate verification failures Solution:

// In config/filesystems.php
'minio' => [
    'driver' => 's3',
    // ... other config
    'options' => [
        'http' => [
            'verify' => false, // Only for development
        ]
    ],
],

Bucket Access Issues

Problem: "Access Denied" errors Solution:

  • Verify bucket exists in MinIO
  • Check access key permissions
  • Review bucket policies

Performance Optimization

Problem: Slow upload/download speeds Solutions:

  • Increase upload_max_filesize and post_max_size in PHP
  • Use multipart uploads for large files
  • Implement connection pooling
  • Consider using MinIO distributed mode

Conclusion

Integrating MinIO with Laravel provides a powerful, cost-effective solution for object storage that scales with your application. The S3-compatible API ensures easy migration and familiar development patterns, while the self-hosted nature gives you complete control over your data.

Key takeaways:

  • MinIO offers high-performance, S3-compatible object storage
  • Laravel's built-in S3 driver works seamlessly with MinIO
  • Proper configuration and security practices are essential
  • Advanced features like pre-signed URLs and multipart uploads enhance functionality

Next steps:

  • Set up MinIO in your development environment
  • Implement basic file operations in your Laravel application
  • Explore advanced features like bucket policies and lifecycle management
  • Consider MinIO's distributed deployment for production scaling

For more advanced MinIO configurations and Laravel integrations, refer to the official MinIO documentation and Laravel filesystem documentation.


Have questions about implementing MinIO with Laravel? Share your experiences and challenges in the comments below.

Tags: #laravel #s3
Oussama GHAIEB - Laravel Certified Developer in Paris

Oussama GHAIEB

Laravel Certified Developer | Full-Stack Web Developer in Paris

14+ years experience 20+ projects
Read more about me →

Comments (0)

No comments yet. Be the first to comment!


Leave a Comment

More Posts :