Plex + NGINX Reverse Proxy + Caching

I wanted to see if anyone else has considered the idea of using NGINX caching to keep the load down to a minimum for actual Plex API calls. If you’re doing so, what locations did you decide to cache and why?

Currently I’m running a setup with the following caching and have cut the chatter down to just library reads and status update information. To be fair, the caching of children, related, similar, and items does cause a delay in the watch status and resume status when viewed, however, when you enter the actual video then the playback status is properly reflected.

In case you are wondering the motivation behind the collections, I have a number of collections based on things such as publisher that are regularly displayed.

# Let's give this a better look
log_format  plex        '#######################################################\n'
                        'Time                   [$time_local]\n'
                        'HTTP Status            [$status]\n'
                        'Plex-Client-Identifier [$http_x_plex_client_identifier]\n'
                        'Plex-Token             [$http_x_plex_token]\n'
                        'Remote IP              [$remote_addr]\n'
                        'Forwarded-For          [$http_x_forwarded_for]\n'
                        'Referrer               [$http_referer]\n'
                        'Request                [$request]\n'
                        '#######################################################\n';

log_format  plex_cache_images  '#######################################################\n'
                        'Time                   [$time_local]\n'
                        'HTTP Status            [$status]\n'
                        'Cache Status           [Cache:$upstream_cache_status]\n'
                        'Key                    [$arg_url$arg_width$arg_height;]\n'
                        'Request                [$request]\n'
                        '#######################################################\n';

log_format  plex_cache_meta  '#######################################################\n'
                        'Time                   [$time_local]\n'
                        'HTTP Status            [$status]\n'
                        'Cache Status           [Cache:$upstream_cache_status]\n'
                        'Key                    [$uri$x_plex_token$content_directory_id]\n'
                        'Request                [$request]\n'
                        '#######################################################\n';

log_format  plex_debug  'Time                   [$time_local]\n'
                        'Remote IP              [$remote_addr]\n'
                        'HTTP Status            [$status]\n'
                        'Referrer               [$http_referer]\n'
                        'User-Agent             [$http_user_agent]\n'
                        'Forwarded-For          [$http_x_forwarded_for]\n'
                        'Request                [$request]\n'
                        'Plex-Client-Identifier [$http_x_plex_client_identifier]\n'
                        'Plex-Device            [$http_x_plex_device]\n'
                        'Plex-Device-Name       [$http_x_plex_device_name]\n'
                        'Plex-Platform          [$http_x_plex_platform]\n'
                        'Plex-Platform-Version  [$http_x_plex_platform_version]\n'
                        'Plex-Product           [$http_x_plex_product]\n'
                        'Plex-Token             [$http_x_plex_token]\n'
                        'Plex-Version           [$http_x_plex_version]\n'
                        'Plex-Nocache           [$http_x_plex_nocache]\n'
                        'Plex-Provides          [$http_x_plex_provides]\n'
                        'Plex-Device-Vendor     [$http_x_plex_device_vendor]\n'
                        'Plex-Model             [$http_x_plex_model]\n';


# Setup some variables to use in our cache keys
map $args $x_plex_client_identifier {
        "~(^|&)X-Plex-Client-Identifier=(?<temp>[^&]+)"  $temp;
}

map $args $x_plex_token {
        "~(^|&)X-Plex-Token=(?<temp>[^&]+)"  $temp;
}

map $args $content_directory_id {
        "~(^|&)contentDirectoryID=(?<temp>[^&]+)"  $temp;
}

#
map $args $include_user_state {
        "~(^|&)includeUserState=(?<temp>[^&]+)"  $temp;
}



# Upstream servers section
upstream plex_upstream {
        server 127.0.0.1:32400;
}

upstream tautulli_upstream {
        server 127.0.0.1:8181;
}

#Cache
proxy_cache_path /tmp/nginx_cache/images levels=1:2 keys_zone=plex-images:100m max_size=50G;
proxy_cache_path /tmp/nginx_cache/meta levels=1:2 keys_zone=plex-meta:100m max_size=50G;

server {

        listen 80 default deferred;
        server_name example.com;

        return 301 https://$http_host:443$request_uri;
}


server {
        listen 443 ssl http2 default deferred;
        server_name example.com;

        # path to fullchain.pem on local machine
        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot

        # path to privkey.pem
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot

        gzip on;
        gzip_vary on;
        gzip_min_length 1000;
        gzip_proxied any;
        gzip_types text/plain text/css text/xml application/xml text/javascript application/x-javascript image/svg+xml;
        gzip_disable "MSIE [1-6]\.";

        # Forward real ip and host to Plex
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Sec-WebSocket-Extensions $http_sec_websocket_extensions;
        proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key;
        proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version;

        # Websockets
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";

        location / {
                proxy_pass https://plex_upstream;

                #access_log off;
                access_log /var/log/nginx/plex-access.log plex;
                error_log /var/log/nginx/plex-error.log;

# Possible way to break cache
#               location ~ /library/(metadata|collections)/[0-9]+/userState {
#                       rewrite ^ https://$http_host:443$uri?includUserState=1 permanent;
#               }

                location /photo {
                        access_log                      /var/log/nginx/plex-cache-images.log plex_cache_images;
                        proxy_ignore_headers            Cache-Control;
                        add_header X-Cache-Status       $upstream_cache_status;
                        add_header X-Cache-Date         $upstream_http_date;
                        add_header Pragma               "public";
                        add_header Cache-Control        "public";
                        expires                         1w;
                        proxy_cache                     plex-images;
                        proxy_cache_key                 $arg_url$arg_width$arg_height;
                        proxy_cache_valid               200 301 302 1d;
                        proxy_pass                      https://plex_upstream;
                }

                location /hubs/promoted {
                        access_log                      /var/log/nginx/plex-cache-meta.log plex_cache_meta;
                        proxy_ignore_headers            Cache-Control;
                        add_header X-Cache-Status       $upstream_cache_status;
                        add_header X-Cache-Date         $upstream_http_date;
                        add_header Pragma               "public";
                        add_header Cache-Control        "public";
                        expires                         1h;
                        proxy_cache                     plex-meta;
                        proxy_cache_key                 meta-hubs-promoted$uri$x_plex_token$content_directory_id;
                        proxy_cache_valid               200 301 302 15m;
                        proxy_cache_bypass              $include_user_state;
                        proxy_pass                      https://plex_upstream;
                }

# Disabled until I find a way to break the cache when a watch status is changed
#               location ~ /library/(metadata|collections)/[0-9]+ {
#                       access_log                      /var/log/nginx/plex-cache-meta.log plex_cache_meta;
#                       proxy_ignore_headers            Cache-Control;
#                       add_header X-Cache-Status       $upstream_cache_status;
#                       add_header X-Cache-Date         $upstream_http_date;
#                       add_header Pragma               "public";
#                       add_header Cache-Control        "public";
#                       expires                         1h;
#                       proxy_cache                     plex-meta;
#                       proxy_cache_key                 meta-item$uri$x_plex_token$content_directory_id;
#                       proxy_cache_valid               200 301 302 1h;
#                       proxy_cache_bypass              $include_user_state;
#                       proxy_pass                      https://plex_upstream;
#               }

                location ~ /library/(metadata|collections)/[0-9]+/(children|related|similar|items)$ {
                        access_log                      /var/log/nginx/plex-cache-meta.log plex_cache_meta;
                        proxy_ignore_headers            Cache-Control;
                        add_header X-Cache-Status       $upstream_cache_status;
                        add_header X-Cache-Date         $upstream_http_date;
                        add_header Pragma               "public";
                        add_header Cache-Control        "public";
                        expires                         1h;
                        proxy_cache                     plex-meta;
                        proxy_cache_key                 meta-related$uri$x_plex_token$content_directory_id;
                        proxy_cache_valid               200 301 302 15m;
                        proxy_cache_bypass              $include_user_state;
                        proxy_pass                      https://plex_upstream;
                }

                location ~ /library/(metadata|collections)/[0-9]+/(extras|theme|thumb)$ {
                        access_log                      /var/log/nginx/plex-cache-meta.log plex_cache_meta;
                        proxy_ignore_headers            Cache-Control;
                        add_header X-Cache-Status       $upstream_cache_status;
                        add_header X-Cache-Date         $upstream_http_date;
                        add_header Pragma               "public";
                        add_header Cache-Control        "public";
                        expires                         1w;
                        proxy_cache                     plex-meta;
                        proxy_cache_key                 meta-extras$uri$x_plex_token$content_directory_id;
                        proxy_cache_valid               200 301 302 1d;
                        proxy_cache_bypass              $include_user_state;
                        proxy_pass                      https://plex_upstream;
                }

                location ~* \.(html|css|jpg|gif|ico|js)$ {
                        access_log                      /var/log/nginx/plex-cache-meta.log plex_cache_meta;
                        proxy_ignore_headers            Cache-Control;
                        add_header X-Cache-Status       $upstream_cache_status;
                        add_header X-Cache-Date         $upstream_http_date;
                        add_header Pragma               "public";
                        add_header Cache-Control        "public";
                        expires                         1d;
                        proxy_cache                     plex-meta;
                        proxy_cache_key                 static$uri;
                        proxy_cache_valid               200 301 302 1d;
                        proxy_pass                      https://plex_upstream;
                }
        }
}

My next step is to try and make the cache buckets a bit broader by playing with the proxy_cache_keys so it can cache for everyone as a whole instead of for each user on things like posters and what not. Currently it is using the $args as part of it which is not terribly effective, but will be refined as time permits.

You could extend the validity time if you enable proxy_cache_revalidate. Also I’d restrict to proxy_cache_methods to GET

Thanks for the info, I like the idea of revalidate, however, I wonder if Plex actually uses the proper headers to let it know if it can revalidate. I guess it can not hurt as if it does not then it would be the same as it is now.

I do believe proxy_cache is limited to HEAD and GET calls by default, any particular reason to refine it to just GET?

oh, ok. yeah nah, then you don’t need that.