12 Commits

5 changed files with 237 additions and 60 deletions

View File

@@ -1,14 +1,11 @@
FROM nginx:alpine
FROM openresty/openresty:alpine
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Copy config (OpenResty uses this path)
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
# HTML and data dirs (config references /usr/share/nginx/html and /etc/nginx/data)
RUN mkdir -p /usr/share/nginx/html /etc/nginx/data/cache /etc/nginx/data/temp
COPY index.html /usr/share/nginx/html/index.html
# Create cache directory
RUN mkdir -p /var/cache/nginx && \
chown nginx:nginx /var/cache/nginx
# Expose port
EXPOSE 3000
# Start nginx
CMD ["nginx", "-g", "daemon off;"]
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]

View File

@@ -1,45 +1,31 @@
# Nginx Cache Proxy
An nginx proxy server that caches requests only when explicitly requested via the `X-Cache: 1` header.
Navigate to `http://localhost:3000/` for interactive usage with URL builder.
## Quick Start
```bash
# Build the image
./build.sh
# Run with docker-compose (uses the built image)
docker-compose up
# Build and run
./build.sh && docker-compose up
# Or run directly
docker run -p 3000:3000 docker.site.quack-lab.dev/nginx-cache-proxy:latest
```
## Configuration
The nginx configuration implements an inverted caching logic:
- **Default behavior**: Requests are NOT cached
- **Cached behavior**: Only requests with `X-Cache: 1` header are cached
## Usage
```bash
# Proxy a URL
# Proxy and cache any URL
curl "http://localhost:3000/?url=https://api.example.com/data"
# Enable caching with X-Cache header
curl -H "X-Cache: 1" "http://localhost:3000/?url=https://api.example.com/data"
# Skip cache for this request
curl -H "X-NoCache: 1" "http://localhost:3000/?url=https://api.example.com/data"
```
## Features
- Cache path: `/var/cache/nginx`
- Cache zone: `api_cache` (10MB)
- Max cache size: 1GB
- Cache validity: 365 days for 200 responses
- Cache status header: `X-Cache-Status` shows cache hit/miss status
- HTTPS support with proper SSL/TLS headers
## In n8n
Add the `X-Cache: 1` header only to requests you want cached. All other requests will bypass the cache completely.
- **Always caches** 2xx status codes for 1 year
- Cache size: 100GB max
- X-Cache-Status header shows cache status
- X-NoCache header bypasses cache
- Interactive web interface at root path

View File

@@ -9,16 +9,11 @@ DOCKER_REPO="docker.site.quack-lab.dev"
IMAGE_NAME="nginx-cache-proxy"
# ============================================================================
# VALIDATE CONFIGURATION
# BUILD PROJECT
# ============================================================================
echo "Validating nginx configuration..."
docker run --rm -v "$(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro" nginx:alpine nginx -t
if [ $? -ne 0 ]; then
echo "Error validating nginx configuration"
exit 1
fi
echo "Building application..."
# No build step needed for nginx
# ============================================================================
# DOCKER BUILD AND TAG

136
index.html Normal file
View File

@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html>
<head>
<title>Nginx Cache Proxy</title>
<style>
:root {
--bg: #ffffff;
--text: #333333;
--heading: #007cba;
--border: #007cba;
--code-bg: #f4f4f4;
--code-inline-bg: #f4f4f4;
--pre-bg: #2d2d2d;
--pre-text: #f8f8f2;
--copy-bg: #e8f4fd;
--copy-border: #007cba;
--success: #28a745;
--error: #dc3545;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #e0e0e0;
--heading: #58a6ff;
--border: #58a6ff;
--code-bg: #2d2d2d;
--code-inline-bg: #3d3d3d;
--pre-bg: #0d0d0d;
--copy-bg: #1c3d5e;
--copy-border: #58a6ff;
--success: #3fb950;
--error: #f85149;
}
}
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; color: var(--text); background: var(--bg); }
h1 { border-bottom: 2px solid var(--border); padding-bottom: 10px; color: var(--text); }
h2 { color: var(--heading); margin-top: 30px; }
h3 { color: var(--text); opacity: 0.8; }
code { background: var(--code-inline-bg); padding: 2px 6px; border-radius: 3px; font-family: "Consolas", "Monaco", monospace; font-size: 0.9em; color: var(--text); }
pre { background: var(--pre-bg); color: var(--pre-text); padding: 15px; border-radius: 5px; overflow-x: auto; }
pre code { background: none; padding: 0; color: inherit; }
.copy-section { background: var(--copy-bg); padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid var(--copy-border); }
.copy-section h3 { margin-top: 0; color: var(--text); }
.copy-section p { color: var(--text); }
button { background: var(--heading); color: white; border: none; padding: 12px 24px; cursor: pointer; border-radius: 5px; font-size: 14px; font-weight: 600; }
button:hover { filter: brightness(0.9); }
.status { margin-top: 10px; font-size: 14px; }
.status.success { color: var(--success); }
.status.error { color: var(--error); }
ul { line-height: 1.8; }
li { color: var(--text); }
</style>
</head>
<body>
<h1>Nginx Cache Proxy</h1>
<p>Proxy and cache any HTTP/HTTPS URL with automatic caching.</p>
<div class="copy-section">
<h3>Quick Copy</h3>
<p>Copy this proxy URL with <code>?url=</code> appended, then paste any URL after it:</p>
<button onclick="copyProxyUrl()">Copy Proxy URL</button>
<div id="status" class="status"></div>
</div>
<h2>Usage Examples</h2>
<h3>Basic Proxy (Cached)</h3>
<pre><code id="basicExample">Loading...</code></pre>
<h3>Skip Cache</h3>
<pre><code id="nocacheExample">Loading...</code></pre>
<h2>Features</h2>
<ul>
<li>Always caches <strong>2xx</strong> status codes for <strong>1 year</strong></li>
<li>Cache size: <strong>100GB</strong> max</li>
<li><code>X-Cache-Status</code> header shows cache hit/miss</li>
<li><code>X-NoCache</code> header bypasses cache</li>
<li>Interactive web interface</li>
</ul>
<script>
function getBaseUrl() {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
const port = window.location.port;
let baseUrl = protocol + '//' + hostname;
if ((protocol === 'http:' && port !== '80' && port !== '') ||
(protocol === 'https:' && port !== '443' && port !== '')) {
baseUrl += ':' + port;
}
return baseUrl;
}
document.addEventListener('DOMContentLoaded', function() {
const base = getBaseUrl();
document.getElementById('basicExample').textContent =
'curl "' + base + '/?url=https://api.example.com/data"';
document.getElementById('nocacheExample').textContent =
'curl -H "X-NoCache: 1" "' + base + '/?url=https://api.example.com/data"';
});
function copyProxyUrl() {
const proxyUrl = window.location.href + '?url=';
const statusEl = document.getElementById('status');
navigator.clipboard.writeText(proxyUrl).then(() => {
statusEl.className = 'status success';
statusEl.textContent = 'Copied: ' + proxyUrl;
}).catch(err => {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = proxyUrl;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
statusEl.className = 'status success';
statusEl.textContent = 'Copied: ' + proxyUrl;
} catch (e) {
statusEl.className = 'status error';
statusEl.textContent = 'Copy failed. URL: ' + proxyUrl;
}
document.body.removeChild(textArea);
});
}
</script>
</body>
</html>

View File

@@ -9,37 +9,100 @@ http {
uwsgi_temp_path /etc/nginx/data/temp;
scgi_temp_path /etc/nginx/data/temp;
proxy_cache_path /etc/nginx/data/cache levels=1:2 keys_zone=api_cache:10m max_size=1g inactive=365d;
proxy_cache_path /etc/nginx/data/cache levels=1:2 keys_zone=api_cache:10m max_size=100g inactive=365d;
error_log /dev/stdout debug;
access_log /dev/stdout;
error_log /dev/stdout warn;
# Use Docker's internal DNS
resolver 127.0.0.11 valid=10s;
log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" cache:$upstream_cache_status';
access_log /dev/stdout main;
resolver 127.0.0.11 valid=60s;
# Raw url= value from request (stops at next & so %26 in encoded URLs is preserved)
map $request_uri $url_encoded {
default "";
"~*[?&]url=((?:[^&%]|%[0-9A-Fa-f][0-9A-f])*)(?:&|$)" $1;
}
server {
listen 3001;
listen 3000;
location / {
if ($arg_url = "") {
return 400 "Missing url parameter";
set $backend_base "";
# Decode url, build upstream URL, strip our "url=". Force that URI to upstream (variable proxy_pass can send original request URI otherwise)
rewrite_by_lua_block {
local enc = ngx.var.url_encoded
local decoded = (enc and enc ~= "") and ngx.unescape_uri(enc) or ngx.var.arg_url or ""
if decoded == "" then
ngx.exec("/index.html")
return
end
local args = ngx.var.args or ""
local rest = args:gsub("^url=[^&]*&?", ""):gsub("&url=[^&]*", ""):gsub("^url=[^&]*$", "")
local full = decoded
if rest ~= "" then
local sep = decoded:find("?") and "&" or "?"
full = decoded .. sep .. rest
end
local scheme, host, pathquery = full:match("^(https?)://([^/]+)(.*)$")
if not host then
ngx.status = 400
ngx.say("invalid url")
return ngx.exit(400)
end
if pathquery == "" then pathquery = "/" end
local path = pathquery:match("^([^?]*)") or "/"
local query = pathquery:match("%?(.*)$") or ""
ngx.var.backend_base = scheme .. "://" .. host
ngx.req.set_uri(path)
ngx.req.set_uri_args((query:gsub("^%?", "")))
}
proxy_pass $arg_url;
proxy_pass $backend_base$uri$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $proxy_host;
proxy_ssl_server_name on;
proxy_cache api_cache;
proxy_cache_valid 200 365d;
proxy_cache_bypass $http_x_cache_inverse;
proxy_no_cache $http_x_cache_inverse;
# Strip upstream CORS so we only send our own (duplicate = browser reject)
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
set $http_x_cache_inverse 1;
if ($http_x_cache) {
set $http_x_cache_inverse 0;
# CORS headers — replace with our own *
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Cache,X-NoCache,X-Status" always;
add_header Access-Control-Expose-Headers "X-Cache-Status" always;
# Handle preflight OPTIONS requests
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_cache api_cache;
proxy_cache_valid 200 201 202 203 204 205 206 207 208 226 365d;
proxy_ignore_headers Cache-Control Expires;
proxy_cache_bypass $http_x_nocache;
proxy_no_cache $http_x_nocache;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status always;
}
location = /index.html {
root /usr/share/nginx/html;
# CORS headers for HTML interface
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type" always;
# Handle preflight OPTIONS requests
if ($request_method = 'OPTIONS') {
return 204;
}
}
}
}