> 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/deploy-with-docker.md).

# Deploy with Docker

Docker is the recommended way to self-host MyIP. One image contains both halves of the app: the built Vue frontend and the Express backend.

## Images

| Registry        | Image                            |
| --------------- | -------------------------------- |
| Docker Hub      | `jason5ng32/myip:latest`         |
| GitHub Packages | `ghcr.io/jason5ng32/myip:latest` |

Both are built from the same release and published for `linux/amd64` and `linux/arm64`. Every release also gets a version tag alongside `:latest` — pin to one if you want reproducible upgrades.

## Run it

{% tabs %}
{% tab title="docker run" %}
{% code title="docker run" %}

```bash
docker run -d \
  -p 18966:18966 \
  -e MAXMIND_ACCOUNT_ID="your-account-id" \
  -e MAXMIND_LICENSE_KEY="your-license-key" \
  -e MAXMIND_AUTO_UPDATE="true" \
  -e ALLOWED_DOMAINS="myip.example.com" \
  -e CAIDA_AUTO_UPDATE="true" \
  -e GOOGLE_MAP_API_KEY="your-key" \
  -e IPINFO_API_KEY="your-key" \
  -e SECURITY_RATE_LIMIT="500" \
  -e LOG_LEVEL="info" \
  --name myip \
  --restart always \
  jason5ng32/myip:latest
```

{% endcode %}
{% endtab %}

{% tab title="Docker Compose" %}
{% code title="compose.yaml" %}

```yaml
services:
  myip:
    image: jason5ng32/myip:latest
    container_name: myip
    restart: always
    ports:
      - "18966:18966"
    environment:
      # Required — see MaxMind Setup
      MAXMIND_ACCOUNT_ID: "your-account-id"
      MAXMIND_LICENSE_KEY: "your-license-key"
      MAXMIND_AUTO_UPDATE: "true"
      # Required once you serve MyIP on a domain
      ALLOWED_DOMAINS: "myip.example.com"
      # Optional
      CAIDA_AUTO_UPDATE: "true"
      GOOGLE_MAP_API_KEY: "your-key"
      IPINFO_API_KEY: "your-key"
      SECURITY_RATE_LIMIT: "500"
      LOG_LEVEL: "info"
```

{% endcode %}

Start it with `docker compose up -d`.
{% endtab %}
{% endtabs %}

Only `MAXMIND_ACCOUNT_ID`, `MAXMIND_LICENSE_KEY` and `MAXMIND_AUTO_UPDATE` are needed to get a working instance. Everything else above is optional — the complete list lives in [Environment Variables](/developer/reference/environment-variables.md).

{% hint style="info" %}
Prefer an env file over inline secrets: put the variables in `.env` next to your compose file and Compose picks them up, or pass `--env-file .env` to `docker run`.
{% endhint %}

## Ports

The container runs two Node processes:

| Process                                                           | Port                      | Published? |
| ----------------------------------------------------------------- | ------------------------- | ---------- |
| Frontend — serves the built SPA and proxies `/api` to the backend | `18966` (`FRONTEND_PORT`) | Yes        |
| Backend — the Express API                                         | `11966` (`BACKEND_PORT`)  | No         |

The frontend proxies `/api` to `http://localhost:<BACKEND_PORT>` **inside the container**, so you never publish the backend port. Only `18966` is exposed by the image.

To serve MyIP on a different host port, change the left side of the mapping:

```bash
-p 8080:18966
```

If you change `FRONTEND_PORT`, change the right side of the mapping to match.

## `VITE_*` variables do not work on the prebuilt image

Variables whose name starts with `VITE_` are read by Vite at **build time** and baked into the JavaScript bundle. The published image is built in CI without an `.env` file, so passing them with `-e` at runtime has no effect on the frontend.

That applies to `VITE_CURL_IPV4_DOMAIN`, `VITE_CURL_IPV6_DOMAIN`, `VITE_CURL_IPV64_DOMAIN`, `VITE_GOOGLE_ANALYTICS_ID`, `VITE_SITE_URL` and `VITE_SENTRY_DSN_FRONTEND`. To set them you need to build your own image (or use [Deploy with Node.js](/developer/getting-started/deploy-with-nodejs.md)):

```bash
git clone https://github.com/jason5ng32/MyIP.git
cd MyIP
cp .env.example .env   # fill in the VITE_* values
docker build -t myip-custom .
```

{% hint style="info" %}
`VITE_SENTRY_DSN_FRONTEND` is the exception: the backend also reads it **at runtime** to decide whether to mount the `/api/monitoring` tunnel. If you bake it in at build time, pass the same value to the container at runtime too. See [Error Monitoring](/developer/configuration/error-monitoring.md).
{% endhint %}

## Where data lives

MyIP downloads a few datasets at runtime. They live **inside the container**, under `/app/common/`:

| Path                      | Contents                                                                           |
| ------------------------- | ---------------------------------------------------------------------------------- |
| `/app/common/maxmind-db/` | `GeoLite2-City.mmdb`, `GeoLite2-ASN.mmdb`, plus the updater's state and lock files |
| `/app/common/as-org-db/`  | CAIDA `as-org2info.txt` — ASN organization names                                   |
| `/app/common/as-rel-db/`  | CAIDA `as-rel2.txt` — the ASN connectivity graph                                   |

None of these are baked into the image (MaxMind's GeoLite2 license forbids redistribution), and none of them are precious — every one is re-downloaded on a fresh start. Recreating the container simply means MyIP fetches them again on first boot.

If you would rather not re-download on every upgrade, mount volumes:

{% code title="compose.yaml (excerpt)" %}

```yaml
    volumes:
      - myip-maxmind:/app/common/maxmind-db
      - myip-asorg:/app/common/as-org-db
      - myip-asrel:/app/common/as-rel-db

volumes:
  myip-maxmind:
  myip-asorg:
  myip-asrel:
```

{% endcode %}

## Upgrading

{% tabs %}
{% tab title="docker run" %}

```bash
docker pull jason5ng32/myip:latest
docker stop myip && docker rm myip
# then re-run your original `docker run` command
```

{% endtab %}

{% tab title="Docker Compose" %}

```bash
docker compose pull
docker compose up -d
```

{% endtab %}
{% endtabs %}

After an upgrade without volumes, the first boot re-downloads the GeoLite2 and CAIDA datasets. That takes a moment — the backend waits for MaxMind before it starts listening (capped at 5 minutes).

## Checking on it

```bash
docker logs -f myip
```

A healthy startup shows the backend and frontend listeners plus the MaxMind load:

```
🚀 Backend server ready on http://localhost:11966
🚀 Static file server ready on http://localhost:18966
📦 MaxMind databases loaded (startup)
```

If you see `❌ MaxMind API will return 503 until databases are loaded successfully`, go to [MaxMind Setup](/developer/getting-started/maxmind-setup.md#troubleshooting).

## Next steps

* [MaxMind Setup](/developer/getting-started/maxmind-setup.md) — required credentials and how the auto-updater works
* [Reverse Proxy & Domains](/developer/getting-started/reverse-proxy-and-domains.md) — TLS, Nginx/Caddy, `ALLOWED_DOMAINS`
* [Security Options](/developer/configuration/security-options.md) — rate limiting and abuse protection
* [Logging](/developer/configuration/logging.md) — log level, JSON output, HTTP request logs


---

# 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/deploy-with-docker.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.
