Self-hosted service
AlloVince/EvaThumber avatar
AlloVince/EvaThumber

EvaThumber resizes images by rewriting the URL, with no upload API

A light-weight url based image transformation php library

546 stars179 forksPHPBSD-3-Clause

At a glance

What is it?
EvaThumber 2 is a self-hosted image transformation service for PHP: point it at a directory of originals, start one Docker container, and request resized, cropped and converted images through Cloudinary-style URLs. There is no upload API, no database and no configuration file, and the original images are never modified.
Who is it for?
Adopt EvaThumber if you already have a directory of images and want on-demand derivatives without giving a third party your originals, and if you can accept a fixed transformation vocabulary rather than a programmable pipeline. Skip it if you need animated GIF processing, SVG or PDF output, overlays, or upload and administration APIs, since the README lists all of those as deliberately unimplemented.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 23 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The whole product is a URL grammar

EvaThumber describes itself as a light-weight URL based image transformation PHP library, and the README makes the operating model plain: put your original images in a directory, start one Docker container, then resize, crop and convert them by changing the URL. The path format is fixed.

text
/image/upload/{transformations}/{public_id}.{ext}

The Cloudinary form with a cloud name is also accepted.

text
/{cloud_name}/image/upload/{transformations}/{public_id}.{ext}

Within a single transformation, parameters are separated by commas, and separate transformations are separated by a slash. So a request that fills a 600 by 400 box, greyscales the result, sets quality to 80 and writes WebP looks like this.

text
/image/upload/c_fill,w_600,h_400/e_grayscale/q_80/f_webp/demo.jpg

That grammar is the API. There is no upload endpoint, no database and no configuration file, which means there is also no admin surface to protect and no state to keep consistent beyond the files themselves and the cache directory.

Transformation vocabulary: resize, crop, gravity, quality, format

The parameter set is deliberately finite. Resize covers width, height and relative size with w_, h_ and w_0.5. Crop and fit offer c_scale, c_fit, c_fill, c_crop, c_thumb, c_pad and c_limit, each with a different overflow behaviour, and c_limit is the one that shrinks without enlarging. Gravity is expressed with g_ and accepts the compass values center, north, north_east, east, south_east, south, south_west, west and north_west, so a fill crop can be anchored to a corner rather than the middle. Aspect ratio is set with ar_16:9, and DPR with dpr_2, which renders a 300 by 200 request at 600 by 400 pixels. Rotation and flipping use a_, with values 90, 180, hflip and vflip, and effects use e_, currently grayscale and negate. Quality takes an explicit number or q_auto with the levels best, good, eco and low. The README is candid that q_auto is EvaThumber's local content-adaptive heuristic and not Cloudinary's proprietary perceptual algorithm, which matters if you were planning to migrate between the two.

Running the container and where the cache lives

The images directory is mounted read-only and the container never writes to your originals.

bash
docker run \
  -p 8080:8080 \
  -v /path/to/your/images:/data/images:ro \
  docker.io/allovince/evathumber

With that in place, a request for http://localhost:8080/image/upload/w_600/demo.jpg generates the image on the first hit and caches it afterwards. The cache directory is /data/cache and needs no configuration, but by default it lives inside the container, so removing the container clears every generated image and the next request regenerates it. If the cache has to survive container replacement, mount it as a volume.

bash
docker run \
  -p 8080:8080 \
  -v /path/to/your/images:/data/images:ro \
  -v evathumber-cache:/data/cache \
  docker.io/allovince/evathumber

The README notes that the cache holds derived data only and does not need backing up, which is the right way round for a cache that can be rebuilt at the cost of some CPU.

Production packaging: FrankenPHP, libvips and a locked-down container

EvaThumber 2 is built on PHP 8.5, FrankenPHP and libvips, and the container reflects that stack. The Dockerfile starts from a FrankenPHP image on PHP 8.5, installs libvips42 with its tools plus libffi-dev and unzip, and enables the ffi and pcntl extensions, then copies in the Composer binary. A small ini file turns on ffi, sets memory_limit to 256M, turns expose_php off, disables display_errors and sends errors to stderr. Two libvips tuning variables are set in the environment: VIPS_CONCURRENCY=2 and VIPS_DISC_THRESHOLD=32m. The production stage installs Composer dependencies without dev packages, uses a classmap-authoritative autoloader, copies only src, public and bin, installs the Caddyfile from the docker directory, drops the FrankenPHP capability bit with setcap, creates the data and caddy directories and chowns them to www-data. The container then runs as the www-data user with SERVER_NAME set to port 8080, an entry point of php /app/bin/serve.php, and a healthcheck that curls the /healthz endpoint. The compose file goes further and runs the service read-only, with tmpfs mounts for /tmp and the caddy directories, no-new-privileges, all capabilities dropped, a 512m memory limit, two CPUs and a pids limit of 256.

Output limits and format negotiation

There is a hard ceiling on output size. The result of every request must fit within 4096 pixels on each side and 16 megapixels in total, and because rotation, flipping, effects and quality settings do not change the pixel count, applying any of those on their own to a large original is rejected with 413 image_too_large. In other words, a resize must be part of the same request, which is a sensible rule that avoids spending CPU on a derivative nobody can display. The 4096 pixel figure also shows up in a second place. The demo image is 5184 by 3456, and the README explains that EvaThumber never emits anything larger than 4096 pixels on a side, so the demo picture is the file as it sits in the images directory and cannot be fetched back through a transformation URL. Format selection comes either from the URL extension or from an f_ parameter.

text
/image/upload/w_600/f_webp/demo.jpg
/image/upload/w_600/f_avif/demo.jpg
/image/upload/w_600/f_png/demo.jpg

Automatic negotiation is also available through f_auto, and EvaThumber uses the request's Accept header to choose the format. Input is limited to JPEG, PNG, WebP, AVIF and static GIF, while output adds animated GIF.

What EvaThumber deliberately leaves out

The README has a section on what the project does not do, and it is long enough to be the most useful part of the documentation for anyone evaluating it. EvaThumber 2 does not implement remote or S3 image sources, animated image processing, SVG or PDF, text and image overlays, face detection, automatic AI gravity, rounded corners, blur, sharpen and other stylised effects, video transformation, or upload and administration APIs. The design choice behind that list is stated: unsupported transformations fail explicitly instead of being silently approximated. That is the right call for a service where a silently wrong crop looks like a rendering bug, but it means the vocabulary in the earlier sections is the complete vocabulary. If your roadmap includes a face-aware crop or an overlay of a logo on every thumbnail, this project does not get you there. The one behaviour worth flagging is the first: static GIF only on input, so an animated GIF uploaded as a source will not animate in its derivatives.

Where it sits against Cloudinary and a local build

The obvious comparison is Cloudinary, since EvaThumber deliberately borrows its URL grammar. The difference in approach is that Cloudinary is a hosted multi-tenant service with an administration API, upload endpoints, transformation pipelines and a proprietary quality heuristic, while EvaThumber is a single container that serves one directory of files that you already hold. What you gain is control of the originals and no per-request billing. What you give up is the breadth of the transformation set, the upload APIs, and the managed delivery network. The other alternative is to skip the service entirely and resize with libvips or ImageMagick in your own application, or with a build-time script. That approach gives you full control over the output and no runtime cost, but it means the image pipeline is coupled to your application's deployment, and every consumer of an image has to agree on where the derivative comes from. EvaThumber's position between the two is the URL contract: any client that can build a URL can request an image, and the container does the rest.

Maintenance, licensing and the two-year gap

The release history deserves attention. Version 2.0.0 was published on 2026-09-27, and before that the project sat at 1.0.1 from 2018-05-29 and 1.0.0 from 2018-05-28, whose release note reads Support PHP7 and Docker. In other words, this is a rewrite rather than an incremental line, and the 2.0 release is effectively a new product sharing the name with an eight-year-old library. The last push to the repository was on 2026-09-30, so the 2.0 line is where the work is. The licence is BSD-3-Clause, with the text in the LICENSE file at the repository root, and the repository also carries a Chinese-language README, an AGENTS.md, a bench directory, a phpstan.neon configuration, a phpunit.xml test setup and a composer.lock, so the type checking and test tooling are in place. The homepage given in the project metadata is the avnpc.com pages site.

Editorial conclusion

Adopt EvaThumber if you already have a directory of images and want on-demand derivatives without giving a third party your originals, and if you can accept a fixed transformation vocabulary rather than a programmable pipeline. Skip it if you need animated GIF processing, SVG or PDF output, overlays, or upload and administration APIs, since the README lists all of those as deliberately unimplemented. Before you deploy it, check the 4096 pixel and 16 megapixel output limit against your originals, and mount a volume for /data/cache if generated images must survive container replacement.

Frequently asked questions

How do I run EvaThumber?

Mount a directory of original images read-only into /data/images and start the container with a port mapping, then request a transformation URL such as http://localhost:8080/image/upload/w_600/demo.jpg. The image is generated on the first request and cached afterwards.

Does EvaThumber need an upload API or a database?

No. The README states there is no upload API, no database and no configuration file. You place originals in a directory and change the URL to get a derivative, and EvaThumber keeps the originals untouched.

What are the output size limits in EvaThumber?

Every result must fit within 4096 pixels on each side and 16 megapixels in total. Rotation, flipping, effects and quality settings do not change the pixel count, so applying them alone to a large original is rejected with 413 image_too_large, which means a resize has to be part of the same request.

Which transformations does EvaThumber 2 not support?

The README lists remote or S3 image sources, animated image processing, SVG or PDF, text and image overlays, face detection, automatic AI gravity, rounded corners, blur and other stylised effects, video transformation, and upload or administration APIs. Unsupported transformations fail explicitly rather than being approximated.

Official sources

  1. AlloVince/EvaThumber on GitHub
  2. License: BSD-3-Clause
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/allovince-evathumber.svg)](https://hysenlabs.com/projects/allovince-evathumber)

Was this useful?