Kuanary - Custom Media CDN¶
Custom-built media API for uploading, storing, and serving images via S3 + imgproxy.
Formerly known as: kavicloud (renamed during infrastructure migration)
Overview¶
Purpose: Provide a simple API for uploading images to S3 Object Storage and serving them through imgproxy with custom transformation presets.
Architecture:
Client Upload → Kuanary API → Hetzner S3 Object Storage
↓
Client Request → Kuanary API → imgproxy → S3 → Transformed Image
Access:
- URL: https://media.kua.cl
- Port: 5001
- Container: kuanary
- Location: /opt/kuanary
Features¶
- Upload images to S3 Object Storage
- Generate signed imgproxy URLs for transformations
- Custom transformation presets (resize, crop, format conversion)
- S3-backed storage (scalable, durable)
- Integration with imgproxy for on-the-fly image optimization
Architecture¶
Components¶
- Kuanary API: Node.js/Express API for upload and URL generation
- S3 Object Storage: Hetzner Object Storage for image storage
- imgproxy: Image transformation service
- Presets: Custom transformation presets stored in
/opt/kuanary/data/presets.json
Data Flow¶
Upload Flow¶
1. Client sends image to Kuanary API (POST /upload)
2. Kuanary uploads image to S3 with unique filename
3. Kuanary returns signed imgproxy URL with preset
4. Client receives URL for accessing transformed image
Retrieval Flow¶
1. Client requests image via imgproxy URL
2. imgproxy validates signature
3. imgproxy fetches original from S3
4. imgproxy applies transformations (resize, crop, format)
5. imgproxy returns optimized image
6. Client caches transformed image
Installation & Configuration¶
Directory Structure¶
/opt/kuanary/
├── data/
│ └── presets.json # Image transformation presets
├── .env # Environment variables
├── docker-compose.yml # Docker Compose configuration
└── Dockerfile (if custom build)
Environment Variables¶
Located in /opt/kuanary/.env:
# S3 Configuration
S3_ENDPOINT=https://fsn1.your-objectstorage.com
S3_REGION=fsn1
S3_BUCKET=kuanary-media
S3_ACCESS_KEY=<stored in Infisical>
S3_SECRET_KEY=<stored in Infisical>
# imgproxy Configuration
IMGPROXY_URL=http://cdn.kua.cl
IMGPROXY_KEY=<stored in Infisical>
IMGPROXY_SALT=<stored in Infisical>
# API Configuration
PORT=5001
NODE_ENV=production
Security Note: S3 and imgproxy credentials are stored in Infisical (secrets.kua.cl)
Docker Compose¶
version: '3.8'
services:
kuanary:
image: kuanary:latest
container_name: kuanary
restart: unless-stopped
ports:
- "5001:5001"
env_file:
- .env
volumes:
- ./data:/app/data
networks:
- kuanary-network
networks:
kuanary-network:
driver: bridge
API Endpoints¶
POST /upload¶
Upload an image to S3 and get a signed imgproxy URL.
Request:
Response:
{
"success": true,
"url": "https://cdn.kua.cl/<signature>/rs:fit:300:300/plain/s3://kuanary-media/image-uuid.jpg",
"filename": "image-uuid.jpg"
}
GET /presets¶
List available transformation presets.
Request:
Response:
{
"thumbnail": {
"resize": "fit",
"width": 300,
"height": 300,
"format": "webp"
},
"hero": {
"resize": "fill",
"width": 1920,
"height": 1080,
"format": "webp"
}
}
Transformation Presets¶
Presets are defined in /opt/kuanary/data/presets.json:
{
"thumbnail": {
"resize": "fit",
"width": 300,
"height": 300,
"format": "webp",
"quality": 80
},
"hero": {
"resize": "fill",
"width": 1920,
"height": 1080,
"format": "webp",
"quality": 85
},
"og-image": {
"resize": "fill",
"width": 1200,
"height": 630,
"format": "jpg",
"quality": 90
}
}
Adding a New Preset¶
- Edit
/opt/kuanary/data/presets.json - Add new preset definition
- Restart Kuanary:
docker-compose restart
Example:
{
"avatar": {
"resize": "fill",
"width": 200,
"height": 200,
"format": "webp",
"quality": 85,
"crop": "face"
}
}
Backup & Recovery¶
What to Backup¶
- Data directory:
/opt/kuanary/data(presets.json) - Configuration:
/opt/kuanary/.env - Docker Compose:
/opt/kuanary/docker-compose.yml - S3 data: Managed separately (Hetzner Object Storage)
Backup Strategy¶
- Data directory: Daily backup to Storage Box
- S3 credentials: Stored in Infisical
- S3 data: Hetzner manages durability (not backed up separately)
Recovery Process¶
# 1. Restore data directory
rsync -avz /mnt/storagebox/backups/daily/kuanary/ /opt/kuanary/data/
# 2. Restore configuration
cp /mnt/storagebox/backups/configs/kuanary/.env /opt/kuanary/
# 3. Get S3 credentials from Infisical
# (manually update .env with credentials from secrets.kua.cl)
# 4. Rebuild container (if needed)
cd /opt/kuanary && docker-compose up -d --build
# 5. Verify service
curl https://media.kua.cl/presets
Recovery Time: 10 minutes
Integration with imgproxy¶
Kuanary generates signed imgproxy URLs to prevent unauthorized transformations.
imgproxy URL Structure¶
Example:
https://cdn.kua.cl/abc123def456/rs:fit:300:300/g:ce/plain/s3://kuanary-media/image.jpg
^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
signature resize gravity source URL
Processing Options¶
rs:fit:300:300- Resize to fit within 300x300rs:fill:1920:1080- Resize and crop to fill 1920x1080g:ce- Gravity center (crop from center)g:sm- Gravity smart (use ML to detect subject)f:webp- Convert format to WebPq:85- Quality 85%
Signature Generation¶
imgproxy uses HMAC-SHA256 signatures to validate requests:
const crypto = require('crypto');
function generateSignature(path) {
const key = Buffer.from(process.env.IMGPROXY_KEY, 'hex');
const salt = Buffer.from(process.env.IMGPROXY_SALT, 'hex');
const hmac = crypto.createHmac('sha256', key);
hmac.update(salt);
hmac.update(path);
return hmac.digest('base64url');
}
Maintenance¶
View Logs¶
Restart Service¶
Update Presets¶
# 1. Edit presets on MacBook
vim /Users/kavi/kavi-infra/kuanary/data/presets.json
# 2. Copy to VPS
scp presets.json production:/opt/kuanary/data/
# 3. Restart Kuanary
ssh production "cd /opt/kuanary && docker-compose restart"
Check S3 Storage Usage¶
# Using AWS CLI (requires credentials)
aws s3 ls s3://kuanary-media --recursive --summarize --human-readable
Troubleshooting¶
Issue: Upload fails with "S3 credentials invalid"¶
Solution:
1. Check Infisical for current S3 credentials
2. Update /opt/kuanary/.env with correct credentials
3. Restart Kuanary: docker-compose restart
Issue: imgproxy returns "Invalid signature"¶
Solution:
1. Verify IMGPROXY_KEY and IMGPROXY_SALT match between Kuanary and imgproxy
2. Check /opt/kuanary/.env and /opt/imgproxy/.env
3. Restart both services
Issue: Images not loading¶
Solution:
1. Check imgproxy is running: docker ps | grep imgproxy
2. Check S3 connectivity: curl https://fsn1.your-objectstorage.com
3. Verify S3 bucket exists and is accessible
Performance Optimization¶
Client-Side Caching¶
imgproxy sets appropriate cache headers:
Clients should cache transformed images for 1 year.
CDN Integration (Future)¶
Consider adding Cloudflare CDN in front of imgproxy:
Benefits: - Reduced imgproxy load - Faster global delivery - DDoS protection
Related Documentation¶
- imgproxy Configuration - imgproxy setup and optimization
- Services Overview - All Production VPS services
- Backup Procedures - Backup strategies
Migration Notes¶
Renamed from kavicloud¶
During the December 2025 infrastructure migration, this service was renamed from "kavicloud" to "kuanary".
Migration checklist:
- [x] Renamed container from kavicloud to kuanary
- [x] Updated directory from /opt/kavicloud to /opt/kuanary
- [x] Updated DNS from kavicloud.kua.cl to media.kua.cl
- [x] Updated documentation references
- [x] Updated navigation in mkdocs.yml
No data migration required - S3 bucket and data unchanged.