Building a quick CDN with PHP
I’ve been using Bunny CDN (referral link) as my CDN for a while, and I’ve been really happy with it. In particular, the Image Optimizer is great value for money - $9.50 a month for on-the-fly dynamic image resizing and re-encoding.
The only real problem is that my site isn’t particularly high-traffic. In the last 30 days I’ve served a grand total of 13mb worth of requests; Instagram, I am not. So, I’m spending $10 a month for very little benefit for my current use case.
So, why not try making my own in PHP. Why PHP? Because PHP is nice to work in, deploying it is easy, and I already write too much Javascript at my day job.
The basic system is:
- Given a url, e.g. https://my-cdn.lewisdale.dev/path/to/image.jpeg
- Lookup the actual file at https://lewisdale.dev/path/to/image.jpeg
- Download it, transform it to either a given width & quality setting, or use some sensible defaults
- Save the transformed file to serve again later
- Serve the new file
I’m using GdImage for image transformation, and to be honest that makes life pretty simple. Here’s the function for resizing an image to a given width:
public function toWidth(\GdImage $image, int $width): \GdImage
{
$rawWidth = imagesx($image);
$rawHeight = imagesy($image);
if ($width < $rawWidth) {
$ratio = $width / $rawWidth;
$height = intval(floor($rawHeight * $ratio));
return imagescale($this->image, $width, $height);
}
return $image;
}
If the requested width is greater than the image width, I don’t try to scale it - the original width is the maximum. Other than that, I just scale down the width and maintain the aspect ratio to avoid stretching.
Then to serve the image:
public function encode(\GdImage $image, int $type, int $quality): string
{
ob_start();
switch ($type) {
case IMAGETYPE_PNG:
imagepng($image, null, $quality);
break;
case IMAGETYPE_GIF:
imagegif($image);
break;
case IMAGETYPE_JPEG:
imagejpeg($image, null, $quality);
break;
case IMAGETYPE_WEBP:
imagewebp($image, null, $quality);
break;
}
$image_data = ob_get_contents();
ob_end_clean();
return base64_encode($image_data);
}
There are some other improvements I’ve made further along, such as serving WebP images if the user’s browser supports them. I then save the resulting base64 string in a database, ready to be served later. Right now it’s just a SQLite database - a filesystem or document store would be quicker and scale better, but in reality I’m serving about 15 images in total.
Here’s it serving with default params:
And resized to 300px:
And at a reduced quality:
It’s not a perfect replication of the features Bunny offers, but it does all the things I needed it for.