Caching processed images

Copy Markdown View Source

Store processed images on disk, so a repeat request is served from the cache instead of being processed again. This guide assumes ImagePipe is already running in your app (see Getting started with Phoenix) or as image_pipe_server. How long cached images stay valid is covered in Caching and freshness.

Add a processed-image cache

Point the cache at a directory that ImagePipe can write to. ImagePipe creates the directory if it is missing.

Plug

# lib/my_app_web/router.ex
forward "/images", ImagePipe.Plug,
  sources: [
    media: [
      adapter: ImagePipe.Source.File,
      match: :path,
      options: [root: "/srv/images", root_id: "media"]
    ]
  ],
  cache: {ImagePipe.Cache.FileSystem, root: "/var/cache/image_pipe/processed"}

image_pipe_server

[cache.output]
root = "/var/cache/image_pipe/processed"

In the Docker image, put cache directories under /var/cache/image_pipe and mount a volume there, as shown in Running.

Add an originals cache

Add an originals cache when you serve several sizes or formats of the same image from a remote source: new sizes are then made from the stored original instead of downloading it again. It needs its own directory.

Plug

cache: {ImagePipe.Cache.FileSystem, root: "/var/cache/image_pipe/processed"},
input_cache: {ImagePipe.Cache.FileSystem, root: "/var/cache/image_pipe/originals"}

image_pipe_server

[cache.input]
root = "/var/cache/image_pipe/originals"

Local files are read where they are. To keep copies of them in the originals cache, for example on a network filesystem, set the file source's copy option to keep (see copy files from network storage).

Bound the cache size

Both caches grow without limit by default. Set max_size_bytes to cap a cache. The cache then evicts rarely requested images to stay under it. A bounded cache also needs a node_id that names this server.

Plug

Move the configuration from your forward into an instance, which starts the processes a bounded cache needs. Add the instance to your application's supervision tree, before the endpoint:

# lib/my_app/application.ex
children = [
  {ImagePipe,
   name: MyApp.Images,
   sources: [
     media: [
       adapter: ImagePipe.Source.File,
       match: :path,
       options: [root: "/srv/images", root_id: "media"]
     ]
   ],
   cache:
     {ImagePipe.Cache.FileSystem,
      root: "/var/cache/image_pipe/processed",
      max_size_bytes: 5_000_000_000,
      node_id: "node-0"}},
  MyAppWeb.Endpoint
]

Then mount the instance by name:

# lib/my_app_web/router.ex
forward "/images", ImagePipe.Plug, instance: MyApp.Images

Mount options such as http_cache stay on the forward (see Mounting an instance).

To bound an originals cache, add max_size_bytes and node_id to the instance's input_cache options.

image_pipe_server

[cache.output]
root = "/var/cache/image_pipe/processed"
max_size_bytes = 5_000_000_000
node_id = "node-0"

The server starts the cache's processes itself. Bound [cache.input] the same way.

Run several replicas

If replicas share a cache volume, give each one a different node_id and the same max_size_bytes, which limits the shared root, not each replica's share. If a replica's cache volume outlives the replica, keep its node_id the same when it restarts. A cache volume that belongs to one pod and is lost with it, such as an emptyDir, works with a fixed node_id.

On Kubernetes, run a StatefulSet and use the pod name, such as image-pipe-0, which covers both cases. Deployment pod names change on restart.

Plug

Expose the pod name to the container:

env:
  - name: POD_NAME
    valueFrom: { fieldRef: { fieldPath: metadata.name } }

Read it into each bounded cache's options on the instance:

node_id: System.fetch_env!("POD_NAME")

image_pipe_server

Set node_id from the pod name:

env:
  - name: IPS_CACHE__OUTPUT__NODE_ID
    valueFrom: { fieldRef: { fieldPath: metadata.name } }
  - name: IPS_CACHE__INPUT__NODE_ID
    valueFrom: { fieldRef: { fieldPath: metadata.name } }

Confirm caching works

Allow debug headers for now:

Plug

allow_debug_headers: true

image_pipe_server

[http]
allow_debug_headers = true

Request the same image twice with debug added to its options. If your URLs are signed, sign the URL with debug included.

Plug

$ curl -s -o /dev/null -D - http://localhost:4000/images/w=400/debug/src/photos/beach.jpg | grep 'x-imagepipe-cache:'
x-imagepipe-cache: miss
$ curl -s -o /dev/null -D - http://localhost:4000/images/w=400/debug/src/photos/beach.jpg | grep 'x-imagepipe-cache:'
x-imagepipe-cache: hit

image_pipe_server

Use one of your own image paths. The server listens on port 8080 and serves from / unless [server] sets another port or mount_path.

$ curl -s -o /dev/null -D - http://localhost:8080/w=400/debug/src/photos/beach.jpg | grep 'x-imagepipe-cache:'
x-imagepipe-cache: miss
$ curl -s -o /dev/null -D - http://localhost:8080/w=400/debug/src/photos/beach.jpg | grep 'x-imagepipe-cache:'
x-imagepipe-cache: hit

If [server] sets auth_token, add -H "Authorization: Bearer <token>".

If the second request is still a miss, the origin may forbid storage (see storage permission).

Turn debug headers off again when you're done, since they disclose details about your sources.

Next steps