Self-hosted media upload, storage, and serving service. Handles image validation, resizing, content moderation, and serves files via Nginx with edge caching.
Upload: App → POST /api/v1/media/upload (JWT) → validate → moderate → resize → local filesystem
Serve: App → GET /media/{owner}/{id}/{variant}.jpg → Nginx (static) → CDN cache
Delete: App → DELETE /api/v1/media/:id (JWT) → remove files + metadata
Admin: Browser → GET /gallery → SSO login → manage all uploads
- Storage: Local filesystem at
{STORAGE_PATH}/{owner_id}/{file_id}/{variant}.jpg - Metadata: PostgreSQL
- Serving: Nginx serves static files directly; CDN caches at edge
- Processing: Auto-resize to thumb (200px), standard (800px), and original
- Moderation: Optional Azure Content Safety integration blocks inappropriate content
- Admin Gallery: Built-in web UI with SSO authentication for managing all uploads
| Environment | Port |
|---|---|
| Production | 8005 |
| Local dev | 8003 |
cp .env.example .env
docker compose up -d # Starts service + local PostgreSQLOr without Docker:
# Requires local PostgreSQL with media_db database
cp .env.example .env
make run| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
No | Health check |
| POST | /api/v1/media/upload |
JWT | Upload an image file |
| GET | /api/v1/media/list |
JWT | List user's files |
| DELETE | /api/v1/media/:id |
JWT | Delete a file |
| GET | /media/:owner_id/:file_id/:variant |
No | Serve a file (dev fallback) |
Each uploaded image is processed into three variants:
| Variant | Width | Use Case |
|---|---|---|
thumb.jpg |
200px | Thumbnails, lists |
standard.jpg |
800px | Detail views |
original.jpg |
unchanged | Full resolution |
| Variable | Default | Description |
|---|---|---|
PORT |
8003 | Server port |
DATABASE_URL |
— | PostgreSQL connection string |
STORAGE_PATH |
./uploads | Local file storage path |
JWT_SECRET |
— | Shared JWT signing secret |
BASE_URL |
http://localhost:8003 | Public URL for building file links |
MAX_FILE_SIZE_MB |
20 | Max upload size |
MAX_STORAGE_MB |
500 | Per-user storage quota |
ALLOWED_TYPES |
image/jpeg,image/png,image/webp | Accepted MIME types |
ADMIN_USER_IDS |
— | Comma-separated user IDs for gallery admin access |
AUTH_SERVICE_URL |
http://localhost:8001 | OAuth auth service URL for gallery SSO |
INTERNAL_API_KEY |
— | Key for service-to-service calls (GDPR deletion) |
AZURE_CONTENT_SAFETY_ENDPOINT |
— | Azure Content Safety endpoint (optional) |
AZURE_CONTENT_SAFETY_KEY |
— | Azure Content Safety API key (optional) |
Deploy with Docker/Podman:
docker build -t media-service .
docker run -d --name media-service \
--env-file .env \
-v media-data:/data/media \
-p 8003:8003 \
media-serviceFor production, place behind a reverse proxy (Nginx) that:
- Serves
/media/as static files from the storage volume - Proxies API requests to the service
- Terminates TLS