A production-ready Laravel application for uploading, processing, and managing media files (images and videos) with distributed processing capabilities.
- Media Upload: Upload photos and videos with automatic processing
- Queue Processing: Asynchronous media compression, thumbnail generation, and metadata extraction
- Advanced Search: Full-text search with Meilisearch integration
- Real-time Status: Track processing status of uploaded media
- Admin Dashboard: Comprehensive admin interface with filters, sorting, and pagination
- Microservice Ready: Structured for easy service separation
- Load Testing: Included k6 and Artillery scripts for performance testing
- Laravel 12 (Latest)
- PHP 8.2+
- PostgreSQL (Main database)
- Redis (Queue + Caching)
- Meilisearch (Search indexing)
- FFmpeg (Video processing)
- Docker & Docker Compose (Containerization)
- Nginx (Reverse proxy)
- Laravel Scout (Search integration)
app/
├── Repositories/ # Repository interfaces and implementations
│ ├── MediaRepositoryInterface.php
│ ├── MediaRepository.php
│ ├── TagRepositoryInterface.php
│ └── TagRepository.php
├── Services/ # Business logic layer
│ └── MediaService.php
├── Http/
│ ├── Controllers/ # API controllers
│ │ ├── MediaController.php
│ │ ├── MediaStatusController.php
│ │ └── Admin/
│ │ └── AdminMediaController.php
│ └── Middleware/ # Custom middleware
│ ├── ApiKeyMiddleware.php
│ ├── LogRequestMiddleware.php
│ └── ValidateMediaTypeMiddleware.php
├── Jobs/ # Queue jobs
│ ├── MediaProcessJob.php
│ ├── ThumbnailJob.php
│ └── MetadataJob.php
├── Events/ # Event classes
│ └── MediaProcessed.php
├── Listeners/ # Event listeners
│ ├── SendMediaProcessedNotification.php
│ └── IndexMediaInSearch.php
└── Models/ # Eloquent models
├── MediaFile.php
├── MediaMetadata.php
└── Tag.php
- Repository Pattern: Data access abstraction
- Service Layer: Business logic encapsulation
- Dependency Injection: Used throughout controllers and services
- Event-Driven: Events and listeners for media processing notifications
- Docker & Docker Compose installed
- At least 4GB RAM available
-
Clone and navigate to the project:
cd distributed-photo-compresser -
Copy environment file:
cp .env.example .env
-
Update
.envfile with your configuration:APP_NAME="Distributed Media Library" APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://localhost:8080 DB_CONNECTION=pgsql DB_HOST=postgres DB_PORT=5432 DB_DATABASE=distributed_media DB_USERNAME=postgres DB_PASSWORD=postgres REDIS_HOST=redis REDIS_PORT=6379 QUEUE_CONNECTION=redis MEILISEARCH_HOST=http://meilisearch:7700 MEILISEARCH_KEY=masterKey SCOUT_DRIVER=meilisearch API_KEY=your-secret-api-key-here
-
Build and start containers:
docker-compose up -d --build
-
Install dependencies:
docker-compose exec app composer install -
Generate application key:
docker-compose exec app php artisan key:generate -
Run migrations:
docker-compose exec app php artisan migrate -
Create additional indexes (optional but recommended):
docker-compose exec postgres psql -U postgres -d distributed_media -f /path/to/indexes.sql -
Access the application:
- API: http://localhost:8080/api
- Health check: http://localhost:8080/up
- app: Laravel PHP-FPM application
- nginx: Reverse proxy server
- postgres: PostgreSQL database
- redis: Redis cache and queue
- meilisearch: Search engine
- queue: Queue worker container
- scheduler: Laravel scheduler container
- users: User accounts
- media_files: Main media file records
- media_metadata: Extracted metadata (JSONB)
- tags: Media tags
- media_tag: Pivot table for media-tag relationships
- jobs: Queue jobs table
- failed_jobs: Failed queue jobs
- Fulltext indexes: For name and tag search (GIN indexes)
- B-tree indexes: For created_at, type, status, size
- GIN indexes: For JSONB metadata queries
- Composite indexes: For common query patterns (user_id + type, etc.)
- Partial indexes: For pending and failed media
See database/indexes.sql for all index definitions.
All endpoints require authentication via Laravel Sanctum.
POST /api/media/upload
Content-Type: multipart/form-data
Authorization: Bearer {token}
Body:
- file: (required) Media file
- tags[]: (optional) Array of tag namesGET /api/media?per_page=15&type=image&status=completed&sort_by=created_at&sort_order=desc
Authorization: Bearer {token}GET /api/media/{id}
Authorization: Bearer {token}GET /api/media/search?query=nature&type=image&status=completed&per_page=15
Authorization: Bearer {token}GET /api/media/{id}/status
Authorization: Bearer {token}DELETE /api/media/{id}
Authorization: Bearer {token}Requires API key in header: X-API-Key: {api_key}
GET /api/admin/media?per_page=15&type=video&status=processing&user_id=1
Authorization: Bearer {token}
X-API-Key: {api_key}GET /api/admin/media/statistics
Authorization: Bearer {token}
X-API-Key: {api_key}CORS is configured in config/cors.php:
- Allows all origins (
*) - Allows all methods (GET, POST, PUT, DELETE, etc.)
- Allows all headers
Validates API key for internal microservice communication.
Logs all HTTP requests with method, URL, IP, status, and duration.
Validates uploaded files are images or videos only.
Queue jobs are processed asynchronously using Redis:
- MediaProcessJob: Main processing job (1 hour timeout)
- ThumbnailJob: Generates thumbnails (10 minutes timeout)
- MetadataJob: Extracts metadata (5 minutes timeout)
- Compression: Scales down to max 1920x1080, 85% JPEG quality
- Thumbnails: Generates 3 sizes (150x150, 300x300, 800x800)
- Metadata: Extracts width, height, format, colorspace
- Compression: FFmpeg with H.264 codec, CRF 28, AAC audio
- Thumbnails: Extracts frame at 1 second, generates 3 sizes
- Metadata: Extracts width, height, duration, codec, bitrate, FPS
- User uploads media →
MediaService::uploadMedia() - Media file record created with status
pending MediaProcessJobdispatched to queue- Job updates status to
processing - Parallel jobs dispatched:
ThumbnailJob→ Generates thumbnailsMetadataJob→ Extracts metadata
- Main job compresses media
- Status updated to
completed MediaProcessedevent fired- Listeners:
SendMediaProcessedNotification→ Logs completionIndexMediaInSearch→ Indexes in Meilisearch
Search is powered by Meilisearch via Laravel Scout:
- Indexed fields: name, original_name, type, mime_type, status
- Filterable: type, status, user_id
- Sortable: created_at, size
- Searchable: name, original_name
# Install k6
# macOS: brew install k6
# Linux: https://k6.io/docs/getting-started/installation/
# Run upload test
k6 run --vus 10 --duration 60s load-testing/k6-upload.js
# With custom base URL and token
BASE_URL=http://localhost:8080 API_TOKEN=your-token k6 run load-testing/k6-upload.jsThresholds:
- 95% of requests < 2s
- Error rate < 1%
- Throughput > 5 req/s
# Install Artillery
npm install -g artillery
# Run search test
artillery run load-testing/artillery-search.yml
# With custom token
API_TOKEN=your-token artillery run load-testing/artillery-search.ymlTest Phases:
- Warm-up: 2 req/s for 30s
- Ramp up: 5-20 req/s over 60s
- Sustained: 20 req/s for 120s
- Spike: 50 req/s for 30s
- Cool down: 5 req/s for 30s
- Select only needed fields: Reduces memory usage
- Eager loading: Prevents N+1 queries
- Indexed queries: Uses database indexes efficiently
MediaFile::select(['id', 'name', 'type', 'status', 'created_at'])
->where('user_id', $userId)
->where('type', 'image')
->with(['metadata:id,media_file_id,width,height'])
->orderBy('created_at', 'desc')
->paginate(15);This query:
- Selects only required columns
- Uses indexes on
user_id,type, andcreated_at - Eager loads metadata to prevent N+1
- Uses pagination to limit results
- Authentication: Laravel Sanctum
- API Key: For internal microservice communication
- File Validation: MIME type validation middleware
- CORS: Configured for cross-origin requests
- Input Validation: Request validation on all endpoints
# Run tests
docker-compose exec app php artisan test
# Run specific test suite
docker-compose exec app php artisan test --testsuite=FeatureKey environment variables (see .env.example):
# Application
APP_NAME="Distributed Media Library"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8080
API_KEY=your-secret-api-key
# Database
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=distributed_media
DB_USERNAME=postgres
DB_PASSWORD=postgres
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# Queue
QUEUE_CONNECTION=redis
# Meilisearch
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_KEY=masterKey
SCOUT_DRIVER=meilisearch- Set
APP_DEBUG=falsein production - Use strong
API_KEYfor admin endpoints - Configure proper CORS origins (not
*) - Set up proper logging (Laravel Log, Sentry, etc.)
- Use S3 or similar for media storage
- Scale queue workers based on load
- Monitor queue failures regularly
- Set up database backups
- Use CDN for serving media files
- Enable Redis persistence for queue reliability
The architecture supports horizontal scaling:
- Upload Service: Can be separated into its own service
- Processor Service: Queue workers can run on separate servers
- Search Service: Meilisearch can be scaled independently
- Laravel Documentation
- Laravel Scout
- Meilisearch Documentation
- FFmpeg Documentation
- K6 Documentation
- Artillery Documentation
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
This project is open-sourced software licensed under the MIT license.
# Check queue worker logs
docker-compose logs queue
# Restart queue worker
docker-compose restart queue# Check if FFmpeg is installed in container
docker-compose exec app ffmpeg -version
# If not, rebuild container
docker-compose up -d --build app# Check Meilisearch logs
docker-compose logs meilisearch
# Test connection
docker-compose exec app php artisan scout:sync "App\Models\MediaFile"# Check PostgreSQL logs
docker-compose logs postgres
# Test connection
docker-compose exec app php artisan migrate:statusFor issues and questions, please open an issue on the repository.