> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/getting-started/reverse-proxy-and-domains.md).

# Reverse Proxy & Domains

MyIP serves plain HTTP on port `18966` and does not terminate TLS. For any public deployment, put a reverse proxy in front of it.

## Proxying to MyIP

There is nothing unusual to configure. Point your proxy at port `18966` and you are done:

* **No WebSocket upgrade handling is needed.** MyIP's backend is plain HTTP.
* **No `try_files` or SPA rewrite rules.** The frontend server already falls back to `index.html` for client routes like `/tools/whois`.
* **No `/api` special-casing.** The same server proxies `/api` to the backend internally.
* **Do not add proxy-level caching.** MyIP sets its own `Cache-Control` per asset class — hashed assets are immutable for a year, `index.html` is revalidated, `/api/*` defaults to `no-store`. Overriding that will serve stale pages after a deploy.

{% tabs %}
{% tab title="Nginx" %}
{% code title="/etc/nginx/sites-available/myip" %}

```nginx
server {
    listen 443 ssl http2;
    server_name myip.example.com;

    ssl_certificate     /etc/letsencrypt/live/myip.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myip.example.com/privkey.pem;

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

        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;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Caddy" %}
{% code title="Caddyfile" %}

```
myip.example.com {
    reverse_proxy 127.0.0.1:18966
}
```

{% endcode %}

Caddy handles TLS certificates and sets the `X-Forwarded-*` headers for you.
{% endtab %}
{% endtabs %}

### Headers that matter

The backend runs with Express's `trust proxy` set to one hop, and resolves the client IP in this order:

1. `CF-Connecting-IP` (Cloudflare)
2. the first entry of `X-Forwarded-For`
3. `CF-Connecting-IPv6`
4. the socket address

That address is what rate limiting and the blocked-IP ledger record. If your proxy does not forward `X-Forwarded-For`, every visitor looks like one client and rate limits apply to all of them together. See [Security Options](/developer/configuration/security-options.md).

{% hint style="info" %}
**Request body size.** Shared diagnostic reports post up to 500 KB to `/api/report`, and if you enable the Sentry frontend tunnel, `/api/monitoring` accepts envelopes up to 10 MB. If you tighten `client_max_body_size` (Nginx defaults to 1 MB), keep it above those limits.
{% endhint %}

## `ALLOWED_DOMAINS` — required on a real domain

Every `/api/*` route is behind a `Referer` check. The request is rejected with **403** unless the `Referer` header's hostname is `localhost` or appears in `ALLOWED_DOMAINS`.

This is what stops other sites from embedding your instance and using your API keys and your rate limits. It is also the single most common self-hosting mistake: the app loads, then every tool fails.

{% hint style="danger" %}
If you serve MyIP at `https://myip.example.com` and leave `ALLOWED_DOMAINS` empty, **the entire API returns 403**. The page renders, and nothing on it works.
{% endhint %}

```bash
ALLOWED_DOMAINS="myip.example.com,www.myip.example.com"
```

Exact behavior, so you can get it right the first time:

| Rule                | Detail                                                                                                  |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| Separator           | Comma. **No spaces** — entries are matched literally, and `" b.com"` never matches anything.            |
| Matching            | Exact hostname. `example.com` does **not** allow `sub.example.com`, and vice versa.                     |
| `www`               | A separate hostname. List both if both are reachable.                                                   |
| Port and path       | Ignored. `example.com` covers `https://example.com:8443/anything`.                                      |
| `localhost`         | Always allowed, whatever you configure.                                                                 |
| Bare IP address     | Treated as a hostname. To reach the app at `http://192.168.1.10:18966`, add `192.168.1.10` to the list. |
| Missing `Referer`   | Rejected with `{"error":"What are you doing?"}`.                                                        |
| Disallowed hostname | Rejected with `{"error":"Access denied"}`.                                                              |

{% hint style="info" %}
A missing `Referer` is always a rejection, which is why `curl https://myip.example.com/api/...` returns 403 by design. The API is meant for the app's own frontend, not for direct scripting.
{% endhint %}

## curl-friendly IP endpoints

MyIP has a "Command Line API" panel that shows visitors a `curl` one-liner for checking their IP from a terminal. The optional `/geo` path adds geolocation to the answer:

```bash
curl 4.example.com
curl 4.example.com/geo
```

Three variables control which hostnames the panel prints:

| Variable                 | Panel entry                                 |
| ------------------------ | ------------------------------------------- |
| `VITE_CURL_IPV4_DOMAIN`  | Get the machine's IPv4 address              |
| `VITE_CURL_IPV6_DOMAIN`  | Get the machine's IPv6 address              |
| `VITE_CURL_IPV64_DOMAIN` | Get the machine's preferred network exit IP |

Point them at hostnames that resolve accordingly — A-only, AAAA-only, and dual-stack.

{% hint style="warning" %}
**All three are required.** The frontend only shows the curl panel when every one of them is set; if any is empty, the feature stays hidden and the dialog says it is unavailable.
{% endhint %}

{% hint style="warning" %}
**These are build-time variables.** Like every `VITE_*` variable, they are baked into the JavaScript bundle by Vite. Passing them to the prebuilt Docker image at runtime does nothing — set them in `.env` before `pnpm run build`, or before `docker build` on your own image. See [Deploy with Node.js](/developer/getting-started/deploy-with-nodejs.md).
{% endhint %}

{% hint style="info" %}
**MyIP does not implement the `/geo` endpoint.** These variables only tell the frontend which hostnames to print in the curl panel. The service answering `4.example.com/geo` is something you run separately. Leave the variables empty and the feature simply stays off — see [Features Tied to IPCheck.ing](/developer/configuration/features-tied-to-ipcheck-ing.md).
{% endhint %}

## Troubleshooting

<details>

<summary>The page loads but every tool shows an error</summary>

Almost always `ALLOWED_DOMAINS`. Open your browser's network tab and look for 403 responses on `/api/*`. The response body tells you which case you hit:

* `{"error":"Access denied"}` — the hostname is not in the list. Add the exact hostname you type in the address bar.
* `{"error":"What are you doing?"}` — no `Referer` reached the backend. Check that your proxy or a privacy extension is not stripping it.

Restart the backend after changing `ALLOWED_DOMAINS`.

</details>

<details>

<summary>Rate limits trigger for everyone at once</summary>

Your proxy is not forwarding the real client IP, so all traffic collapses onto one address. Add `X-Forwarded-For` (see the Nginx example above) and restart.

</details>

<details>

<summary>Stale content after an upgrade</summary>

Check for caching added at the proxy or CDN. MyIP already sets appropriate `Cache-Control` headers; if your proxy caches `index.html` for longer, visitors keep loading a build whose asset files no longer exist.

</details>

## Next steps

* [Security Options](/developer/configuration/security-options.md) — rate limiting, slow-down, blocked-IP logging
* [Environment Variables](/developer/reference/environment-variables.md)
* [API Endpoints](/developer/reference/api-endpoints.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/getting-started/reverse-proxy-and-domains.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
