NB logo
Cloudinary and Umbraco logos side by side on a dark background with a cloud upload illustration

Cloudinary Media Hosting with Umbraco: My Practical Setup Guide

profile

Nitesh Babu

07 May 2026
6 min read

If you have been running Umbraco for any length of time, media storage tends to become a quiet problem before it becomes a loud one. Your /media folder grows. Your deployment pipeline starts to slow down because it is syncing hundreds of megabytes of uploads. Your images arrive at the browser unoptimised and unsized.

Your CMS should manage content, not babysit gigabytes of image files on a server disk.

Cloudinary solves all three problems at once. It hosts your files externally, serves them via a fast global CDN, and handles resizing, format conversion, and compression through URL parameters alone.

This post walks through how to implement a custom Cloudinary backed IFileSystem in Umbraco using the official Cloudinary .NET SDK, so uploads go directly to Cloudinary and Umbraco stores only the references.

What We Are Setting Up

By the end of this guide Umbraco will still handle the upload UI and all media management you are used to. The difference is that the actual files will live in Cloudinary rather than on disk. The stored URL in Umbraco’s media table will point to Cloudinary’s CDN, and image transformations happen through URL manipulation, no additional server processing required.

Prerequisites

  • Umbraco 10 or later
  • Cloudinary account (free tier works for development)
  • Your Cloud Name, API Key, and API Secret from the Cloudinary dashboard

Installing the SDK

We’ll be using the official Cloudinary .NET SDK to work with Cloudinary in our Umbraco project.

dotnet add package CloudinaryDotNet

Configuration

Add your Cloudinary credentials to appsettings.json. Keep the actual secret out of source control, use environment variables or user secrets in development.

{
  "Cloudinary": {
    "CloudName": "your-cloud-name",
    "ApiKey": "your-api-key",
    "ApiSecret": "your-api-secret"
  }
}

Bind it to a settings class:

public class CloudinarySettings
{
    public string CloudName { get; set; } = string.Empty;
    public string ApiKey { get; set; } = string.Empty;
    public string ApiSecret { get; set; } = string.Empty;
}

Register it in Program.cs:

builder.Services.Configure<CloudinarySettings>(
    builder.Configuration.GetSection("Cloudinary"));

Implementing the File System

IFileSystem is the seam Umbraco exposes for exactly this — swap the storage backend without touching a single controller or template.

Umbraco’s media pipeline runs through an IFileSystem abstraction. We implement that interface backed by Cloudinary’s upload and resource APIs.

using CloudinaryDotNet;
using CloudinaryDotNet.Actions;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.IO;

public class CloudinaryFileSystem : IFileSystem
{
    private readonly Cloudinary _cloudinary;

    public CloudinaryFileSystem(IOptions<CloudinarySettings> options)
    {
        var s = options.Value;
        var account = new Account(s.CloudName, s.ApiKey, s.ApiSecret);
        _cloudinary = new Cloudinary(account) { Api = { Secure = true } };
    }

    public bool CanAddPhysical => false;

    public IEnumerable<string> GetDirectories(string path) => [];

    public void DeleteDirectory(string path, bool recursive) { }

    public bool DirectoryExists(string path) => true;

    public void AddFile(string path, Stream stream, bool overrideIfExists = true)
    {
        var publicId = PathToPublicId(path);
        var uploadParams = new ImageUploadParams
        {
            File = new FileDescription(path, stream),
            PublicId = publicId,
            Overwrite = overrideIfExists,
        };
        _cloudinary.Upload(uploadParams);
    }

    public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false)
    {
        var publicId = PathToPublicId(path);
        var uploadParams = new ImageUploadParams
        {
            File = new FileDescription(physicalPath),
            PublicId = publicId,
            Overwrite = overrideIfExists,
        };
        _cloudinary.Upload(uploadParams);
    }

    public IEnumerable<string> GetFiles(string path, SearchOption searchOption) => [];

    public Stream OpenFile(string path)
    {
        var url = GetFullPath(path);
        var http = new HttpClient();
        return http.GetStreamAsync(url).GetAwaiter().GetResult();
    }

    public void DeleteFile(string path)
    {
        var publicId = PathToPublicId(path);
        _cloudinary.Destroy(new DeletionParams(publicId));
    }

    public bool FileExists(string path)
    {
        var publicId = PathToPublicId(path);
        var result = _cloudinary.GetResource(publicId);
        return result.StatusCode == System.Net.HttpStatusCode.OK;
    }

    public string GetRelativePath(string fullPathOrUrl) => fullPathOrUrl;

    public string GetFullPath(string path) => path.StartsWith("http")
        ? path
        : _cloudinary.Api.UrlImgUp.BuildUrl(PathToPublicId(path));

    public string GetUrl(string path) => GetFullPath(path);

    public DateTimeOffset GetLastModified(string path) => DateTimeOffset.UtcNow;

    public DateTimeOffset GetCreated(string path) => DateTimeOffset.UtcNow;

    public long GetSize(string path) => 0;

    private static string PathToPublicId(string path) =>
        path.TrimStart('/').Replace('\\', '/');
}

Registering the File System

Register the implementation in a composer so Umbraco picks it up at startup.

using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Infrastructure.DependencyInjection;

public class CloudinaryComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.SetMediaFileSystem(() =>
        {
            var settings = builder.Services
                .BuildServiceProvider()
                .GetRequiredService<IOptions<CloudinarySettings>>();
            return new CloudinaryFileSystem(settings);
        });
    }
}

How Uploads Work After This

Nothing changes in the backoffice editor experience. Content editors upload images exactly as they always have. The difference happens behind the scenes, the IFileSystem sends the file to Cloudinary via the upload API, and the resulting secure URL is what Umbraco stores in the media item’s umbracoFile property.

https://res.cloudinary.com/your-cloud-name/image/upload/v1234567890/media/image.jpg

Applying Transformations via URL

No image processing library. No server CPU. Just a URL.

This is where Cloudinary pays off. Transformations are URL parameters inserted between /upload/ and the version segment, no server processing required.

Resize to 800px wide with automatic format and quality:

https://res.cloudinary.com/your-cloud-name/image/upload/w_800,f_auto,q_auto/v1234567890/media/image.jpg

Square crop with face detection:

https://res.cloudinary.com/your-cloud-name/image/upload/w_400,h_400,c_fill,g_face/v1234567890/media/image.jpg

A small Razor extension method makes this reusable:

public static string CloudinaryTransform(
    this IPublishedContent media,
    int width,
    int height,
    string crop = "fill")
{
    var src = media.Url();
    var uploadIndex = src.IndexOf("/upload/", StringComparison.Ordinal);
    if (uploadIndex < 0) return src;

    var transform = $"w_{width},h_{height},c_{crop},f_auto,q_auto";
    return src.Insert(uploadIndex + 8, transform + "/");
}

Then in a template:

<img
    src="@photo.CloudinaryTransform(800, 600)"
    alt="@photo.Name"
    width="800"
    height="600"
/>

Handling Existing Media

If you have media already on disk, run a one off migration using the Cloudinary SDK directly before switching the provider.

var cloudinary = new Cloudinary(new Account(cloudName, apiKey, apiSecret));

foreach (var file in Directory.EnumerateFiles(mediaFolder, "*", SearchOption.AllDirectories))
{
    var publicId = Path.GetRelativePath(mediaFolder, file).Replace("\\", "/");
    var uploadParams = new ImageUploadParams
    {
        File = new FileDescription(file),
        PublicId = publicId,
    };
    await cloudinary.UploadAsync(uploadParams);
}

Run this once, then update the stored URLs in the Umbraco database if needed and enable the provider.

Production Considerations

The setup is a one time cost. The operational benefits compound every time a new image is uploaded.

Credentials security. Never commit API keys to source control. Use environment variables, Azure Key Vault, or AWS Secrets Manager depending on where you deploy.

Folder structure. The PathToPublicId helper mirrors Umbraco’s media folder structure as the Cloudinary public ID, so your existing path conventions carry over cleanly.

Environment separation. Use separate Cloudinary environments or folder prefixes (dev/, staging/, prod/) so test uploads do not pollute your production media library.

Backup. Cloudinary retains uploaded assets, but enable Cloudinary’s own backup feature for production and do not treat a CDN as your sole source of truth.

Why This Is Worth the Setup

The implementation is not trivial, but the returns are clear. Your deployment pipeline stops touching media files. Your server disk stays flat regardless of upload volume. Every image served gets automatic format negotiation and quality compression with no template changes. And your CDN coverage is global from day one.

🔗 Share this post

Spread the word wherever you hang out online.

Related Posts

Be part of the thinking behind the craft.

Why real systems behave the way they do. Technical paths that change outcomes.
The web’s native language, decoded.

Monthly. No pandering, no gimmicks, no shallow summaries.