> 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/environment-variables.md).

# Environment Variables

This is the authoritative list. Every variable below is read somewhere in the code (`process.env.*` on the server, `import.meta.env.*` in the browser bundle). Anything not listed here is ignored.

Copy `.env.example` to `.env` to start:

{% code title="shell" %}

```bash
cp .env.example .env
```

{% endcode %}

`dotenv` loads `.env` from the repository root in `backend-server.js`, `frontend-server.js`, `common/logger.js`, `sentry-instrument.js`, and `vite.config.js`.

## Build-time vs runtime

MyIP has two kinds of variables, and mixing them up is the single most common deployment mistake.

| Kind       | Prefix    | Read by                                                                     | When it takes effect |
| ---------- | --------- | --------------------------------------------------------------------------- | -------------------- |
| Runtime    | no prefix | Node.js processes (`backend-server.js`, `frontend-server.js`, API handlers) | On process restart   |
| Build-time | `VITE_`   | Vite, during `pnpm run build`                                               | Only after a rebuild |

`VITE_*` values are **inlined into the JavaScript bundle** by Vite at build time. They are not read from the environment when the app runs in the browser. Changing a `VITE_*` variable and restarting the server does nothing — you must rebuild.

{% hint style="warning" %}
**Docker implication.** The official `jason5ng32/myip:latest` image ran `pnpm run build` at image-build time with no `.env` present (`.env` is listed in `.dockerignore`). Passing `-e VITE_CURL_IPV4_DOMAIN=...` to that image has **no effect on the frontend bundle**. To use build-time variables in Docker you must build your own image with the values present at build time.
{% endhint %}

## All variables at a glance

Click a variable name to jump to its full description.

| Variable                                                                  | Default        | Scope           | Required                  |
| ------------------------------------------------------------------------- | -------------- | --------------- | ------------------------- |
| [MAXMIND\_ACCOUNT\_ID](#maxmind-account-id)                               | `""`           | Runtime         | MaxMind features          |
| [MAXMIND\_LICENSE\_KEY](#maxmind-license-key)                             | `""`           | Runtime         | MaxMind features          |
| [MAXMIND\_AUTO\_UPDATE](#maxmind-auto-update)                             | `"false"`      | Runtime         | —                         |
| [CAIDA\_AUTO\_UPDATE](#caida-auto-update)                                 | `"false"`      | Runtime         | —                         |
| [BACKEND\_PORT](#backend-port)                                            | `11966`        | Runtime         | —                         |
| [FRONTEND\_PORT](#frontend-port)                                          | `18966`        | Runtime         | —                         |
| [ALLOWED\_DOMAINS](#allowed-domains)                                      | `""`           | Runtime         | Non-localhost deployments |
| [SECURITY\_RATE\_LIMIT](#security-rate-limit)                             | `0`            | Runtime         | —                         |
| [SECURITY\_DELAY\_AFTER](#security-delay-after)                           | `0`            | Runtime         | —                         |
| [SECURITY\_BLACKLIST\_LOG\_FILE\_PATH](#security-blacklist-log-file-path) | `""`           | Runtime         | —                         |
| [LOG\_LEVEL](#log-level)                                                  | `"info"`       | Runtime         | —                         |
| [LOG\_FORMAT](#log-format)                                                | pretty         | Runtime         | —                         |
| [LOG\_HTTP](#log-http)                                                    | `"false"`      | Runtime         | —                         |
| [VITE\_SENTRY\_DSN\_FRONTEND](#vite-sentry-dsn-frontend)                  | `""`           | Build + runtime | —                         |
| [SENTRY\_DSN\_BACKEND](#sentry-dsn-backend)                               | `""`           | Runtime         | —                         |
| [SENTRY\_ENVIRONMENT](#sentry-environment)                                | `"production"` | Runtime + build | —                         |
| [SENTRY\_ORG](#sentry-org)                                                | `""`           | Build           | —                         |
| [SENTRY\_PROJECT\_FRONTEND](#sentry-project-frontend)                     | `""`           | Build           | —                         |
| [SENTRY\_AUTH\_TOKEN](#sentry-auth-token)                                 | `""`           | Build           | —                         |
| [GOOGLE\_MAP\_API\_KEY](#google-map-api-key)                              | `""`           | Runtime         | —                         |
| [IPINFO\_API\_KEY](#ipinfo-api-key)                                       | `""`           | Runtime         | —                         |
| [IPAPIIS\_API\_KEY](#ipapiis-api-key)                                     | `""`           | Runtime         | —                         |
| [IP2LOCATION\_API\_KEY](#ip2location-api-key)                             | `""`           | Runtime         | —                         |
| [CLOUDFLARE\_API\_KEY](#cloudflare-api-key)                               | `""`           | Runtime         | Report sharing            |
| [MAC\_LOOKUP\_API\_KEY](#mac-lookup-api-key)                              | `""`           | Runtime         | —                         |
| [RIPESTAT\_SOURCE\_APP](#ripestat-source-app)                             | `"myip"`       | Runtime         | —                         |
| [CLOUDFLARE\_ACCOUNT\_ID](#cloudflare-account-id)                         | `""`           | Runtime         | Report sharing            |
| [CLOUDFLARE\_KV\_NAMESPACE\_ID](#cloudflare-kv-namespace-id)              | `""`           | Runtime         | Report sharing            |
| [VITE\_CURL\_IPV4\_DOMAIN](#vite-curl-ipv4-domain)                        | `""`           | Build           | All three or none         |
| [VITE\_CURL\_IPV6\_DOMAIN](#vite-curl-ipv6-domain)                        | `""`           | Build           | All three or none         |
| [VITE\_CURL\_IPV64\_DOMAIN](#vite-curl-ipv64-domain)                      | `""`           | Build           | All three or none         |
| [VITE\_SITE\_URL](#vite-site-url)                                         | `""`           | Build + runtime | —                         |
| [VITE\_DOCS\_URL](#vite-docs-url)                                         | `""`           | Build           | —                         |
| [VITE\_GOOGLE\_ANALYTICS\_ID](#vite-google-analytics-id)                  | `""`           | Build           | —                         |
| [VITE\_INVISIBILITY\_TEST\_KEY](#vite-invisibility-test-key)              | `""`           | Build           | —                         |
| [IPCHECKING\_API\_KEY](#ipchecking-api-key)                               | `""`           | Runtime         | —                         |
| [IPCHECKING\_API\_ENDPOINT](#ipchecking-api-endpoint)                     | `""`           | Runtime         | —                         |

## Required — MaxMind & offline datasets

MyIP's own IP-geolocation source and the country badges throughout the UI are served from local MaxMind GeoLite2 databases. Without them, `GET /api/maxmind` answers **503**. See [MaxMind Setup](/developer/getting-started/maxmind-setup.md).

#### `MAXMIND_ACCOUNT_ID` <a href="#maxmind-account-id" id="maxmind-account-id"></a>

Default `""` · Runtime · **Required for MaxMind features**

MaxMind account ID. Paired with `MAXMIND_LICENSE_KEY` as HTTP Basic credentials when downloading the GeoLite2 databases.

#### `MAXMIND_LICENSE_KEY` <a href="#maxmind-license-key" id="maxmind-license-key"></a>

Default `""` · Runtime · **Required for MaxMind features**

MaxMind license key, generated from your account's "Manage License Keys" page.

#### `MAXMIND_AUTO_UPDATE` <a href="#maxmind-auto-update" id="maxmind-auto-update"></a>

Default `"false"` · Runtime · Optional

`"true"` starts the periodic updater: first check 60 seconds after startup, then every 24 hours.

#### `CAIDA_AUTO_UPDATE` <a href="#caida-auto-update" id="caida-auto-update"></a>

Default `"false"` · Runtime · Optional

`"true"` refreshes the CAIDA datasets every 24 hours — as2org for ASN organization names, as-rel2 for the ASN connectivity graph.

{% hint style="info" %}
`MAXMIND_AUTO_UPDATE` gates **only the periodic scheduler**. The boot-time "download the databases if they are missing" path does not consult it — if valid credentials are present, the databases are fetched on first boot regardless (capped at a 5-minute timeout). Likewise, `CAIDA_AUTO_UPDATE=false` still lets missing CAIDA snapshots download at startup; it only disables the daily refresh.
{% endhint %}

The backend never refuses to start over missing databases. It logs a warning and listens anyway.

## Ports

#### `BACKEND_PORT` <a href="#backend-port" id="backend-port"></a>

Default `11966` · Runtime · Optional

Port the Express API server binds. Also used as the proxy target by `frontend-server.js` and by the Vite dev server's `/api` proxy.

#### `FRONTEND_PORT` <a href="#frontend-port" id="frontend-port"></a>

Default `18966` · Runtime · Optional

Port the static/SPA server binds in production, and the Vite dev server port in development.

Both are parsed with `parseInt(..., 10)`. The Dockerfile `EXPOSE`s `18966` only; the backend port stays internal to the container.

## Security

See [Security Options](/developer/configuration/security-options.md) and [Reverse Proxy & Domains](/developer/getting-started/reverse-proxy-and-domains.md).

#### `ALLOWED_DOMAINS` <a href="#allowed-domains" id="allowed-domains"></a>

Default `""` · Runtime · **Effectively required for non-localhost deployments**

Comma-separated hostname allowlist for the global `Referer` gate on `/api/*`. `localhost` is always allowed; everything else must be listed here, or every API call from your domain gets a 403.

#### `SECURITY_RATE_LIMIT` <a href="#security-rate-limit" id="security-rate-limit"></a>

Default `0` (disabled) · Runtime · Optional

Maximum `/api/*` requests per client IP per **20-minute** window. Over the limit → `429 {"message":"Too Many Requests"}`. `0` or unset disables the limiter entirely.

#### `SECURITY_DELAY_AFTER` <a href="#security-delay-after" id="security-delay-after"></a>

Default `0` (disabled) · Runtime · Optional

After this many `/api/*` requests from one IP within a **60-minute** window, each further request is delayed by `hits × 400 ms`. `0` or unset disables it.

#### `SECURITY_BLACKLIST_LOG_FILE_PATH` <a href="#security-blacklist-log-file-path" id="security-blacklist-log-file-path"></a>

Default `""` · Runtime · Optional

Opt-in on-disk ledger of rate-limited IPs, e.g. `"logs/blacklist-ip.log"`, resolved relative to the repo root. Empty means no file is written; the `IP rate-limited` warning is logged either way.

Behaviour notes, straight from the code:

* **Referer matching is exact hostname matching.** `ALLOWED_DOMAINS=example.com` does **not** allow `sub.example.com` — list every hostname you serve from.
* A request with **no** `Referer` header is rejected with `403 {"error":"What are you doing?"}`. A request with a disallowed `Referer` gets `403 {"error":"Access denied"}`.
* Both limiters **skip** `/api/monitoring`; the Sentry tunnel has its own fixed limiter (600 requests per 20 minutes per IP).
* The rate limiter logs only on the transition into the limited state, not on every blocked request.
* `app.set('trust proxy', 1)` is set, so put exactly one trusted proxy in front. The client IP is resolved from `cf-connecting-ip`, then `x-forwarded-for` (first entry), then `cf-connecting-ipv6`, then the socket address.

## Logging

See [Logging](/developer/configuration/logging.md).

#### `LOG_LEVEL` <a href="#log-level" id="log-level"></a>

Default `"info"` · Runtime · Optional

Minimum pino level: `debug` / `info` / `warn` / `error`. Lower-level messages are dropped — including their mirroring to Sentry.

#### `LOG_FORMAT` <a href="#log-format" id="log-format"></a>

Default pretty · Runtime · Optional

`"json"` emits one JSON event per line for log shippers. Any other value (or unset) uses colorized `pino-pretty` output.

#### `LOG_HTTP` <a href="#log-http" id="log-http"></a>

Default `"false"` · Runtime · Optional

`"true"` mounts `pino-http` on `/api/*`: one line per request with method, URL and status. Mounted before the rate limiter, so 429s are logged too.

## Sentry

All optional. With no DSN set, neither Sentry SDK is loaded and no Sentry code ships in the bundle. See [Error Monitoring (Sentry)](/developer/configuration/error-monitoring.md).

#### `VITE_SENTRY_DSN_FRONTEND` <a href="#vite-sentry-dsn-frontend" id="vite-sentry-dsn-frontend"></a>

Default `""` · Build **and** runtime · Optional

Frontend DSN, baked into the bundle at build time. **Also read at runtime** by `backend-server.js` — `POST /api/monitoring` (the first-party envelope tunnel) is only mounted when this is set on the server process.

#### `SENTRY_DSN_BACKEND` <a href="#sentry-dsn-backend" id="sentry-dsn-backend"></a>

Default `""` · Runtime · Optional

Backend DSN, read by `sentry-instrument.js` (loaded via `node --import`) and by `common/logger.js`, which mirrors `warn+` lines to Sentry Logs and elevates `error+` to Issues.

#### `SENTRY_ENVIRONMENT` <a href="#sentry-environment" id="sentry-environment"></a>

Default `"production"` · Runtime + build · Optional

Environment tag on backend Sentry events. Also gates source-map upload: uploads run only when this is unset or `production`.

#### `SENTRY_ORG` <a href="#sentry-org" id="sentry-org"></a>

Default `""` · Build · Optional

Sentry organization slug for build-time source-map upload.

#### `SENTRY_PROJECT_FRONTEND` <a href="#sentry-project-frontend" id="sentry-project-frontend"></a>

Default `""` · Build · Optional

Sentry project slug for build-time source-map upload.

#### `SENTRY_AUTH_TOKEN` <a href="#sentry-auth-token" id="sentry-auth-token"></a>

Default `""` · Build · Optional

Auth token for build-time source-map upload. When unset, Vite does not generate source maps at all, so they never end up publicly served from `dist/`. Build-time secret — never exposed to the browser.

{% hint style="warning" %}
If you self-build a Docker image with `VITE_SENTRY_DSN_FRONTEND` baked in, pass the same value to the container at runtime too. Otherwise the bundle posts envelopes to `/api/monitoring`, a route the backend never mounted, and every report 404s.
{% endhint %}

## Optional API keys

Each key unlocks one IP data source or tool. `GET /api/configs` reports which of these are set — as booleans only, never the values — and the frontend hides the corresponding UI when a key is missing. See [Optional API Keys](/developer/configuration/optional-api-keys.md) and [IP Data Sources](/developer/architecture/ip-data-sources.md).

#### `GOOGLE_MAP_API_KEY` <a href="#google-map-api-key" id="google-map-api-key"></a>

Default `""` · Runtime · Optional

Google Static Maps key, used by `/api/map`. Accepts a comma-separated list; one key is picked at random per request.

#### `IPINFO_API_KEY` <a href="#ipinfo-api-key" id="ipinfo-api-key"></a>

Default `""` · Runtime · Optional

ipinfo.io token for `/api/ipinfo`. Comma-separated list supported. The endpoint still works without a key, at ipinfo.io's anonymous rate.

#### `IPAPIIS_API_KEY` <a href="#ipapiis-api-key" id="ipapiis-api-key"></a>

Default `""` · Runtime · Optional

api.ipapi.is key for `/api/ipapiis`. Comma-separated list supported.

#### `IP2LOCATION_API_KEY` <a href="#ip2location-api-key" id="ip2location-api-key"></a>

Default `""` · Runtime · Optional

ip2location.io key for `/api/ip2location`. Comma-separated list supported.

#### `CLOUDFLARE_API_KEY` <a href="#cloudflare-api-key" id="cloudflare-api-key"></a>

Default `""` · Runtime · **Required for report sharing**

Cloudflare API token with two roles: Bearer token for Cloudflare Radar (`/api/cfradar`), and — together with the two variables in [Report sharing](#report-sharing-cloudflare-kv) — the credential for storing shareable reports in Workers KV. The KV role additionally needs the **Workers KV Storage: Edit** permission on the token.

#### `MAC_LOOKUP_API_KEY` <a href="#mac-lookup-api-key" id="mac-lookup-api-key"></a>

Default `""` · Runtime · Optional

maclookup.app key for `/api/macchecker`. The endpoint works without it, at the anonymous rate.

#### `RIPESTAT_SOURCE_APP` <a href="#ripestat-source-app" id="ripestat-source-app"></a>

Default `"myip"` · Runtime · Optional

Value sent as RIPEstat's `sourceapp` query parameter, so RIPE can attribute traffic to your deployment.

{% hint style="info" %}
`/api/ipapiis` and `/api/ip2location` build their upstream URL by calling `.split(',')` on the key with no null check. With the key unset the handler throws and returns 500. The frontend never calls them in that state, because `/api/configs` reports the source as unavailable and the UI hides it.
{% endhint %}

## Report sharing (Cloudflare KV) <a href="#report-sharing-cloudflare-kv" id="report-sharing-cloudflare-kv"></a>

Shareable diagnostic reports are stored in Cloudflare Workers KV over the REST API. **All three** variables — [`CLOUDFLARE_API_KEY`](#cloudflare-api-key) plus the two below — must be set, or `POST /api/report` and `GET /api/report/:id` both answer `503 {"error":"Report sharing is not configured"}` and `/api/configs` reports `reportSharing: false`, which hides the share UI.

#### `CLOUDFLARE_ACCOUNT_ID` <a href="#cloudflare-account-id" id="cloudflare-account-id"></a>

Default `""` · Runtime · **Required for report sharing**

Cloudflare account ID that owns the KV namespace.

#### `CLOUDFLARE_KV_NAMESPACE_ID` <a href="#cloudflare-kv-namespace-id" id="cloudflare-kv-namespace-id"></a>

Default `""` · Runtime · **Required for report sharing**

The namespace's **hex ID** from the dashboard — not its display name.

Report constraints, from `common/report-schema.js`: selectable retention is 1, 3 or 7 days (anything else is forced down to 1 day), and the stored payload is capped at 256 KB (`413` beyond that). The Express JSON body limit is 500 KB, deliberately above the report cap.

## Curl API

#### `VITE_CURL_IPV4_DOMAIN` <a href="#vite-curl-ipv4-domain" id="vite-curl-ipv4-domain"></a>

Default `""` · Build · All three or none

IPv4-only hostname advertised in the curl API card.

#### `VITE_CURL_IPV6_DOMAIN` <a href="#vite-curl-ipv6-domain" id="vite-curl-ipv6-domain"></a>

Default `""` · Build · All three or none

IPv6-only hostname.

#### `VITE_CURL_IPV64_DOMAIN` <a href="#vite-curl-ipv64-domain" id="vite-curl-ipv64-domain"></a>

Default `""` · Build · All three or none

Dual-stack hostname.

{% hint style="warning" %}
The curl API card only renders when **all three** are non-empty — `curlDomainsHadSet` in `frontend/store.js` ANDs them together. Setting one or two shows nothing. And because these are `VITE_*`, they must be present at build time.
{% endhint %}

These variables only supply the hostnames shown to users. MyIP does not serve those endpoints itself — you point the DNS records at whatever plain-text IP echo service you run.

## Misc

#### `VITE_SITE_URL` <a href="#vite-site-url" id="vite-site-url"></a>

Default `""` · Build **and** runtime · Optional

Your deployment's absolute origin. At build time it fills `__SITE_URL__` in `index.html` (canonical / OG tags); when empty, that whole marked block is stripped. At runtime it becomes the last segment of the upstream `User-Agent` (`MyIP/v7.2.0/https://example.com`), so forks identify themselves to upstream APIs instead of impersonating ipcheck.ing. Trailing slashes are trimmed.

#### `VITE_DOCS_URL` <a href="#vite-docs-url" id="vite-docs-url"></a>

Default `""` · Build · Optional

Origin of the GitBook docs site that backs the in-app documentation assistant and the Help Center link in the footer, e.g. `https://docs.ipcheck.ing`. Trailing slashes are trimmed; the embed script is loaded from `<origin>/~gitbook/embed/script.js` on first use, never at page load.

When empty, neither the assistant nor the footer link is built into the bundle at all. The nav entry point additionally requires the canonical deployment — see [Features Tied to IPCheck.ing](/developer/configuration/features-tied-to-ipcheck-ing.md).

#### `VITE_GOOGLE_ANALYTICS_ID` <a href="#vite-google-analytics-id" id="vite-google-analytics-id"></a>

Default `""` · Build · Optional

Google Analytics measurement ID. When empty, `gtag.js` is never injected and the privacy page reports analytics as disabled. Self-hosters should leave it empty.

#### `VITE_INVISIBILITY_TEST_KEY` <a href="#vite-invisibility-test-key" id="vite-invisibility-test-key"></a>

Default `""` · Build · Optional

Key passed as `pdKey` to the third-party proxy-detection script used by the Invisibility Test. Only meaningful on deployments that also have `IPCHECKING_API_KEY`. Not present in `.env.example`.

#### `IPCHECKING_API_KEY` <a href="#ipchecking-api-key" id="ipchecking-api-key"></a>

Default `""` · Runtime · Optional

API key for the private IPCheck.ing companion API. Gates `/api/ipchecking`, `/api/getuserinfo`, `PUT /api/updateuserachievement`, `/api/invisibility`, and `/api/dnsleaktest/session/:token` — without it those routes return `500 {"error":"API key is missing"}`. Also surfaced as `ipChecking` in `/api/configs`. Not in `.env.example`.

#### `IPCHECKING_API_ENDPOINT` <a href="#ipchecking-api-endpoint" id="ipchecking-api-endpoint"></a>

Default `""` · Runtime · Optional

Base URL of that companion API, used together with `IPCHECKING_API_KEY`. Not in `.env.example`.

{% hint style="info" %}
`IPCHECKING_API_KEY` / `IPCHECKING_API_ENDPOINT` and `VITE_INVISIBILITY_TEST_KEY` belong to a closed-source companion service. Self-hosted forks cannot obtain them, and the features they gate are hidden on non-`ipcheck.ing` deployments anyway. See [Features Tied to IPCheck.ing](/developer/configuration/features-tied-to-ipcheck-ing.md).
{% endhint %}

## Legacy aliases

Two variables were renamed. The old spellings are still read as fallbacks so existing deployments do not lose features on upgrade. Prefer the new names.

| Old name           | Current name         | Read where                                                 |
| ------------------ | -------------------- | ---------------------------------------------------------- |
| `IPINFO_API_TOKEN` | `IPINFO_API_KEY`     | `api/ipinfo-io.js`, `api/configs.js`                       |
| `CLOUDFLARE_API`   | `CLOUDFLARE_API_KEY` | `api/cf-radar.js`, `api/share-report.js`, `api/configs.js` |

Resolution order is `NEW || OLD` — if both are set, the new name wins.

## Minimal working configuration

{% code title=".env" %}

```bash
BACKEND_PORT=11966
FRONTEND_PORT=18966
ALLOWED_DOMAINS="myip.example.com"
MAXMIND_ACCOUNT_ID="your-account-id"
MAXMIND_LICENSE_KEY="your-license-key"
MAXMIND_AUTO_UPDATE="true"
```

{% endcode %}

Everything else is optional and only adds data sources or tools. See [Quickstart](/developer/getting-started/quickstart.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/reference/environment-variables.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.
