Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/binary-person/rammerhead/llms.txt

Use this file to discover all available pages before exploring further.

Running Rammerhead behind nginx (or another reverse proxy) requires a small amount of extra configuration. Because Rammerhead uses testcafe-hammerhead to rewrite every URL it proxies, it must know the public hostname, port, and protocol that clients see — not the internal address it binds to. Without this, hammerhead generates rewritten URLs pointing at localhost, breaking the proxy for end users.

Why getServerInfo matters

Hammerhead constructs all rewritten URLs from a ServerInfo object. Rammerhead builds that object by calling the getServerInfo function in config.js on every incoming request. The default implementation hard-codes localhost:8080:
// src/config.js (default)
getServerInfo: () => ({
  hostname: 'localhost',
  port: 8080,
  crossDomainPort: 8081,
  protocol: 'http:',
}),
This works fine when clients connect directly. Behind a reverse proxy, clients reach Rammerhead through the proxy’s public hostname and port, so you need to return those values instead.

Overriding with request headers

nginx forwards the original Host header to the upstream, so you can read it from req.headers.host and return the correct public values dynamically:
// config.js (root of repository)
module.exports = {
  getServerInfo: (req) => {
    return {
      hostname: new URL('http://' + req.headers.host).hostname,
      port: 443,
      crossDomainPort: 8443,
      protocol: 'https:',
    };
  },
};
Hard-code port and protocol to the values your proxy exposes to the public. The req.headers.host approach for hostname makes the config portable across multiple domain names or subdomains without changes.

Cross-domain port

Hammerhead uses a second HTTP port (crossDomainPort) to simulate cross-origin requests accurately, which matters for sites that inspect the Origin header. By default, Rammerhead listens on 8081 for this purpose. Behind a reverse proxy, you need to either:
  • Expose a second public port (e.g., 8443) and proxy it to 8081
  • Or set crossDomainPort: null to use a single port (see the note on single-port mode below)
In the getServerInfo example above, crossDomainPort: 8443 tells hammerhead to include 8443 in rewritten cross-origin URLs, which nginx then proxies back to Rammerhead’s internal 8081.

Stripping reverse-proxy headers

When clients connect through Cloudflare or another CDN, the proxy injects headers such as CF-Connecting-IP and x-forwarded-for. If Rammerhead forwards these to destination sites, the sites see infrastructure-level metadata instead of the expected request. Strip them with stripClientHeaders:
// config.js
module.exports = {
  // Cloudflare example
  stripClientHeaders: [
    'cf-ipcountry',
    'cf-ray',
    'x-forwarded-proto',
    'cf-visitor',
    'cf-connecting-ip',
    'cdn-loop',
    'x-forwarded-for',
  ],
};

Reading the real client IP

When Rammerhead sits behind a proxy, req.socket.remoteAddress resolves to the proxy’s address, not the client’s. Override getIP to read the forwarded header:
// config.js
module.exports = {
  getIP: (req) =>
    (req.headers['x-forwarded-for'] || req.connection.remoteAddress || '')
      .split(',')[0]
      .trim(),
};

Rewriting response headers

Some destination sites return headers like X-Frame-Options that prevent their pages from loading inside the proxy. Use rewriteServerHeaders to remove or replace them in Rammerhead’s responses to the browser:
// config.js
module.exports = {
  rewriteServerHeaders: {
    // Set to null to delete the header entirely
    'x-frame-options': null,
    // Or pass a function to transform the original value
    // 'x-frame-options': (original) => 'SAMEORIGIN',
  },
};

nginx configuration

The following example terminates TLS at nginx and proxies both the main port and the cross-domain port to Rammerhead running on 127.0.0.1:
# /etc/nginx/sites-available/rammerhead

# Main proxy port (public: 443 → internal: 8080)
server {
    listen 443 ssl;
    server_name proxy.example.com;

    ssl_certificate     /etc/ssl/certs/proxy.example.com.crt;
    ssl_certificate_key /etc/ssl/private/proxy.example.com.key;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $remote_addr;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";

        proxy_read_timeout 86400;
    }
}

# Cross-domain port (public: 8443 → internal: 8081)
server {
    listen 8443 ssl;
    server_name proxy.example.com;

    ssl_certificate     /etc/ssl/certs/proxy.example.com.crt;
    ssl_certificate_key /etc/ssl/private/proxy.example.com.key;

    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_http_version 1.1;

        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $remote_addr;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";

        proxy_read_timeout 86400;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name proxy.example.com;
    return 301 https://$host$request_uri;
}
The proxy_read_timeout 86400 value (24 hours) is intentionally long. Rammerhead proxies WebSocket connections that can stay open for extended periods. Shortening this timeout will disconnect long-lived sessions.

SSL termination

Single-port mode

If you cannot expose a second port, set crossDomainPort: null. Rammerhead will use a single HTTP server. Some sites that check the Origin header may behave differently, but most will work correctly:
// config.js
module.exports = {
  crossDomainPort: null,
  getServerInfo: (req) => ({
    hostname: new URL('http://' + req.headers.host).hostname,
    port: 443,
    crossDomainPort: null,
    protocol: 'https:',
  }),
};
When crossDomainPort is null, _rewriteServerInfo in RammerheadProxy falls back to the main port for cross-domain requests, so crossDomainPort in getServerInfo can also be omitted or set to null.

Build docs developers (and LLMs) love