> 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

Answers to the most common deployment and configuration problems.

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 entry never appears</summary>

The `curlDomainsHadSet` getter in `frontend/store.js` ANDs the three domains together, so the footer lists the Command Line API entry 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>

First check the response body. `429 {"message":"Too Many Requests"}` is `SECURITY_RATE_LIMIT`: the client crossed the limit, whose window is **20 minutes** per client IP. A 429 carrying `code: "quota_exceeded"` is something else entirely — an upstream per-account monthly quota passed through by `/api/invisibility`, `/api/dnsleaktest/session/:token` or `/api/ipblocklist` — and no local setting affects it.

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 available"</summary>

The backend could not open `common/maxmind-db/GeoLite2-City.mmdb` and `GeoLite2-ASN.mmdb` once its boot downloads had finished. It keeps running, but `GET /api/maxmind` answers 503 and the country badges throughout the UI stay empty.

The full line tells you the fix:

{% code title="log" overflow="wrap" %}

```
❌ MaxMind API will return 503 until databases are available: set MAXMIND_ACCOUNT_ID + MAXMIND_LICENSE_KEY, or drop GeoLite2-City.mmdb + GeoLite2-ASN.mmdb into common/maxmind-db/
```

{% endcode %}

Set credentials, or pre-seed the files:

{% code title=".env" %}

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

{% endcode %}

Success looks like `📦 MaxMind databases loaded (…)`. 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` and `MAXMIND_LICENSE_KEY` with `-e`. Without them the container starts, serves the UI, and 503s on `/api/maxmind` on every boot.

The first download runs right after the server starts listening and is capped at 5 minutes; until it finishes, `/api/maxmind` answers 503. If it times out, check the logs and restart. See [Deploy with Docker](/developer/getting-started/deploy-with-docker.md).

</details>

<details>

<summary>DATASET_AUTO_UPDATE is "false" but the datasets downloaded anyway</summary>

Working as intended. `DATASET_AUTO_UPDATE` gates **only the daily refresh**. The boot-time download does not consult it: a dataset that is missing, unreadable, or left half-written by an interrupted update is fetched once — MaxMind only when its credentials are set, PeeringDB only when `CLOUDFLARE_API_KEY` is. The reasoning is that a deployment should never be left without the data its configuration asks for.

With auto-update on (the default), every dataset is checked daily at `DATASET_UPDATE_CRON` — 04:30 server-local time unless you change it — and downloaded only when the remote has a newer version.

</details>

<details>

<summary>Can Bulk IP Lookup use IPinfo or another source instead of MaxMind?</summary>

Not today. The tool looks up to 200 IPs per request, so its source must be local data — a metered API would burn through its quota in a few pastes. [`BULK_LOOKUP_SOURCE`](/developer/reference/environment-variables.md#bulk-lookup-source) picks the source, and `maxmind` is the only one implemented; any other value logs a warning at boot and falls back to it. The source list lives in `server/bulk-lookup-sources.js`.

This is independent of the IP database a visitor picks in Preferences: that choice drives the IP cards and the **IP details** dialog a Bulk IP Lookup row opens, not the list itself. If the tool fails with every lookup, check that MaxMind is set up — `POST /api/bulk-lookup` answers 503 without it.

</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>The ASN Profile tool is missing, or some of its sections stay empty</summary>

The tool is listed in Advanced Tools and the navigation only when `/api/configs` reports `cloudFlare: true` — set `CLOUDFLARE_API_KEY` and restart (and allow for the 1-hour cache on `/api/configs`). The same key enables the PeeringDB download, so a deployment that just gained the key fetches the index on its next boot.

A section that stays empty while the rest of the page renders is reported in the `status` map of `GET /api/asn-profile`: `empty` means the source has nothing for that AS, `error` means it failed or timed out (that answer is edge-cached for one day instead of seven, so the source is tried again the next day), and `disabled` means the deployment lacks what it needs — for `peeringdb`, the `CLOUDFLARE_API_KEY`. With the key set but no index loaded (still downloading, or the download failed), `peeringdb` is `error`; look for `📦 PeeringDB index loaded` in the logs. Details in [API Endpoints](/developer/reference/api-endpoints.md#api-asn-profile).

</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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.ipcheck.ing/developer/reference/faq.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
