Skip to content

Commit 8e3733d

Browse files
committed
Use Hypercorn for dual-stack GeoIP binds on Railway
1 parent a640c1b commit 8e3733d

4 files changed

Lines changed: 26 additions & 6 deletions

File tree

apps/geoip/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ GEOIP_SERVICE_URL=http://localhost:8083
5959

6060
Deploy this app as its own Railway service named `geoip`.
6161

62-
The service binds to `::` by default so Railway private networking can reach it in both new dual-stack environments and older IPv6-only ones.
62+
The service uses Hypercorn and binds both `0.0.0.0` and `::` by default so Railway's public deployment healthchecks and private networking can both reach it.
6363

6464
Use `/live` as the Railway deployment healthcheck path. `/health` remains a readiness endpoint and can return `503` until the MaxMind databases have been downloaded and loaded.
6565

apps/geoip/requirements.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
fastapi==0.115.12
22
geoip2==5.1.0
3-
uvicorn[standard]==0.34.2
4-
3+
hypercorn==0.18.0

apps/geoip/src/main.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from typing import AsyncIterator
77

88
from fastapi import FastAPI, HTTPException, Response
9-
import uvicorn
109

1110
from .config import Settings
1211
from .database import GeoIPDatabaseManager
@@ -26,6 +25,17 @@ async def refresh_databases_forever(manager: GeoIPDatabaseManager) -> None:
2625
logger.exception("Periodic GeoIP database refresh failed")
2726

2827

28+
def build_bind_addresses(host: str, port: int) -> list[str]:
29+
normalized_host = host.strip()
30+
if not normalized_host or normalized_host == "::":
31+
return [f"0.0.0.0:{port}", f"[::]:{port}"]
32+
33+
if ":" in normalized_host and not normalized_host.startswith("["):
34+
return [f"[{normalized_host}]:{port}"]
35+
36+
return [f"{normalized_host}:{port}"]
37+
38+
2939
def create_app(manager: GeoIPDatabaseManager | None = None) -> FastAPI:
3040
if manager is None:
3141
manager = GeoIPDatabaseManager(Settings.from_env())
@@ -73,5 +83,12 @@ async def lookup(payload: LookupRequest) -> LookupResponse:
7383

7484

7585
if __name__ == "__main__":
86+
from hypercorn.asyncio import serve
87+
from hypercorn.config import Config
88+
7689
settings = Settings.from_env()
77-
uvicorn.run("src.main:app", host=settings.host, port=settings.port)
90+
config = Config()
91+
config.bind = build_bind_addresses(settings.host, settings.port)
92+
config.accesslog = "-"
93+
config.errorlog = "-"
94+
asyncio.run(serve(app, config))

apps/geoip/tests/test_main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from fastapi.testclient import TestClient
66

7-
from src.main import create_app
7+
from src.main import build_bind_addresses, create_app
88
from src.models import HealthResponse, LookupResponse
99

1010

@@ -73,6 +73,10 @@ def test_live_returns_200_even_when_not_ready() -> None:
7373
assert response.json() == {"status": "ok"}
7474

7575

76+
def test_build_bind_addresses_defaults_to_dual_stack_for_railway() -> None:
77+
assert build_bind_addresses("::", 8080) == ["0.0.0.0:8080", "[::]:8080"]
78+
79+
7680
def test_lookup_returns_payload() -> None:
7781
with TestClient(create_app(FakeManager(ready=True))) as client:
7882
response = client.post("/v1/lookup", json={"ip": "8.8.8.8"})

0 commit comments

Comments
 (0)