Self-hosted media CDN platform. Upload, process, and serve images, videos, and files at scale.
Quick Start · Features · API Reference · SDK · Dashboard · Deployment
demo.mp4
MediaOS is an open-source, self-hosted media CDN built for developers. It handles the full lifecycle of media files — upload, process, store, transform, and serve — so you don't have to stitch together S3, image resizers, video transcoders, and CDN configs yourself.
Upload an image and get back an optimized WebP with instant resize URLs. Upload a video and get an H.264 MP4 with a thumbnail. Everything is served with proper caching headers, range requests, and CORS — ready for production.
curl -fsSL https://raw.githubusercontent.com/arrrrniii/MediaOs/main/install.sh | bashThis pulls the pre-built Docker images, generates secure secrets, and starts everything. Done in under a minute. The installer will ask if you want the dashboard — it's optional.
- API:
http://localhost:3000 - Dashboard:
http://localhost:3001(optional — setup wizard creates your admin account) - Master Key is displayed at the end — save it for API access
# Skip the prompt — API only (no dashboard)
curl -fsSL https://raw.githubusercontent.com/arrrrniii/MediaOs/main/install.sh | bash -s -- --no-dashboard
# Skip the prompt — with dashboard
curl -fsSL https://raw.githubusercontent.com/arrrrniii/MediaOs/main/install.sh | bash -s -- --with-dashboardcurl -O https://raw.githubusercontent.com/arrrrniii/MediaOs/main/docker-compose.hub.yml
curl -O https://raw.githubusercontent.com/arrrrniii/MediaOs/main/.env.example
cp .env.example .env
# Generate a master key and add to .env
node -e "console.log('mv_master_' + require('crypto').randomBytes(24).toString('hex'))"
docker compose -f docker-compose.hub.yml up -dgit clone https://github.com/arrrrniii/MediaOs.git
cd MediaOs
cp .env.example .envGenerate your master key:
node -e "console.log('mv_master_' + require('crypto').randomBytes(24).toString('hex'))"Paste the output into .env as the MASTER_KEY value.
Start everything:
docker compose up -dThis starts 6 services:
| Service | Port | Description |
|---|---|---|
| Worker API | 3000 |
Express API — upload, serve, manage |
| Dashboard | 3001 |
Next.js admin panel |
| PostgreSQL | internal | Database |
| MinIO | internal | S3-compatible object storage |
| Redis | internal | Rate limiting and caching |
| imgproxy | internal | On-the-fly image resizing |
Only the API and Dashboard are exposed. All infrastructure services communicate over the internal Docker network.
The installer asks if you want the dashboard. You can also toggle it anytime in .env:
# Dashboard enabled (default)
COMPOSE_PROFILES=dashboard
# API-only — no dashboard
COMPOSE_PROFILES=Then restart: docker compose up -d
Open the dashboard at http://localhost:3001. On first launch, a setup wizard will guide you through creating your admin account.
Alternatively, create an account via environment variables (headless):
# Add to .env before starting
ADMIN_EMAIL=admin@yoursite.com
ADMIN_PASSWORD=your_secure_passwordOr via the API:
curl -X POST http://localhost:3000/api/v1/accounts \
-H "X-API-Key: YOUR_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Admin", "email": "admin@yoursite.com", "password": "your_password"}'Then create a project and API key from the dashboard, or via API:
# Create a project
curl -X POST http://localhost:3000/api/v1/projects \
-H "X-API-Key: YOUR_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"account_id": "ACCOUNT_ID", "name": "my-app", "slug": "my-app"}'
# Create an API key
curl -X POST http://localhost:3000/api/v1/projects/PROJECT_ID/keys \
-H "X-API-Key: YOUR_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Production Key", "scopes": ["upload", "read", "delete"]}'Save the API key from the response — you'll use it for all file operations.
curl -X POST http://localhost:3000/api/v1/upload \
-H "X-API-Key: mv_live_YOUR_API_KEY" \
-F "file=@photo.jpg"Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"filename": "photo-a8x3k2.webp",
"url": "http://localhost:3000/f/proj-id/photo-a8x3k2.webp",
"urls": {
"original": "http://localhost:3000/f/proj-id/photo-a8x3k2.webp",
"thumb": "http://localhost:3000/img/fit/200/200/f/proj-id/photo-a8x3k2.webp",
"sm": "http://localhost:3000/img/fit/400/0/f/proj-id/photo-a8x3k2.webp",
"md": "http://localhost:3000/img/fit/800/0/f/proj-id/photo-a8x3k2.webp",
"lg": "http://localhost:3000/img/fit/1200/0/f/proj-id/photo-a8x3k2.webp"
},
"type": "image",
"mime_type": "image/webp",
"size": 45200,
"original_size": 182400,
"width": 1200,
"height": 800,
"status": "done",
"processing_ms": 124
}That's it. Your image is converted to WebP, stored, and ready to serve at multiple sizes.
- Images — Auto-converted to WebP with configurable quality. Respects max dimensions. Animated GIFs are converted to MP4.
- Videos — Transcoded to H.264 MP4 with configurable CRF and max resolution. Thumbnails extracted automatically. Async processing (returns
202, fires webhook on completion). - Audio — Stored as-is with duration extraction.
- Documents — Stored as-is with proper MIME types.
Every image gets instant resize URLs powered by imgproxy:
/f/{key} → Original
/img/fit/200/200/f/{key} → Fit within 200x200
/img/fill/500/500/f/{key} → Fill 500x500 (crop)
/img/auto/800/0/f/{key} → Smart resize, 800px wide
/img/force/100/100/f/{key} → Force exact 100x100
Resize modes: fit (preserve aspect ratio), fill (crop to fill), auto (smart), force (exact dimensions).
- Accounts — Each account can have multiple projects
- Projects — Isolated storage, settings, API keys, and usage tracking
- API Keys — Scoped permissions (
upload,read,delete,admin), rate-limited, revocable - Usage Tracking — Per-project storage, bandwidth, uploads, downloads, transforms
Cache-Control: public, max-age=31536000, immutableon all served files- HTTP range requests (video seeking)
ETagheaders for conditional requests- Cross-origin resource sharing (CORS)
- Proper
Content-Typeheaders
- API key hashing — Keys are SHA-256 hashed in the database. Prefix stored for fast lookup, full key shown once at creation (with optional encrypted reveal later).
- Signed URLs — HMAC-SHA256 time-limited URLs for private files
- Webhook signatures — HMAC-SHA256 signatures on all webhook deliveries
- Rate limiting — Per-key rate limiting via Redis
- Input sanitization — File paths sanitized against traversal, parameterized SQL queries
- Helmet — Security headers on all API routes (relaxed for CDN serving routes)
- bcrypt — Password hashing for dashboard accounts
- Constant-time comparison — For all secret comparisons
Subscribe to events and get HTTP POST notifications:
| Event | Description |
|---|---|
file.uploaded |
File uploaded successfully |
file.processed |
Async processing completed (video) |
file.deleted |
File deleted |
file.failed |
Async processing failed |
Payloads are signed with HMAC-SHA256. Failed deliveries retry up to 3 times with backoff (10s, 60s).
Built-in admin panel (Next.js 15) with:
- Project overview with stats (files, storage, bandwidth)
- File browser with image thumbnails and preview modal
- Single file download and bulk ZIP export for backups
- API key management (create, reveal, revoke)
- Webhook management and testing
- Usage analytics with charts
- Auto-update notifications when a new version is available
- API documentation page
- Dark/light theme
- Responsive design (mobile sidebar)
┌─────────────────────────────────────────────────────────┐
│ Clients / SDK │
└───────────────────────────┬─────────────────────────────┘
│
┌────────▼────────┐
│ Worker API │ Express 4 (Node.js)
│ port 3000 │
└──┬───┬───┬───┬──┘
│ │ │ │
┌───────┘ │ │ └───────┐
▼ ▼ ▼ ▼
┌──────────┐ ┌──────┐ ┌─────┐ ┌──────────┐
│PostgreSQL│ │MinIO │ │Redis│ │ imgproxy │
│ 16 │ │(S3) │ │ 7 │ │ │
└──────────┘ └──────┘ └─────┘ └──────────┘
┌────────────────┐
│ Dashboard │ Next.js 15
│ port 3001 │
└────────────────┘
mediaos/
├── worker/ # Express API server
│ ├── src/
│ │ ├── app.js # Express app setup
│ │ ├── config.js # Environment config (single source)
│ │ ├── db.js # PostgreSQL connection
│ │ ├── minio.js # MinIO/S3 client
│ │ ├── middleware/
│ │ │ ├── auth.js # API key authentication
│ │ │ ├── adminAuth.js # Master key authentication
│ │ │ ├── cors.js # CORS configuration
│ │ │ ├── rateLimit.js # Redis-based rate limiting
│ │ │ └── errorHandler.js
│ │ ├── routes/
│ │ │ ├── upload.js # POST /api/v1/upload
│ │ │ ├── files.js # GET/DELETE /api/v1/files
│ │ │ ├── serve.js # GET /f/* and /img/*
│ │ │ ├── webhooks.js # Webhook CRUD
│ │ │ ├── usage.js # Usage stats
│ │ │ ├── accounts.js # Admin: account management
│ │ │ ├── projects.js # Admin: project management
│ │ │ └── apiKeys.js # Admin: API key management
│ │ ├── services/
│ │ │ ├── fileService.js # Upload, process, delete logic
│ │ │ ├── imageProcessor.js # Sharp: WebP conversion
│ │ │ ├── videoProcessor.js # FFmpeg: transcode, thumbnail
│ │ │ ├── keyService.js # API key generation, validation
│ │ │ ├── usageService.js # Usage tracking
│ │ │ ├── webhookService.js # Webhook dispatch + retry
│ │ │ ├── signedUrl.js # HMAC signed URL generation
│ │ │ └── queue.js # Bounded concurrency queue
│ │ └── utils/
│ │ ├── crypto.js # SHA-256, HMAC, AES encrypt/decrypt
│ │ ├── slugify.js # Filename sanitization
│ │ ├── mimeTypes.js # MIME type detection
│ │ └── fileTypes.js # File type classification
│ ├── migrations/ # Raw SQL migrations
│ └── tests/ # Jest test suite
│
├── dashboard/ # Next.js 15 admin panel
│ └── src/
│ ├── app/
│ │ ├── login/ # Login page
│ │ ├── dashboard/ # Dashboard pages
│ │ └── api/ # API proxy routes
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ ├── layout/ # Sidebar, Header, Nav
│ │ ├── files/ # FileGrid, FilePreview
│ │ └── projects/ # CreateProjectModal
│ └── lib/
│ ├── api.ts # Admin fetch helper
│ ├── auth.ts # NextAuth config
│ ├── types.ts # TypeScript definitions
│ └── utils.ts # Formatters
│
├── sdk/ # TypeScript SDK (@mediaos/sdk)
│ └── src/
│ ├── index.ts # MediaOS class
│ └── types.ts # All type definitions
│
├── deploy/ # Nginx, Caddy, Traefik configs
├── docker-compose.yml # Full stack deployment
└── .env.example # Configuration template
All file operations use API keys via the X-API-Key header:
X-API-Key: mv_live_xxxxxxxxxxxxxxxxxxxx
Admin operations (account/project/key management) use the master key via Authorization: Bearer:
Authorization: Bearer mv_master_xxxxxxxxxxxxxxxxxxxx
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/v1/upload |
API Key (upload) |
Upload single file |
POST |
/api/v1/upload/bulk |
API Key (upload) |
Upload up to 20 files |
Single upload:
curl -X POST http://localhost:3000/api/v1/upload \
-H "X-API-Key: mv_live_..." \
-F "file=@image.jpg" \
-F "folder=avatars" \
-F "access=public"Bulk upload:
curl -X POST http://localhost:3000/api/v1/upload/bulk \
-H "X-API-Key: mv_live_..." \
-F "files=@img1.jpg" \
-F "files=@img2.png" \
-F "files=@img3.gif" \
-F "folder=gallery"Query/form parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
folder |
string | — | Organize files into folders |
name |
string | original filename | Custom display name |
access |
string | public |
public or private |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/files |
API Key (read) |
List files with filtering |
GET |
/api/v1/files/:id |
API Key (read) |
Get file metadata |
DELETE |
/api/v1/files/:id |
API Key (delete) |
Soft-delete file |
GET |
/api/v1/files/:id/signed-url |
API Key (read) |
Generate signed URL |
List files with filters:
curl "http://localhost:3000/api/v1/files?type=image&folder=avatars&search=hero&sort=created_at&order=desc&page=1&limit=50" \
-H "X-API-Key: mv_live_..."| Parameter | Type | Default | Description |
|---|---|---|---|
page |
number | 1 |
Page number |
limit |
number | 50 |
Items per page (max 100) |
folder |
string | — | Filter by folder |
type |
string | — | image, video, or file |
search |
string | — | Search by filename |
sort |
string | created_at |
created_at, size, or filename |
order |
string | desc |
asc or desc |
status |
string | — | done, processing, or failed |
Generate signed URL for private files:
curl "http://localhost:3000/api/v1/files/FILE_ID/signed-url?expires=3600" \
-H "X-API-Key: mv_live_..."| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/f/:projectId/* |
None (public) | Serve original file |
GET |
/img/:mode/:w/:h/f/:projectId/* |
None (public) | Serve resized image |
Private files require ?token=...&expires=... query parameters from a signed URL.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/usage |
API Key (read) |
Current period usage |
GET |
/api/v1/usage/history |
API Key (read) |
Daily usage history |
# Current usage
curl http://localhost:3000/api/v1/usage -H "X-API-Key: mv_live_..."
# Last 7 days history
curl "http://localhost:3000/api/v1/usage/history?days=7" -H "X-API-Key: mv_live_..."| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/v1/webhooks |
API Key (read) |
List webhooks |
POST |
/api/v1/webhooks |
API Key (admin) |
Create webhook |
DELETE |
/api/v1/webhooks/:id |
API Key (admin) |
Delete webhook |
curl -X POST http://localhost:3000/api/v1/webhooks \
-H "X-API-Key: mv_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://api.yoursite.com/hooks/mediaos", "events": ["file.uploaded", "file.deleted"]}'| Method | Path | Description |
|---|---|---|
POST |
/api/v1/accounts |
Create account |
GET |
/api/v1/accounts |
List accounts |
POST |
/api/v1/projects |
Create project |
GET |
/api/v1/projects |
List projects |
PATCH |
/api/v1/projects/:id |
Update project |
DELETE |
/api/v1/projects/:id |
Delete project |
POST |
/api/v1/projects/:id/keys |
Create API key |
GET |
/api/v1/projects/:id/keys |
List API keys |
DELETE |
/api/v1/projects/:id/keys/:keyId |
Revoke API key |
POST |
/api/v1/projects/:id/keys/:keyId/reveal |
Reveal full API key |
Install the TypeScript SDK:
npm install @mediaos/sdkimport { MediaOS } from '@mediaos/sdk';
const media = new MediaOS({
url: 'https://cdn.yoursite.com',
apiKey: 'mv_live_...',
});
// Upload
const file = await media.upload(buffer, { name: 'hero.jpg', folder: 'images' });
console.log(file.url);
console.log(file.urls.thumb);
// List
const files = await media.files.list({ type: 'image', limit: 20 });
// Delete
await media.files.delete(file.id);
// Signed URL for private files
const { url } = await media.files.signedUrl(file.id, 3600);
// Custom resize URL (no API call)
const thumb = media.url(file.storage_key, { width: 300, height: 300, fit: 'fill' });
// Usage
const usage = await media.usage.current();
// Webhooks
const webhook = await media.webhooks.create('https://api.yoursite.com/hooks', ['file.uploaded']);Full SDK documentation: sdk/README.md
The built-in admin dashboard runs at http://localhost:3001 and provides:
- Overview — Project stats, recent uploads with image previews, quick actions
- Projects — Create and manage projects, view per-project details
- Files — Browse files in a grid, search/filter, preview images, copy URLs
- API Keys — Create keys with scoped permissions, reveal/copy keys, revoke
- Webhooks — Create webhooks, select events, test deliveries, view stats
- Usage — Storage/bandwidth gauges, upload/download charts, file type breakdown
- Settings — Project configuration (max dimensions, quality, allowed types)
The dashboard uses NextAuth with credential-based authentication. On first launch, a setup wizard creates your admin account. After that, log in with your email and password.
All configuration is done via environment variables in .env:
| Variable | Default | Description |
|---|---|---|
MASTER_KEY |
— | Required. Admin key for account/project management |
PUBLIC_URL |
http://localhost:3000 |
Public-facing URL of the API |
API_PORT |
3000 |
Worker API port |
NODE_ENV |
production |
development or production |
| Variable | Default | Description |
|---|---|---|
PG_DATABASE |
mediaos |
Database name |
PG_USER |
mediaos |
Database user |
PG_PASSWORD |
— | Database password |
| Variable | Default | Description |
|---|---|---|
MINIO_ROOT_USER |
mvadmin |
MinIO access key |
MINIO_ROOT_PASSWORD |
— | MinIO secret key |
MINIO_BUCKET |
mediaos |
Storage bucket name |
| Variable | Default | Description |
|---|---|---|
WEBP_QUALITY |
80 |
WebP output quality (1-100) |
MAX_WIDTH |
1600 |
Max image width in pixels |
MAX_HEIGHT |
1600 |
Max image height in pixels |
| Variable | Default | Description |
|---|---|---|
VIDEO_CRF |
20 |
H.264 CRF quality (lower = better, 18-28 recommended) |
VIDEO_MAX_HEIGHT |
1080 |
Max video height in pixels |
CONCURRENCY |
3 |
Max concurrent video processing jobs |
| Variable | Default | Description |
|---|---|---|
MAX_FILE_SIZE |
104857600 |
Max upload size in bytes (default: 100MB) |
| Variable | Default | Description |
|---|---|---|
REDIS_PASSWORD |
— | Redis password |
| Variable | Default | Description |
|---|---|---|
DASHBOARD_PORT |
3001 |
Dashboard port |
DASHBOARD_URL |
http://localhost:3001 |
Dashboard public URL |
NEXTAUTH_SECRET |
— | NextAuth encryption secret |
- Node.js 20+
- Docker and Docker Compose
- FFmpeg (for local video processing without Docker)
Start infrastructure services:
docker compose up postgres minio redis imgproxy -dRun the worker:
cd worker
npm install
npm run devRun the dashboard:
cd dashboard
npm install
npm run dev -- -p 3001cd worker
npm testMigrations run automatically on worker startup. To run manually:
cd worker
node migrations/migrate.jsMigration files are in worker/migrations/ as numbered SQL files.
MediaOS stores all data in Docker volumes — updates never touch your files or database.
curl -fsSL https://raw.githubusercontent.com/arrrrniii/MediaOs/main/update.sh | bashdocker compose pull
docker compose up -dThe dashboard shows a notification banner when a new version is available on GitHub.
The included docker-compose.yml runs the full stack in production:
# Configure
cp .env.example .env
# Edit .env with secure passwords and your master key
# Deploy
docker compose up -d
# Check health
curl http://localhost:3000/healthPut both services behind a single domain:
cdn.yoursite.com/api/v1/* → worker:3000 (API)
cdn.yoursite.com/f/* → worker:3000 (file serving)
cdn.yoursite.com/img/* → worker:3000 (image resizing)
cdn.yoursite.com/* → dashboard:3001 (admin panel)
Ready-to-use configs for Nginx, Caddy, and Traefik are in the deploy/ directory.
Before going to production, make sure to:
- Set strong
MASTER_KEY - Set strong
PG_PASSWORD - Set strong
MINIO_ROOT_PASSWORD - Set strong
REDIS_PASSWORD - Set strong
NEXTAUTH_SECRET - Set
PUBLIC_URLto your actual domain - Set
DASHBOARD_URLto your dashboard domain - Set
NODE_ENV=production
| Component | Technology | Purpose |
|---|---|---|
| API | Node.js 20, Express 4 | HTTP server, routing, middleware |
| Dashboard | Next.js 15, TypeScript, Tailwind, shadcn/ui | Admin panel |
| SDK | TypeScript | Client library for npm |
| Database | PostgreSQL 16 | Metadata, accounts, projects, keys, usage |
| Storage | MinIO | S3-compatible object storage |
| Image Resize | imgproxy | On-the-fly image transformation |
| Cache | Redis 7 | Rate limiting, session cache |
| Image Processing | Sharp | WebP conversion, resizing |
| Video Processing | FFmpeg | H.264 transcoding, thumbnail extraction |
mv_live_c18df9a0b3e74c6a2f8d1b5e9a7c0d3f
└──────┘└──────────────────────────────────┘
prefix 32 hex characters
Prefix (first 12 chars): mv_live_c18d → stored in plaintext for fast lookup
Full key: → SHA-256 hashed for validation
Keys are scoped with permissions: upload, read, delete, admin.
MediaOS is evolving from a media CDN into a complete open-source media infrastructure platform — self-hosted Netflix/YouTube backend that anyone can run.
| Phase | Status | What's Coming |
|---|---|---|
| v1 — Media CDN | Done | Upload, process, serve images/videos/files with resizing |
| v2 — Streaming | Next | HLS/DASH adaptive streaming, video player SDK, playlists |
| v3 — Security | Planned | DRM (Widevine/FairPlay), geo-restrictions, watermarking |
| v4 — Scale | Planned | Multi-node, edge caching, S3/R2/B2 backends |
| v5 — Intelligence | Planned | AI tagging, content moderation, smart thumbnails, search |
See the full Roadmap for detailed feature breakdown.
We're looking for contributors to help build the next phases. Whether it's HLS streaming, a video player component, or better S3 backend support — there's a lot to build.
# Set up dev environment
cp .env.example .env
docker compose up postgres minio redis imgproxy -d
cd worker && npm install && npm run dev
cd dashboard && npm install && npm run dev -- -p 3001Check out CONTRIBUTING.md for the full guide, code style, and areas we need help with.
Good first contributions:
- Bug fixes and documentation
- S3-compatible backend support (AWS S3, R2, B2)
- HLS video streaming pipeline
- Embeddable video player component
- Dashboard improvements
MIT License — see LICENSE for details.
Free to use for personal and commercial projects.
|
|
SendMailOS — Email marketing platform. Send campaigns, automate workflows, manage subscribers. 2,000+ free emails every month. sendmailos.com |
Built by ARN