Geo2tz
Get the timezone from geo coordinates (lat, lon).
Get the timezone from geo coordinates (lat, lon).
# Geo2Tz
[](https://github.com/noandrea/geo2tz/actions/workflows/quality.yml) [](https://godoc.org/github.com/noandrea/geo2tz) [](https://goreportcard.com/report/github.com/noandrea/geo2tz)
A self-hostable service to get the timezone given geo-coordinates (lat/lng)
Timezone data comes from [github.com/evansiroky/timezone-boundary-builder](https://github.com/evansiroky/timezone-boundary-builder).
[](https://stand-with-ukraine.pp.ua/)
## Maturity Level
This project is considered mature and stable, having undergone extensive testing and refinement over time. It is now in a state where it can be reliably used in production environments. The badge below shows the number of Docker pulls for the project:
<!--  -->

### Contributing
We value your feedback and contributions! If you encounter any bugs or have ideas for new features, please don't hesitate to [open an issue](https://github.com/noandrea/geo2tz/issues/new). Your input is crucial in helping us improve and evolve the project.
## Motivations
Geo-coordinates can be sensitive information. This project provides a privacy-friendly, self-hosted solution that ensures coordinates are not leaked to third-party services.
## API
The service exposes two endpoints: one to look up the timezone for a pair of coordinates, and one to report the version of the timezone database in use.
### Timezone lookup
```http
GET /tz/${LATITUDE}/${LONGITUDE}
```
Returns a JSON reply (`http/200`), for example:
```console
curl -s http://localhost:2004/tz/51.477811/0 | jq
```
```json
{
"coords": {
"lat": 51.47781,
"lon": 0
},
"tz": "Europe/London"
}
```
On invalid input it returns a `4xx` response, for example:
```console
curl -v http://localhost:2004/tz/51.477811/1000 | jq
* Trying 127.0.0.1:2004...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to localhost (127.0.0.1) port 2004 (#0)
> GET /tz/51.477811/1000 HTTP/1.1
> Host: localhost:2004
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Content-Type: application/json; charset=UTF-8
< Vary: Origin
< Date: Fri, 23 Jun 2023 19:09:29 GMT
< Content-Length: 54
<
{ [54 bytes data]
100 54 100 54 0 0 89403 0 --:--:-- --:--:-- --:--:-- 54000
* Connection #0 to host localhost left intact
```
```json
{
"message": "lon value 1000 out of range (-180/+180)"
}
```
### Database version
The version of the database in use is exposed at `/tz/version`:
```console
curl -s http://localhost:2004/tz/version | jq
```
```json
{
"version": "2026b",
"url": "https://github.com/evansiroky/timezone-boundary-builder/releases/tag/2026b",
"geo_data_url": "https://github.com/evansiroky/timezone-boundary-builder/releases/download/2026b/timezones-with-oceans.geojson.zip"
}
```
Coordinates are decimal degrees in the ranges `[-90, 90]` for latitude and `[-180, 180]` for longitude.
### Authorization
Geo2Tz supports a basic token authorization mechanism, if the configuration value for `web.auth_token_value` is a non-empty string, geo2tz will check the query parameter value to authorize incoming requests.
For example, running the service with:
```sh
docker run --pull=always -p 2004:2004 -e GEO2TZ_WEB_AUTH_TOKEN_VALUE=secret ghcr.io/noandrea/geo2tz:latest
```
will enable authorization. With the authorization enabled, a query that does not specify the token will fail with an HTTP code 401:
```sh
> curl -sv http://localhost:2004/tz/41.902782/12.496365 | jq
```
```
* Trying 127.0.0.1:2004...
* Connected to localhost (127.0.0.1) port 2004 (#0)
> GET /tz/41.902782/12.496365 HTTP/1.1
> Host: localhost:2004
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< Content-Type: application/json; charset=UTF-8
< Vary: Origin
< Date: Sun, 31 Jul 2022 20:06:56 GMT
< Content-Length: 27
<
{ [27 bytes data]
* Connection #0 to host localhost left intact
{
"message": "unauthorized"
}
```
Passing the token in the query parameters will succeed instead:
```sh
> curl -s http://localhost:2004/tz/41.902782/12.496365\?t\=secret | jq
```
```json
{
"coords": {
"lat": 41.902782,
"lon": 12.496365
},
"tz": "Europe/Rome"
}
```
## Configuration
Geo2Tz is configured via environment variables (prefixed with `GEO2TZ_`) or an optional config file. Defaults are listed below.
| Environment variable | Default | Description |
| --- | --- | --- |
| `GEO2TZ_WEB_LISTEN_ADDRESS` | `:2004` | Address the HTTP server binds to. |
| `GEO2TZ_WEB_AUTH_TOKEN_VALUE` | (empty) | When non-empty, enables token authorization. |
| `GEO2TZ_WEB_AUTH_TOKEN_PARAM_NAME` | `t` | Query-parameter name carrying the auth token. |
| `GEO2TZ_TZ_DATABASE_NAME` | bundled tz DB | Path to the timezone GeoJSON database. |
| `GEO2TZ_TZ_VERSION_FILE` | bundled version file | Path to the version metadata file. |
A config file is loaded automatically when present at `/etc/geo2tz/config.{yaml,toml,json}`. A custom path can be passed with `--config`. Keys mirror the env vars but are nested under `web.*` / `tz.*` (e.g. `web.auth_token_value`).
## MCP server
Geo2Tz also ships as an [MCP](https://modelcontextprotocol.io) server, exposing the timezone lookup as tools for MCP clients (AI assistants, IDEs, agents). It runs over stdio and performs lookups offline against the local timezone database (no coordinates leave the machine).
Start it with:
```sh
geo2tz mcp
```
Available tools:
| Tool | Description |
| ---- | ----------- |
| `get_timezone` | returns the IANA timezone identifier for a pair of coordinates (lat/lon) |
| `get_tz_version` | returns the version of the timezone boundaries database in use |
The Docker image bundles the timezone database, so the Docker configuration below works without extra setup.
Example client configuration (Claude Desktop, `claude_desktop_config.json`):
```json
{
"mcpServers": {
"geo2tz": {
"command": "docker",
"args": ["run", "-i", "--rm", "ghcr.io/noandrea/geo2tz:latest", "mcp"]
}
}
}
```
Or, with a local binary (VS Code `mcp.json`, Claude Code, or any stdio MCP client). The timezone database is not bundled with the binary, so first fetch it into a data directory:
```sh
mkdir -p ~/.local/share/geo2tz
geo2tz update latest \
--db ~/.local/share/geo2tz/timezones.zip \
--version-file ~/.local/share/geo2tz/version.json
```
Then point the `GEO2TZ_TZ_*` environment variables at it (absolute paths, no `~` expansion):
```json
{
"mcpServers": {
"geo2tz": {
"command": "/usr/local/bin/geo2tz",
"args": ["mcp"],
"env": {
"GEO2TZ_TZ_DATABASE_NAME": "/home/you/.local/share/geo2tz/timezones.zip",
"GEO2TZ_TZ_VERSION_FILE": "/home/you/.local/share/geo2tz/version.json"
}
}
}
}
```
## Docker
Docker image is available at [geo2tz](https://github.com/noandrea/geo2tz/pkgs/container/geo2tz)
```sh
docker run --pull=always -p 2004:2004 ghcr.io/noandrea/geo2tz:latest
```
The image is built `FROM` [scratch](https://hub.docker.com/_/scratch), so it contains only the `geo2tz` binary and the bundled timezone database — no shell, package manager, or utilities.
## Docker Compose
Docker Compose YAML example:
```yaml
services:
geo2tz:
container_name: geo2tz
image: ghcr.io/noandrea/geo2tz:latest # pin to a release tag for production deployments
ports:
- "2004:2004"
restart: unless-stopped
# uncomment to enable authorization via request token
# environment:
# GEO2TZ_WEB_AUTH_TOKEN_VALUE: ${GEO2TZ_TOKEN}
# GEO2TZ_WEB_AUTH_TOKEN_PARAM_NAME: t
# GEO2TZ_WEB_LISTEN_ADDRESS: ":2004"
```
The `version` top-level field has been removed from the Compose spec and is no longer needed. The image is built `FROM scratch`, so it has no shell or `wget`/`curl` — Compose healthchecks based on those will not work; use an external probe instead.
## K8s
Kubernetes configuration example:
```yaml
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: geo2tz
name: geo2tz
spec:
replicas: 1
revisionHistoryLimit: 3
selector:
matchLabels:
app: geo2tz
template:
metadata:
labels:
app: geo2tz
spec:
containers:
- name: geo2tz
image: ghcr.io/noandrea/geo2tz:latest # pin to a release tag for production deployments
imagePullPolicy: Always
ports:
- name: http
containerPort: 2004
# if GEO2TZ_WEB_AUTH_TOKEN_VALUE is non-empty, token authorization is enabled
# env:
# - name: GEO2TZ_WEB_AUTH_TOKEN_VALUE
# value: "secretsmaybebetter" # default is empty
# - name: GEO2TZ_WEB_AUTH_TOKEN_PARAM_NAME
# value: "t" # default value
# - name: GEO2TZ_WEB_LISTEN_ADDRESS
# value: ":2004" # default value
readinessProbe:
httpGet:
path: /tz/version
port: http
initialDelaySeconds: 2
periodSeconds: 10
livenessProbe:
httpGet:
path: /tz/version
port: http
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
---
# Service for the above deployment
apiVersion: v1
kind: Service
metadata:
name: geo2tz-service
spec:
type: ClusterIP
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app: geo2tz
```
## Development notes
To update the timezone database you have a few options:
1. download the version specified in the `tzdata/version.json` file
```console
geo2tz update current
```
2. update to the latest version available
```console
geo2tz update latest
```
3. update to a specific version
```console
geo2tz update 2023b
```
The `update` command downloads the timezone GeoJSON zip and writes a version file into the `tzdata` directory; the version file is used to track the current version of the database.