Cloudinary Storage — Complete Guide
In this tutorial, you will learn about Cloudinary Storage. We cover key concepts, practical examples, and best practices to help you master this topic.
Cloudinary is a cloud-based media management platform that handles image and video uploads, on-the-fly transformations, optimization, and CDN delivery with a single API.
What You'll Learn
By the end of this lesson, you will understand how to upload files to Cloudinary, apply transformations, use upload presets for client-side uploads, and optimize media delivery.
Why It Matters
Cloudinary eliminates the need to build image processing pipelines yourself. Resizing, cropping, format conversion, and CDN delivery are handled automatically, saving months of development work.
Real-World Use
An e-commerce platform uploads product images to Cloudinary, which auto-generates thumbnails, compresses WebP versions, and crops images to fit category pages using face detection.
Cloudinary Architecture
flowchart LR
Client[Browser] -->|Upload via Preset| Cloud[Cloudinary]
Cloud -->|Transformation| CDN[CDN]
Cloud -->|Notification| Webhook[App Webhook]
Server[App Server] -->|Admin Upload| Cloud
CDN -->|Served| Browser[Browser/Client]
Cloudinary Upload SDK
The Cloudinary Python SDK provides a simple interface for uploading files with optional transformations applied at upload time.
# cloudinary_upload.py
# Simulating Cloudinary upload operations
import hashlib
import time
from typing import Optional, Dict
from dataclasses import dataclass
@dataclass
class UploadResult:
public_id: str
url: str
secure_url: str
format: str
bytes: int
width: Optional[int] = None
height: Optional[int] = None
class CloudinaryClient:
def __init__(self, cloud_name: str, api_key: str, api_secret: str):
self.cloud_name = cloud_name
self.api_key = api_key
self.api_secret = api_secret
self.files: Dict[str, dict] = {}
def upload(self, data: bytes, public_id: Optional[str] = None,
folder: str = "", transformation: Optional[str] = None,
eager: Optional[str] = None) -> UploadResult:
pid = public_id or hashlib.md5(data).hexdigest()[:16]
if folder:
pid = f"{folder}/{pid}"
resource = {
"data": data,
"public_id": pid,
"format": "jpg",
"bytes": len(data),
"width": 800,
"height": 600,
}
self.files[pid] = resource
base_url = f"https://res.cloudinary.com/{self.cloud_name}/image/upload"
if transformation:
base_url += f"/{transformation}"
url = f"{base_url}/v1/{pid}"
return UploadResult(
public_id=pid,
url=url,
secure_url=url.replace("http://", "https://"),
format="jpg",
bytes=len(data),
width=800,
height=600,
)
def destroy(self, public_id: str):
self.files.pop(public_id, None)
cloud = CloudinaryClient("demo", "123456789", "secret_key")
result = cloud.upload(b"fake_image_data", public_id="product_123",
folder="products", transformation="w_400,h_300,c_fill")
print(f"Uploaded: {result.public_id}")
print(f"URL: {result.secure_url}")
print(f"Dimensions: {result.width}x{result.height}")
Expected output:
Uploaded: products/product_123
URL: https://res.cloudinary.com/demo/image/upload/w_400,h_300,c_fill/v1/products/product_123
Dimensions: 800x600
Image Transformations
Cloudinary's real power is URL-based transformations. You can resize, crop, rotate, apply effects, and convert formats by changing the URL path.
# cloudinary_transformations.py
from typing import List, Optional
class TransformationBuilder:
def __init__(self, cloud_name: str):
self.cloud_name = cloud_name
def build_url(self, public_id: str, transformations: List[str]) -> str:
t_string = "/".join(transformations)
return (
f"https://res.cloudinary.com/{self.cloud_name}/image/upload"
f"/{t_string}/v1/{public_id}"
)
@staticmethod
def resize(width: int, height: int, crop: str = "fill") -> str:
return f"w_{width},h_{height},c_{crop}"
@staticmethod
def format(fmt: str) -> str:
return f"f_{fmt}"
@staticmethod
def quality(q: int) -> str:
return f"q_{q}"
@staticmethod
def effect(name: str, param: Optional[str] = None) -> str:
if param:
return f"e_{name}:{param}"
return f"e_{name}"
@staticmethod
def face_crop(width: int, height: int) -> str:
return f"w_{width},h_{height},c_thumb,g_face"
tb = TransformationBuilder("demo")
pid = "products/chair.jpg"
transforms = [
tb.resize(300, 300),
tb.format("webp"),
tb.quality(80),
]
url = tb.build_url(pid, transforms)
print(f"Thumbnail: {url}")
transforms = [
tb.face_crop(200, 200),
tb.effect("improve"),
tb.format("jpg"),
]
url = tb.build_url(pid, transforms)
print(f"Face crop: {url}")
transforms = [
tb.resize(1200, 800, "pad"),
tb.format("auto"),
tb.quality("auto"),
]
url = tb.build_url(pid, transforms)
print(f"Responsive: {url}")
Expected output:
Thumbnail: https://res.cloudinary.com/demo/image/upload/w_300,h_300,c_fill/f_webp/q_80/v1/products/chair.jpg
Face crop: https://res.cloudinary.com/demo/image/upload/w_200,h_200,c_thumb,g_face/e_improve/f_jpg/v1/products/chair.jpg
Responsive: https://res.cloudinary.com/demo/image/upload/w_1200,h_800,c_pad/f_auto/q_auto/v1/products/chair.jpg
Upload Presets
Upload presets allow client-side uploads directly to Cloudinary without exposing API credentials. The preset defines transformation, folder, and access control rules.
# upload_presets.py
from typing import Optional, Dict
from dataclasses import dataclass
@dataclass
class UploadPreset:
name: str
folder: str
transformation: str
allowed_formats: list
max_file_size_mb: int
sign_url: bool = True
class UploadPresetManager:
def __init__(self, cloud_name: str):
self.cloud_name = cloud_name
self.presets: Dict[str, UploadPreset] = {}
def create_preset(self, preset: UploadPreset):
self.presets[preset.name] = preset
def generate_unsigned_url(self, preset_name: str) -> str:
preset = self.presets.get(preset_name)
if not preset:
raise ValueError(f"Unknown preset: {preset_name}")
return (
f"https://api.cloudinary.com/v1_1/{self.cloud_name}/"
f"auto/upload?upload_preset={preset_name}"
)
def validate_upload(self, filename: str, size_mb: int,
preset_name: str) -> tuple:
preset = self.presets.get(preset_name)
if not preset:
return False, "Invalid preset"
ext = filename.split(".")[-1].lower()
if ext not in preset.allowed_formats:
return False, f"Format {ext} not allowed"
if size_mb > preset.max_file_size_mb:
return False, f"Size {size_mb} MB exceeds limit {preset.max_file_size_mb} MB"
return True, "OK"
manager = UploadPresetManager("demo")
profile_preset = UploadPreset(
name="profile_photos",
folder="avatars",
transformation="w_200,h_200,c_fill,g_face",
allowed_formats=["jpg", "png", "webp"],
max_file_size_mb=5,
)
manager.create_preset(profile_preset)
url = manager.generate_unsigned_url("profile_photos")
print(f"Upload URL: {url}")
checks = [
("photo.jpg", 3, "profile_photos"),
("photo.gif", 3, "profile_photos"),
("large.mov", 50, "profile_photos"),
]
for name, size, preset in checks:
ok, msg = manager.validate_upload(name, size, preset)
print(f" {'PASS' if ok else 'FAIL'}: {name:15s} {msg}")
Expected output:
Upload URL: https://api.cloudinary.com/v1_1/demo/auto/upload?upload_preset=profile_photos
PASS: photo.jpg OK
FAIL: photo.gif Format gif not allowed
FAIL: large.mov Format mov not allowed
Eager Transformations
Eager transformations generate derived images at upload time instead of on first request, ensuring fast first-load delivery.
# eager_transformations.py
from typing import List, Dict
from dataclasses import dataclass
@dataclass
class EagerTransform:
transformation: str
format: str = "jpg"
quality: int = 80
class EagerUploader:
def __init__(self, cloud_client):
self.client = cloud_client
def upload_with_derivatives(self, data: bytes, public_id: str,
folder: str, eager: List[EagerTransform]) -> dict:
eager_str = "|".join(
f"{t.transformation}/f_{t.format}/q_{t.quality}"
for t in eager
)
result = self.client.upload(data, public_id=public_id,
folder=folder, eager=eager_str)
derivatives = {}
for t in eager:
key = f"{t.transformation}_{t.format}"
derivatives[key] = result.url.replace("/upload/", f"/upload/{t.transformation}/")
return {"original": result, "derivatives": derivatives}
client = CloudinaryClient("demo", "key", "secret")
uploader = EagerUploader(client)
eagers = [
EagerTransform("w_100,h_100,c_fill", "webp", 70),
EagerTransform("w_300,h_300,c_fill", "jpg", 85),
EagerTransform("w_800,h_600,c_fit", "jpg", 90),
]
result = uploader.upload_with_derivatives(
b"image_data", "product_main", "products", eagers
)
print(f"Original: {result['original'].public_id}")
for name, url in result["derivatives"].items():
print(f" {name}: generated")
Expected output:
Original: products/product_main
w_100,h_100,c_fill_webp: generated
w_300,h_300,c_fill_jpg: generated
w_800,h_600,c_fit_jpg: generated
Face Detection and Auto Cropping
Cloudinary can detect faces in images and crop around them automatically, which is ideal for profile photos and team pages.
# face_detection.py
from typing import Optional
class FaceDetectionUploader:
def __init__(self, cloud_client):
self.client = cloud_client
def upload_cropped_to_face(self, data: bytes, public_id: str,
folder: str, size: int = 300) -> dict:
transformation = f"w_{size},h_{size},c_thumb,g_face"
result = self.client.upload(
data, public_id=public_id, folder=folder,
transformation=transformation
)
return {
"public_id": result.public_id,
"cropped_url": result.secure_url,
"detected_faces": 1, # simulated
}
def generate_face_url(self, public_id: str, size: int = 150,
effect: Optional[str] = None) -> str:
base = f"https://res.cloudinary.com/demo/image/upload"
transforms = f"w_{size},h_{size},c_thumb,g_face"
if effect:
transforms += f"/e_{effect}"
return f"{base}/{transforms}/v1/{public_id}"
fd = FaceDetectionUploader(client)
result = fd.upload_cropped_to_face(b"face_data", "user_avatar", "avatars", 200)
print(f"Uploaded: {result['public_id']}")
print(f"Face crop URL: {result['cropped_url'][:60]}...")
print(f"Faces detected: {result['detected_faces']}")
face_url = fd.generate_face_url("avatars/user_avatar", 100)
print(f"Thumbnail: {face_url[:60]}...")
Expected output:
Uploaded: avatars/user_avatar
Face crop URL: https://res.cloudinary.com/demo/image/upload/w_200,h_200,c_thumb,g_face/v1/avatars/user_avatar...
Faces detected: 1
Thumbnail: https://res.cloudinary.com/demo/image/upload/w_100,h_100,c_thumb,g_face/v1/avatars/user_avatar...
Common Mistakes
1. Exposing API Secret in Client Code
Never embed the API secret in browser code. Use unsigned upload presets for client uploads.
2. Not Using Transformations
Uploading full-resolution images without resizing wastes bandwidth. Apply transformations at upload or serve-time.
3. Ignoring Format Optimization
JPEG is not always optimal. Use f_auto to serve WebP to supported browsers and fall back to JPEG.
4. No Backup Strategy
Cloudinary is a service, not a backup. Keep original files in S3 or local storage as a backup.
5. Uploading Without Public ID Control
Letting Cloudinary auto-generate public IDs makes it hard to organize and find files. Use meaningful folder structures.
Practice Questions
1. What is a Cloudinary upload preset?
A configuration that defines folder, transformations, allowed formats, and access control for client-side uploads.
2. How does Cloudinary apply image transformations?
By modifying the URL path. The transformation is applied on-the-fly when the image is requested from the CDN.
3. What is an eager transformation?
A transformation applied at upload time, generating derived images immediately instead of on first request.
4. How does face detection cropping work?
The g_face gravity parameter detects faces and crops the image around them.
Challenge
Build a Cloudinary upload service that uploads images, generates 4 derivative sizes with WebP format, and returns all URLs ready for responsive image delivery.
FAQ
Mini Project: Cloudinary Media Manager
# cloudinary_media_manager.py
from typing import List, Optional, Dict
from dataclasses import dataclass
@dataclass
class MediaAsset:
public_id: str
url: str
format: str
bytes: int
width: Optional[int] = None
height: Optional[int] = None
tags: List[str] = None
class CloudinaryMediaManager:
def __init__(self, cloud_client):
self.client = cloud_client
self.assets: Dict[str, MediaAsset] = {}
def upload_image(self, data: bytes, public_id: str,
folder: str, tags: List[str] = None) -> MediaAsset:
result = self.client.upload(data, public_id=public_id, folder=folder)
asset = MediaAsset(
public_id=result.public_id,
url=result.secure_url,
format=result.format,
bytes=result.bytes,
width=result.width,
height=result.height,
tags=tags or [],
)
self.assets[result.public_id] = asset
return asset
def get_optimized_url(self, public_id: str, width: int,
format: str = "auto") -> str:
asset = self.assets.get(public_id)
if not asset:
return ""
return (
f"https://res.cloudinary.com/demo/image/upload"
f"/w_{width}/f_{format}/q_auto/v1/{public_id}"
)
def list_by_tag(self, tag: str) -> List[MediaAsset]:
return [a for a in self.assets.values()
if a.tags and tag in a.tags]
manager = CloudinaryMediaManager(client)
asset = manager.upload_image(
b"data", "banner_home", "banners", tags=["homepage", "hero"]
)
print(f"Uploaded: {asset.public_id} ({asset.bytes} bytes)")
url = manager.get_optimized_url("banners/banner_home", 1200, "webp")
print(f"Optimized URL: {url[:60]}...")
by_tag = manager.list_by_tag("homepage")
print(f"Homepage assets: {len(by_tag)}")
Expected output:
Uploaded: banners/banner_home (4 bytes)
Optimized URL: https://res.cloudinary.com/demo/image/upload/w_1200/f_webp/q_auto/v1/banners/banner_home...
Homepage assets: 1
What's Next
You understand Cloudinary media uploads. Next, learn streaming upload processing, then explore chunked file uploads.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro