> 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/reference/faq.md).

# FAQ

Every answer below describes actual behaviour in the code, not guesses. If something here does not match your instance, you are probably on a different version.

## Deployment

<details>

<summary>Every API call returns 403 "Access denied" or "What are you doing?"</summary>

The global referer gate rejected the request. Two distinct messages:

* `{"error":"What are you doing?"}` — the request carried **no** `Referer` header.
* `{"error":"Access denied"}` — a `Referer` was sent, but its hostname is not allowed.

Allowed hostnames are `localhost` plus everything in `ALLOWED_DOMAINS`. Set it to the hostname(s) your users actually load the site from:

{% code title=".env" %}

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

{% endcode %}

Then restart the backend. Three things trip people up:

1. **Matching is exact.** `example.com` does not cover `sub.example.com`. List each hostname.
2. **Raw IP access counts as a hostname.** Browsing to `http://192.168.1.10:18966` sends that IP as the referer hostname — add it, or use a hostname.
3. **Port and scheme are irrelevant**, only the hostname is compared.

See [Security Options](/developer/configuration/security-options.md).

</details>

<details>

<summary>My VITE_* variable has no effect after restarting the container</summary>

`VITE_*` variables are read by Vite during `pnpm run build` and **inlined into the JavaScript bundle**. They are not read at runtime. Restarting the server cannot change a value that is already compiled into `dist/`.

The official `jason5ng32/myip:latest` image was built without your values — `.env` is in `.dockerignore`, so no `.env` was present during that build either. Passing `-e VITE_CURL_IPV4_DOMAIN=...` to the prebuilt image does nothing to the frontend. To use build-time variables in Docker you must build your own image. In a Node deployment, re-run `pnpm run build`.

Two `VITE_*` variables are also read at runtime and *do* respond to `-e`: `VITE_SENTRY_DSN_FRONTEND` (which mounts `/api/monitoring`) and `VITE_SITE_URL` (which builds the upstream `User-Agent`). Full breakdown in [Environment Variables](/developer/reference/environment-variables.md).

</details>

<details>

<summary>Port 18966 is already in use, or I want different ports</summary>

MyIP runs two listeners: the static / SPA server on `FRONTEND_PORT` (default `18966`) and the API server on `BACKEND_PORT` (default `11966`). The frontend server proxies `/api` to the backend, so only the frontend port needs to be reachable from outside.

**Node deployment** — set `BACKEND_PORT` and `FRONTEND_PORT` in `.env` and restart. Both are read by `backend-server.js`, `frontend-server.js` and `vite.config.js`, so changing one without the other breaks the proxy.

**Docker** — do not change the container's internal port; remap on the host side instead. The image `EXPOSE`s `18966`:

{% code title="shell" %}

```bash
docker run -d -p 8080:18966 --name myip --restart always jason5ng32/myip:latest
```

{% endcode %}

</details>

<details>

<summary>The curl API card never appears</summary>

The `curlDomainsHadSet` getter in `frontend/store.js` ANDs the three domains together, so the card renders only when **all three** are non-empty. Setting one or two shows nothing. Set `VITE_CURL_IPV4_DOMAIN`, `VITE_CURL_IPV6_DOMAIN` and `VITE_CURL_IPV64_DOMAIN` together — and remember they are `VITE_*`, so they need a rebuild, not just a restart.

These variables only supply hostnames to display. MyIP does not serve those endpoints; you point the DNS records at your own plain-text IP echo service.

</details>

<details>

<summary>Users are getting 429 "Too Many Requests"</summary>

`SECURITY_RATE_LIMIT` is set and the client crossed it. The window is **20 minutes** per client IP, and exceeding it returns `429 {"message":"Too Many Requests"}`.

{% hint style="info" %}
The startup banner reads `🛡️ Rate limiter enabled — N requests per 60 minutes`, but the configured window in `backend-server.js` is `20 * 60 * 1000` ms. The log message is wrong; 20 minutes is the real window.
{% endhint %}

Raise the number, or set it to `0` to disable the limiter entirely. `SECURITY_DELAY_AFTER` is a separate, gentler mechanism — it never rejects, it just adds `hits × 400 ms` of latency after N requests in a **60-minute** window.

A single MyIP page load fires many `/api` requests, so a low limit will hit ordinary users. The logs carry an `IP rate-limited` warning with the offending IP — once per transition into the limited state, not per blocked request. Set `SECURITY_BLACKLIST_LOG_FILE_PATH` if you also want a persistent on-disk ledger.

</details>

## MaxMind

<details>

<summary>The logs say "MaxMind API will return 503 until databases are loaded successfully"</summary>

The backend could not open `common/maxmind-db/GeoLite2-City.mmdb` and `GeoLite2-ASN.mmdb`. It starts anyway, but `GET /api/maxmind` answers 503 and the country badges throughout the UI stay empty.

You usually see this warning first:

{% code title="log" %}

```
⚠️  MaxMind databases are missing and MAXMIND_ACCOUNT_ID / MAXMIND_LICENSE_KEY are not configured.
  Set the credentials in .env and restart, or drop GeoLite2-City.mmdb + GeoLite2-ASN.mmdb
  into common/maxmind-db/. Starting server anyway; MaxMind API will return 503 until
  databases are available.
```

{% endcode %}

The fix is exactly what the message says — set credentials, or pre-seed the files:

{% code title=".env" %}

```bash
MAXMIND_ACCOUNT_ID="your-account-id"
MAXMIND_LICENSE_KEY="your-license-key"
MAXMIND_AUTO_UPDATE="true"
```

{% endcode %}

Success looks like `📦 MaxMind databases loaded (startup)`. Full walkthrough in [MaxMind Setup](/developer/getting-started/maxmind-setup.md).

</details>

<details>

<summary>A fresh Docker container has an empty maxmind-db directory</summary>

That is deliberate. GeoLite2 databases cannot be redistributed under MaxMind's license, and `.dockerignore` excludes `common/maxmind-db/*.mmdb` so a local build never bakes in files a CI build would not have.

Docker deployers therefore must use the credential path — pass `MAXMIND_ACCOUNT_ID`, `MAXMIND_LICENSE_KEY` and `MAXMIND_AUTO_UPDATE="true"` with `-e`. Without them the container starts, serves the UI, and 503s on `/api/maxmind` on every boot.

The first download happens during boot and is capped at 5 minutes. If it times out, the server still listens — check the logs and restart. See [Deploy with Docker](/developer/getting-started/deploy-with-docker.md).

</details>

<details>

<summary>MAXMIND_AUTO_UPDATE is "false" but the databases downloaded anyway</summary>

Working as intended. `MAXMIND_AUTO_UPDATE` gates **only the periodic scheduler**. The boot-time "download if missing" path does not consult it: if valid credentials are present and the `.mmdb` files are absent, one download cycle runs. The reasoning is that credentials in `.env` already express the intent "I want MaxMind working".

With `MAXMIND_AUTO_UPDATE="true"` you additionally get a first check 60 s after startup and a refresh every 24 h. `CAIDA_AUTO_UPDATE` behaves the same way for the CAIDA datasets.

</details>

## Network & API

<details>

<summary>curl against /api/... returns 403, but the site works in a browser</summary>

curl sends no `Referer` header, so the global gate answers `403 {"error":"What are you doing?"}`. This is not a bug — it is the whole point of the gate.

To test an endpoint by hand, supply an allowed referer:

{% code title="shell" %}

```bash
curl -H "Referer: http://localhost/" http://localhost:11966/api/configs
```

{% endcode %}

Use a hostname listed in `ALLOWED_DOMAINS` (or `localhost`, which is always allowed) and hit the backend port directly.

</details>

<details>

<summary>Some IP data sources are missing from the UI</summary>

The frontend hides sources whose API key the backend does not have. `GET /api/configs` returns one boolean per feature — never the key values. Fetch it (with a valid `Referer`) to see exactly what your instance thinks is configured: `map` needs `GOOGLE_MAP_API_KEY`, `ipapiis` needs `IPAPIIS_API_KEY`, `cloudFlare` needs `CLOUDFLARE_API_KEY`, and so on. The full field list is in [API Endpoints](/developer/reference/api-endpoints.md); the keys are in [Optional API Keys](/developer/configuration/optional-api-keys.md).

Note that this route is edge-cached for 1 hour, so a newly added key may take that long to appear behind a CDN.

</details>

<details>

<summary>/api/ipapiis or /api/ip2location returns 500</summary>

Those two handlers build their upstream URL by calling `.split(',')` on the key with no null check, so an unset key throws and produces a 500 rather than a clean error. Set `IPAPIIS_API_KEY` or `IP2LOCATION_API_KEY`.

In normal use you never see this: `/api/configs` reports the source as unavailable and the frontend does not call it.

</details>

<details>

<summary>Report sharing returns 503 "Report sharing is not configured"</summary>

`POST /api/report` and `GET /api/report/:id` require **all three** Cloudflare Workers KV variables: `CLOUDFLARE_API_KEY`, `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_KV_NAMESPACE_ID`. Missing any one of them means 503 on both routes and `reportSharing: false` in `/api/configs`, which hides the share UI entirely.

Two common mistakes: the token needs the **Workers KV Storage: Edit** permission (the plain Radar token is not enough), and `CLOUDFLARE_KV_NAMESPACE_ID` is the namespace's **hex ID** from the dashboard, not its display name.

</details>

<details>

<summary>Frontend Sentry events 404 against /api/monitoring</summary>

`backend-server.js` mounts the tunnel route only when `VITE_SENTRY_DSN_FRONTEND` is set **on the server process**. If you baked the DSN into a self-built image at build time but did not also pass it at runtime, the bundle posts envelopes to a route that was never mounted.

Pass the same value in both places. See [Error Monitoring (Sentry)](/developer/configuration/error-monitoring.md).

</details>

## Development

<details>

<summary>npm install breaks the project</summary>

MyIP is **pnpm only**. The version is pinned via `packageManager` in `package.json`, `pnpm-lock.yaml` is committed, and `pnpm-workspace.yaml` carries the install-script approvals that native dependencies need. npm or yarn would produce a competing lockfile and miss those approvals.

{% code title="shell" %}

```bash
npm install -g pnpm
pnpm install && pnpm run build
```

{% endcode %}

The Dockerfile does the same thing via `corepack enable`, which provisions the exact pinned pnpm version.

</details>

<details>

<summary>The dev server can't reach the backend</summary>

`pnpm dev` runs Vite and the backend concurrently. Vite serves the frontend on `FRONTEND_PORT` and proxies `/api` to `http://localhost:${BACKEND_PORT}`. If you changed one port but not the other — or set them only in the shell and not in `.env` — the proxy points at nothing.

Both `vite.config.js` and `backend-server.js` read the same two variables from `.env`, so keep them there. See [Dev Environment](/developer/development/dev-environment.md).

</details>

<details>

<summary>Nothing is logged except errors</summary>

`LOG_LEVEL` defaults to `info`, so `debug` lines are suppressed. Per-request HTTP logging is off entirely unless you opt in:

{% code title=".env" %}

```bash
LOG_LEVEL="debug"
LOG_HTTP="true"
```

{% endcode %}

`LOG_HTTP=true` logs one line per `/api/*` request with method, URL and status. It is mounted before the rate limiter, so 429s show up too. Set `LOG_FORMAT="json"` if a log shipper is consuming the output. See [Logging](/developer/configuration/logging.md).

</details>


---

# 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/reference/faq.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.
