A tool to help purge Nginx cache.
Find a file
James Coleman 702cbb36bb Add service management, harden the purge server, and modernize the build.
Server:
- Add a --cache-path allowlist so a server can be limited to the caches it
  is meant to purge, defaulting to any path as before.
- Set socket permissions explicitly (--socket-mode, default 0660) instead of
  inheriting the service manager's umask, which left the socket unreachable.
- Refuse to remove a socket another instance is still serving.
- Read keys both raw and decoded, so keys nginx stored with escapes and keys
  a caller escaped by hand both purge.
- Add an exact= parameter for literal keys containing glob punctuation.
- Report purge failures as 500 rather than 502, and send error bodies through
  http.Error so a failure is not reported as a successful purge.
- Graceful shutdown with systemd readiness notification.

Purge:
- Group purge arguments into PurgeRequest and report the number of entries
  removed.
- Compile exclude globs once, and fail the purge when one is invalid rather
  than purging the keys it was meant to keep.
- Cap header scanning and tolerate entries nginx evicts mid-walk.
- Switch to filepath.WalkDir to avoid an Lstat per cache file.

New:
- service command to install, start, stop, and remove the system service.
- Makefile, VERSION, and build identifiers stamped via ldflags.
- Tests for the server handler.

Build:
- Update to Go 1.25, kong v1, GoReleaser v2, and current GitHub Actions.
- Add vet and test steps to CI.
- Rename purgeCmd.go/serverCmd.go to Go's file naming convention.

Bump version to 0.2.0.
2026-08-12 14:44:33 -05:00
.github/workflows Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
.gitignore First commit 2024-08-01 02:09:30 -05:00
.goreleaser.yaml Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
flags.go Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
go.mod Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
go.sum Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
info.go Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
LICENSE.txt Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
main.go Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
Makefile Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
purge_cmd.go Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
README.md Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
server_cmd.go Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
server_cmd_test.go Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
service_cmd.go Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00
VERSION Add service management, harden the purge server, and modernize the build. 2026-08-12 14:44:33 -05:00

nginx-cache-purge

A tool to help purge Nginx cache. It can either run locally with the purge command, or run as a local unix service to allow for purging by Nginx http requests. The tool supports using wildcard/glob syntax in the purge key to match multiple keys from the cache.

Install

You can install either by downloading the latest binary release, or by building.

Building

Building should be as simple as running:

make

Usage

The following are some examples of ways to purge cache

Purge a specific key

$ nginx-cache-purge purge /var/nginx/proxy_temp/cache example.com/index.html

Purge all keys for a domain

$ nginx-cache-purge purge /var/nginx/proxy_temp/cache 'example.com/*'

Purge all keys for jpeg and png files

$ nginx-cache-purge purge /var/nginx/proxy_temp/cache 'example.com/*.{jpg,jpeg,png}'

Purge all keys

$ nginx-cache-purge purge /var/nginx/proxy_temp/cache '*'

Purge a key that contains wildcard characters

Whether a key is a wildcard is otherwise guessed from the punctuation in it, and real cache keys carry that punctuation: a request URI with a query string puts ? in the key, and PHP-style array parameters put [ and ]. --exact says the key is a literal, for both the key and any excludes.

$ nginx-cache-purge purge --exact /var/nginx/proxy_temp/cache 'example.com/list.php?f[]=x'

Running as a service

If you want to run as a service to allow purge requests via http requests, the service can install itself:

nginx-cache-purge service install
nginx-cache-purge service start

service also accepts stop, restart, status, and uninstall. The installed unit runs nginx-cache-purge server as a notify service, creates the runtime directory the socket lives in, and restarts on failure.

Connecting to a UNIX socket needs write permission on it, so the socket is created mode 0660: the purge server and Nginx have to run as the same user, or share a group. The installed unit runs as root, so either add a User=/Group= drop-in for it, or widen the socket with --socket-mode.

Restricting which caches may be purged

The directory to purge comes from the request, and the purge deletes what it finds under it, so a server left unrestricted will purge any path its user can reach. Pass --cache-path to name the caches it may serve, repeating it for more than one:

ExecStart=/usr/local/bin/nginx-cache-purge server --cache-path /var/nginx/proxy_temp/cache

A request naming any other directory is refused with 403. Directories inside a named cache are allowed, and symlinks are resolved, so a link to a named cache is recognised as that cache. Naming no path leaves every path purgeable, which is what a server without the flag has always done.

The installed unit runs nginx-cache-purge server with no arguments, so add the flag with a drop-in or write the unit yourself.

If you'd rather write the unit yourself, place the following in /etc/systemd/system/nginx-cache-purge.service.

[Unit]
Description=Nginx Cache Purge
After=network.target
 
[Service]
User=nginx
Group=nginx
RuntimeDirectory=nginx-cache-purge
ExecStart=/usr/local/bin/nginx-cache-purge server --cache-path /var/nginx/proxy_temp/cache
Restart=always
RestartSec=3s
 
[Install]
WantedBy=multi-user.target

You can then run the following to start the service:

systemctl daemon-reload
systemctl start nginx-cache-purge.service

Nginx config

If you want to purge via Nginx http requests, you'll need to add configuration to your Nginx config file.

The server reads four query parameters:

Parameter Description
path Path to the cache directory, the same one given to proxy_cache_path.
key Cache key or wildcard match, the same one built by proxy_cache_key.
exclude Key to keep, can be a wildcard. Repeat it to exclude more than one.
exact Read the key and excludes as literals rather than wildcards.

Nginx substitutes $request_uri into the rewrite without escaping it, so a key containing % or + arrives as the literal bytes Nginx stored it under, and that is what is purged. A key escaped by a caller writing the request itself is purged too, so either convention works.

Literal keys and wildcards

A key built from $request_uri is a literal, and a request URI routinely holds the punctuation that would otherwise mark the key as a wildcard: ? from a query string, [ and ] from PHP-style array parameters. Read as wildcards those keys purge the wrong entries, fail outright, or match nothing while still answering PURGED and leaving the stale entry served. The examples below therefore pass exact=1, which is what you want when a PURGE request names one URL.

Leave exact off where the purge is meant to take a wildcard, such as the /purge(/.*) location further down, where a request for /purge/images/* clears everything under /images/.

Because $request_uri carries the client's own query string into the purge parameters, put exact=1 before key= in the rewrite. Parameters are taken first-wins, so an exact=0 a client appends to its request URI arrives second and is ignored. The same ordering already protects path. Note that exclude is collected rather than taken first-wins, so a client can append an exclude that holds back its own purge.

Map PURGE requests

http {
    map $request_method $is_purge {                                                             
        default   0;
        PURGE     1;
    }

    proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
    proxy_cache_key $server_name$request_uri;

    server {
        location / {
            proxy_cache_bypass $is_purge;
            if ($is_purge) {
                proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
                rewrite ^ /?path=/var/nginx/proxy_temp/cache&exact=1&key=$server_name$request_uri break;
            }

            proxy_cache my_cache;
            proxy_pass http://upstream;
        }
    }
}
http {
    map $cookie_purge_token $is_purge {
        default 0;
        nnCgKUx1p2bIABXR 1;
    }

    proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
    proxy_cache_key $server_name$request_uri;

    server {
        location / {
            proxy_cache_bypass $is_purge;
            if ($is_purge) {
                proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
                rewrite ^ /?path=/var/nginx/proxy_temp/cache&exact=1&key=$server_name$request_uri break;
            }

            proxy_cache my_cache;
            proxy_pass http://upstream;
        }
    }
}

Auth via header

http {
    map $http_purge_token $is_purge {
        default 0;
        nnCgKUx1p2bIABXR 1;
    }

    proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
    proxy_cache_key $server_name$request_uri;

    server {
        location / {
            proxy_cache_bypass $is_purge;
            if ($is_purge) {
                proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
                rewrite ^ /?path=/var/nginx/proxy_temp/cache&exact=1&key=$server_name$request_uri break;
            }

            proxy_cache my_cache;
            proxy_pass http://upstream;
        }
    }
}

Auth via header and IP white list.

http {
    map $http_purge_token $is_purge {
        default 0;
        nnCgKUx1p2bIABXR 1;
    }

    geo $purge_allowed {
        default 0;
        127.0.0.1 1;
        192.168.0.0/24 1;
    }

    proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
    proxy_cache_key $server_name$request_uri;

    server {
        location / {
            set $should_purge $purge_allowed;
            if ($is_purge != 1) {
                set $should_purge 0;
            }
            proxy_cache_bypass $should_purge;
            if ($should_purge) {
                proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
                rewrite ^ /?path=/var/nginx/proxy_temp/cache&exact=1&key=$server_name$request_uri break;
            }

            proxy_cache my_cache;
            proxy_pass http://upstream;
        }
    }
}

Using IP whitelists

This location takes a wildcard, so it leaves exact off: a request for /purge/images/* clears everything under /images/.

http {
    proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
    proxy_cache_key $server_name$request_uri;

    server {
        location / {
            proxy_cache my_cache;
            proxy_pass http://upstream;
        }
        location ~ /purge(/.*) {
            allow 127.0.0.1;
            deny all;
            proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
            rewrite ^ /?path=/var/nginx/proxy_temp/cache&key=$server_name$1 break;
        }
    }
}