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

> Author: [Oussama Ghaieb](https://oussama.ghaieb.com/about-me)
> Canonical: https://oussama.ghaieb.com/posts/laravel-minio-integration-step-by-step-s3-alternative-for-local-development

# 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?](#what-is-minio)
2. [Why Use MinIO with Laravel?](#why-use-minio-with-laravel)
3. [Prerequisites](#prerequisites)
4. [Installing and Setting Up MinIO](#installing-and-setting-up-minio)
5. [Configuring Laravel for MinIO](#configuring-laravel-for-minio)
6. [Basic File Operations](#basic-file-operations)
7. [Advanced MinIO Features](#advanced-minio-features)
8. [Best Practices and Security](#best-practices-and-security)
9. [Troubleshooting Common Issues](#troubleshooting-common-issues)
10. [Conclusion](#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

### Method 1: Using Docker (Recommended)

The fastest way to get MinIO running is with Docker:

```bash
# 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:

```bash
# 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:

```bash
composer require league/flysystem-aws-s3-v3 "^3.0"
```

### Step 2: Environment Configuration

Add MinIO configuration to your `.env` file:

```bash
# 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:

```php
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:

```bash
php artisan make:provider MinIOServiceProvider
```

```php
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

```php
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
        ]);
    }
}
```

```php
# 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-vo7Z9esU.css" /><link rel="stylesheet" href="https://oussama.ghaieb.com/build/assets/app-vo7Z9esU.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>

```

```php
// 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

```php
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

```php
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

```php
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

```php
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

```php
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:

```php
// 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:

```json
{
    "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:

```php
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:

```php
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**:
```php
// 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](https://min.io/docs) and [Laravel filesystem documentation](https://laravel.com/docs/filesystem).

---

*Have questions about implementing MinIO with Laravel? Share your experiences and challenges in the comments below.*
