├── src ├── __init__.py ├── configs │ ├── __init__.py │ └── path_database.py ├── types │ ├── __init__.py │ └── ip_geolocaltion.py ├── controlers │ ├── __init__.py │ └── geoip_controler.py ├── services │ ├── __init__.py │ └── geoip_service.py └── app.py ├── requirements.txt ├── .env.example ├── geoip.conf ├── entrypoint.sh ├── main.py ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── .github └── workflows │ └── docker-build.yml └── README.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/configs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/types/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/controlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/configs/path_database.py: -------------------------------------------------------------------------------- 1 | PATH_DATABASE = "/usr/share/GeoIP/" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn[standard] 3 | python-dotenv 4 | pydantic 5 | maxminddb 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=4360 2 | GEOIPUPDATE_ACCOUNT_ID=your_account_id 3 | GEOIPUPDATE_LICENSE_KEY=your_license_key 4 | -------------------------------------------------------------------------------- /geoip.conf: -------------------------------------------------------------------------------- 1 | AccountID $GEOIPUPDATE_ACCOUNT_ID 2 | LicenseKey $GEOIPUPDATE_LICENSE_KEY 3 | EditionIDs GeoLite2-City GeoLite2-Country GeoLite2-ASN 4 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copy GeoIP databases to mounted volume if they don't exist 4 | if [ ! -f "/usr/share/GeoIP/GeoLite2-City.mmdb" ]; then 5 | echo "Copying GeoIP databases to mounted volume..." 6 | cp -r /tmp/geoip/* /usr/share/GeoIP/ 2>/dev/null || echo "No databases to copy from /tmp/geoip" 7 | fi 8 | 9 | # Start the API server 10 | echo "Starting API server..." 11 | python main.py 12 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | import os 3 | import logging 4 | from dotenv import load_dotenv 5 | 6 | # Load environment variables 7 | load_dotenv() 8 | 9 | # Configure logging 10 | logger = logging.getLogger(__name__) 11 | 12 | if __name__ == "__main__": 13 | port = int(os.getenv("PORT", "4360")) 14 | 15 | print(f"🚀 GeoIP Server is starting on port {port}...") 16 | 17 | uvicorn.run( 18 | "src.app:app", 19 | host="0.0.0.0", 20 | port=port, 21 | reload=False, 22 | log_level="info" 23 | ) 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual environments 24 | venv/ 25 | ENV/ 26 | env/ 27 | .venv 28 | 29 | # Environment variables 30 | .env 31 | 32 | # IDE 33 | .vscode/ 34 | .idea/ 35 | *.swp 36 | *.swo 37 | *~ 38 | 39 | # GeoIP data 40 | data/ 41 | *.mmdb 42 | 43 | # Docker 44 | *.log 45 | 46 | # OS 47 | .DS_Store 48 | Thumbs.db 49 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | geoip_api: 3 | container_name: geoip_api 4 | platform: linux/amd64 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | args: 9 | GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID} 10 | GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY} 11 | env_file: .env 12 | ports: 13 | - "${PORT}:${PORT}" 14 | volumes: 15 | - ./data:/usr/share/GeoIP 16 | restart: always 17 | environment: 18 | - PYTHONUNBUFFERED=1 19 | logging: 20 | driver: "json-file" 21 | options: 22 | max-size: "10m" 23 | max-file: "3" 24 | 25 | networks: 26 | default: 27 | driver: bridge 28 | -------------------------------------------------------------------------------- /src/types/ip_geolocaltion.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, ConfigDict 2 | 3 | class IPGeoLocation(BaseModel): 4 | model_config = ConfigDict(populate_by_name=True) 5 | 6 | query: str = "" 7 | status: str = "" 8 | continent: str = "" 9 | continentCode: str = "" 10 | country: str = "" 11 | countryCode: str = "" 12 | region: str = "" 13 | regionName: str = "" 14 | city: str = "" 15 | district: str = "" 16 | zip: str = "" 17 | lat: float = 0.0 18 | lon: float = 0.0 19 | timezone: str = "" 20 | offset: int = 0 21 | currency: str = "" 22 | isp: str = "" 23 | org: str = "" 24 | as_: str = Field(default="", alias="as") # Use alias with default value 25 | asname: str = "" -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from src.controlers.geoip_controler import geoip_router 3 | import logging 4 | import sys 5 | 6 | # Configure logging 7 | logging.basicConfig( 8 | level=logging.INFO, 9 | format='%(asctime)s | %(levelname)-7s | %(message)s', 10 | datefmt='%H:%M:%S', 11 | handlers=[logging.StreamHandler(sys.stdout)] 12 | ) 13 | 14 | # Reduce noise from uvicorn 15 | logging.getLogger("uvicorn.access").setLevel(logging.WARNING) 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | # Create application 20 | app = FastAPI( 21 | title="GeoIP Server", 22 | description="GeoIP Server API", 23 | version="1.0.0", 24 | docs_url="/docs", 25 | redoc_url="/redoc" 26 | ) 27 | 28 | 29 | @app.on_event("startup") 30 | async def startup_event(): 31 | logger.info("🚀 GeoIP Server started") 32 | 33 | 34 | @app.on_event("shutdown") 35 | async def shutdown_event(): 36 | logger.info("🛑 GeoIP Server stopped") 37 | 38 | 39 | # Include router 40 | app.include_router(geoip_router) 41 | -------------------------------------------------------------------------------- /src/controlers/geoip_controler.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, Response 2 | from fastapi.responses import JSONResponse 3 | from typing import Union, Dict, List 4 | from src.services.geoip_service import geoip_service 5 | from src.types.ip_geolocaltion import IPGeoLocation 6 | import logging 7 | import re 8 | 9 | logger = logging.getLogger(__name__) 10 | geoip_router = APIRouter() 11 | 12 | # Simple IP validation regex 13 | IP_PATTERN = re.compile(r'^(\d{1,3}\.){3}\d{1,3}$|^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$') 14 | 15 | 16 | @geoip_router.get("/") 17 | async def get_client_ip(request: Request) -> Dict[str, str]: 18 | """Get the client's IP address""" 19 | return {"ip": request.client.host if request.client else None} 20 | 21 | 22 | @geoip_router.get("/favicon.ico") 23 | async def favicon(): 24 | """Return 204 No Content for favicon requests""" 25 | return Response(status_code=204) 26 | 27 | 28 | @geoip_router.get("/bulk/{ips}") 29 | async def lookup_bulk_ips(ips: str) -> List[Union[IPGeoLocation, Dict[str, str]]]: 30 | """Lookup geolocation for multiple IPs (comma-separated)""" 31 | ip_list = [ip.strip() for ip in ips.split(",")] 32 | return [geoip_service.lookup(ip) for ip in ip_list] 33 | 34 | 35 | @geoip_router.get("/{ip}") 36 | async def lookup_ip(ip: str) -> Union[IPGeoLocation, Dict[str, str]]: 37 | """Lookup geolocation for a single IP address""" 38 | if not IP_PATTERN.match(ip): 39 | return JSONResponse( 40 | status_code=400, 41 | content={"error": "Invalid IP address format"} 42 | ) 43 | return geoip_service.lookup(ip) 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Python slim image 2 | FROM python:3.11-slim 3 | 4 | # Define build arguments 5 | ARG GEOIPUPDATE_ACCOUNT_ID 6 | ARG GEOIPUPDATE_LICENSE_KEY 7 | 8 | # Set environment variables from build arguments 9 | ENV GEOIPUPDATE_ACCOUNT_ID=$GEOIPUPDATE_ACCOUNT_ID 10 | ENV GEOIPUPDATE_LICENSE_KEY=$GEOIPUPDATE_LICENSE_KEY 11 | ENV PYTHONUNBUFFERED=1 12 | 13 | WORKDIR /app 14 | 15 | # Install geoipupdate and necessary tools 16 | RUN apt-get update \ 17 | && apt-get install -y wget ca-certificates jq curl \ 18 | && export GEOIPUPDATE_VERSION=$(curl -s https://api.github.com/repos/maxmind/geoipupdate/releases/latest | jq -r '.tag_name') \ 19 | && wget -O /tmp/geoipupdate.deb "https://github.com/maxmind/geoipupdate/releases/download/${GEOIPUPDATE_VERSION}/geoipupdate_${GEOIPUPDATE_VERSION#v}_linux_amd64.deb" \ 20 | && dpkg -i /tmp/geoipupdate.deb \ 21 | && rm /tmp/geoipupdate.deb \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | # Create directories for GeoIP databases 25 | RUN mkdir -p /usr/share/GeoIP /tmp/geoip 26 | 27 | # Copy GeoIP configuration and download databases 28 | COPY geoip.conf /etc/GeoIP.conf 29 | RUN sed -i "s/\$GEOIPUPDATE_ACCOUNT_ID/${GEOIPUPDATE_ACCOUNT_ID}/g" /etc/GeoIP.conf && \ 30 | sed -i "s/\$GEOIPUPDATE_LICENSE_KEY/${GEOIPUPDATE_LICENSE_KEY}/g" /etc/GeoIP.conf 31 | RUN /usr/bin/geoipupdate 32 | 33 | # Copy databases to temp location so they can be copied to volume on startup 34 | RUN cp -r /usr/share/GeoIP/* /tmp/geoip/ 35 | 36 | # Copy requirements and install Python dependencies 37 | COPY requirements.txt . 38 | RUN pip install --no-cache-dir -r requirements.txt 39 | 40 | # Copy application code 41 | COPY src src 42 | COPY main.py . 43 | 44 | # Alternative approach: Use inline script instead of external file 45 | RUN echo '#!/bin/bash\n\n# Copy GeoIP databases to mounted volume if they don'"'"'t exist\nif [ ! -f "/usr/share/GeoIP/GeoLite2-City.mmdb" ]; then\n echo "Copying GeoIP databases to mounted volume..."\n cp -r /tmp/geoip/* /usr/share/GeoIP/ 2>/dev/null || echo "No databases to copy from /tmp/geoip"\nfi\n\n# Start the API server\necho "Starting API server..."\nexec python main.py' > /entrypoint.sh && chmod +x /entrypoint.sh 46 | 47 | ENV PORT=4360 48 | 49 | EXPOSE ${PORT} 50 | 51 | CMD ["/entrypoint.sh"] 52 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Daily Docker Image Build 2 | 3 | on: 4 | schedule: 5 | # Run daily at midnight UTC 6 | - cron: "0 0 * * *" 7 | push: 8 | branches: ["main"] 9 | paths: 10 | - "src/**" 11 | - "Dockerfile" 12 | - "docker-compose.yml" 13 | - "requirements.txt" 14 | - "main.py" 15 | - "entrypoint.sh" 16 | - "geoip.conf" 17 | workflow_dispatch: 18 | 19 | env: 20 | REGISTRY: ghcr.io 21 | IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} 22 | CONTAINER_NAME: ${{ github.event.repository.name }} 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | permissions: 28 | packages: write 29 | contents: read 30 | 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v4 34 | 35 | - name: Verify MaxMind secrets are set 36 | run: | 37 | if [ -z "${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}" ]; then 38 | echo "::error::GEOIPUPDATE_ACCOUNT_ID secret is not set" 39 | echo "Please add GEOIPUPDATE_ACCOUNT_ID to repository secrets" 40 | echo "Go to Settings → Secrets and variables → Actions → New repository secret" 41 | exit 1 42 | fi 43 | if [ -z "${{ secrets.GEOIPUPDATE_LICENSE_KEY }}" ]; then 44 | echo "::error::GEOIPUPDATE_LICENSE_KEY secret is not set" 45 | echo "Please add GEOIPUPDATE_LICENSE_KEY to repository secrets" 46 | echo "Go to Settings → Secrets and variables → Actions → New repository secret" 47 | exit 1 48 | fi 49 | echo "MaxMind secrets are configured" 50 | 51 | - name: Set up Docker Buildx 52 | uses: docker/setup-buildx-action@v3 53 | 54 | - name: Login to GitHub Container Registry 55 | uses: docker/login-action@v3 56 | with: 57 | registry: ${{ env.REGISTRY }} 58 | username: ${{ github.repository_owner }} 59 | password: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | - name: Extract metadata for Docker 62 | id: meta 63 | uses: docker/metadata-action@v5 64 | with: 65 | images: ${{ env.IMAGE_NAME }} 66 | tags: | 67 | type=raw,value=latest,enable={{is_default_branch}} 68 | type=sha,prefix={{branch}}- 69 | type=schedule,pattern={{date 'YYMMDD'}} 70 | 71 | - name: Build and push Docker image with daily updated GeoIP databases 72 | uses: docker/build-push-action@v5 73 | with: 74 | context: . 75 | file: ./Dockerfile 76 | platforms: linux/amd64 77 | push: true 78 | tags: ${{ steps.meta.outputs.tags }} 79 | labels: ${{ steps.meta.outputs.labels }} 80 | build-args: | 81 | GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }} 82 | GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }} 83 | cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache 84 | cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=max 85 | 86 | - name: Image digest 87 | run: echo ${{ steps.meta.outputs.digest }} 88 | -------------------------------------------------------------------------------- /src/services/geoip_service.py: -------------------------------------------------------------------------------- 1 | import maxminddb 2 | import os 3 | import time 4 | import logging 5 | from typing import Optional, Dict, Union 6 | from src.configs.path_database import PATH_DATABASE 7 | from src.types.ip_geolocaltion import IPGeoLocation 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class GeoIPService: 13 | """Service for looking up IP geolocation using MaxMind databases""" 14 | 15 | def __init__(self): 16 | self.lookup_city: Optional[maxminddb.Reader] = None 17 | self.lookup_country: Optional[maxminddb.Reader] = None 18 | self.lookup_asn: Optional[maxminddb.Reader] = None 19 | self.sync_database() 20 | 21 | def sync_database(self): 22 | """Load all GeoIP databases""" 23 | logger.info("Loading GeoIP databases...") 24 | self.lookup_city = self.load_database(os.path.join(PATH_DATABASE, "GeoLite2-City.mmdb")) 25 | self.lookup_country = self.load_database(os.path.join(PATH_DATABASE, "GeoLite2-Country.mmdb")) 26 | self.lookup_asn = self.load_database(os.path.join(PATH_DATABASE, "GeoLite2-ASN.mmdb")) 27 | logger.info("✓ All databases loaded") 28 | 29 | def load_database(self, path: str) -> maxminddb.Reader: 30 | """Load a MaxMind database with retry logic""" 31 | database_name = os.path.basename(path) 32 | retry_count = 0 33 | 34 | while True: 35 | try: 36 | return maxminddb.open_database(path) 37 | except Exception as error: 38 | retry_count += 1 39 | logger.warning(f"{database_name} failed (attempt #{retry_count}): {error}") 40 | time.sleep(1) 41 | 42 | def lookup(self, ip: str) -> Union[IPGeoLocation, Dict[str, str]]: 43 | """Lookup geolocation information for an IP address""" 44 | if not self.lookup_city or not self.lookup_country or not self.lookup_asn: 45 | return {"error": "Databases not loaded"} 46 | 47 | try: 48 | city_info = self.lookup_city.get(ip) 49 | country_info = self.lookup_country.get(ip) 50 | asn_info = self.lookup_asn.get(ip) 51 | 52 | if not city_info and not country_info and not asn_info: 53 | return {"error": "IP not found"} 54 | 55 | # Extract data safely with fallbacks 56 | continent = "" 57 | continent_code = "" 58 | country = "" 59 | country_code = "" 60 | currency = "" 61 | 62 | if country_info: 63 | continent = country_info.get("continent", {}).get("names", {}).get("en", "") 64 | continent_code = country_info.get("continent", {}).get("code", "") 65 | country = country_info.get("country", {}).get("names", {}).get("en", "") 66 | country_code = country_info.get("country", {}).get("iso_code", "") 67 | currency = country_info.get("country", {}).get("currency", "") 68 | 69 | # Extract city info 70 | region = "" 71 | region_name = "" 72 | city = "" 73 | district = "" 74 | zip_code = "" 75 | lat = 0.0 76 | lon = 0.0 77 | timezone = "" 78 | offset = 0 79 | 80 | if city_info: 81 | subdivisions = city_info.get("subdivisions", []) 82 | if subdivisions: 83 | region = subdivisions[0].get("iso_code", "") 84 | region_name = subdivisions[0].get("names", {}).get("en", "") 85 | if len(subdivisions) > 1: 86 | district = subdivisions[1].get("names", {}).get("en", "") 87 | 88 | city = city_info.get("city", {}).get("names", {}).get("en", "") 89 | zip_code = city_info.get("postal", {}).get("code", "") 90 | 91 | location = city_info.get("location", {}) 92 | lat = location.get("latitude", 0.0) 93 | lon = location.get("longitude", 0.0) 94 | timezone = location.get("time_zone", "") 95 | offset = location.get("accuracy_radius", 0) 96 | 97 | # Extract ASN info 98 | isp = "" 99 | org = "" 100 | as_number = "" 101 | asname = "" 102 | 103 | if asn_info: 104 | asn_org = asn_info.get("autonomous_system_organization", "") 105 | asn_num = asn_info.get("autonomous_system_number", "") 106 | 107 | isp = asn_org 108 | org = asn_org 109 | as_number = f"AS{asn_num} {asn_org}" 110 | asname = asn_org 111 | 112 | # Return Pydantic model instance 113 | return IPGeoLocation( 114 | query=ip, 115 | status="success", 116 | continent=continent, 117 | continentCode=continent_code, 118 | country=country, 119 | countryCode=country_code, 120 | region=region, 121 | regionName=region_name, 122 | city=city, 123 | district=district, 124 | zip=zip_code, 125 | lat=lat, 126 | lon=lon, 127 | timezone=timezone, 128 | offset=offset, 129 | currency=currency, 130 | isp=isp, 131 | org=org, 132 | as_=as_number, 133 | asname=asname, 134 | ) 135 | 136 | except ValueError: 137 | return {"error": "Invalid IP address format"} 138 | except Exception as e: 139 | logger.error(f"Lookup error for {ip}: {str(e)}") 140 | return {"error": f"Lookup error: {str(e)}"} 141 | 142 | 143 | # Create singleton instance 144 | geoip_service = GeoIPService() 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeoIP Server 2 | 3 | [![Daily Docker Build](https://github.com/ntthanh2603/geoip-server/actions/workflows/docker-build.yml/badge.svg)](https://github.com/ntthanh2603/geoip-server/actions/workflows/docker-build.yml) 4 | [![Docker Image](https://img.shields.io/badge/docker-ghcr.io-blue?logo=docker)](https://github.com/ntthanh2603/geoip-server/pkgs/container/geoip-server) 5 | [![Python](https://img.shields.io/badge/python-3.11-blue?logo=python)](https://www.python.org/) 6 | [![FastAPI](https://img.shields.io/badge/FastAPI-0.109+-00a393?logo=fastapi)](https://fastapi.tiangolo.com/) 7 | [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) 8 | 9 | A lightweight FastAPI service that provides IP geolocation data using MaxMind's GeoLite2 databases. 10 | 11 | ## ✨ Features 12 | 13 | - 🌍 **IP Geolocation Lookup** - Detailed location data for any IP address 14 | - 🔄 **Daily Auto Updates** - GeoIP databases refreshed daily via GitHub Actions 15 | - 🐳 **Docker Ready** - Pre-built images on GitHub Container Registry 16 | - 🚀 **Fast API** - Built with FastAPI for high performance 17 | - 📊 **Bulk Lookup** - Query multiple IPs in a single request 18 | - 📚 **Auto Documentation** - Swagger UI and ReDoc included 19 | - 🔒 **Type Safe** - Full Pydantic validation 20 | 21 | ## 🚀 Quick Start 22 | 23 | ### Using Docker (Recommended) 24 | 25 | Pull and run the pre-built image from GitHub Container Registry: 26 | 27 | ```bash 28 | docker pull ghcr.io/ntthanh2603/geoip-server:latest 29 | docker run -p 4360:4360 ghcr.io/ntthanh2603/geoip-server:latest 30 | ``` 31 | 32 | Access the API at: **http://localhost:4360** 33 | 34 | ### Using Docker Compose 35 | 36 | ```bash 37 | git clone https://github.com/ntthanh2603/geoip-server.git 38 | cd geoip-server 39 | cp .env.example .env 40 | # Edit .env with your MaxMind credentials 41 | docker-compose up -d 42 | ``` 43 | 44 | ## 📡 API Endpoints 45 | 46 | | Endpoint | Method | Description | Example | 47 | |----------|--------|-------------|---------| 48 | | `/` | GET | Get client's IP address | `curl http://localhost:4360/` | 49 | | `/{ip}` | GET | Lookup single IP | `curl http://localhost:4360/8.8.8.8` | 50 | | `/bulk/{ips}` | GET | Lookup multiple IPs | `curl http://localhost:4360/bulk/8.8.8.8,1.1.1.1` | 51 | | `/docs` | GET | Swagger UI documentation | Open in browser | 52 | | `/redoc` | GET | ReDoc documentation | Open in browser | 53 | 54 | ## 📄 Response Format 55 | 56 | ```json 57 | { 58 | "query": "8.8.8.8", 59 | "status": "success", 60 | "continent": "North America", 61 | "continentCode": "NA", 62 | "country": "United States", 63 | "countryCode": "US", 64 | "region": "CA", 65 | "regionName": "California", 66 | "city": "Mountain View", 67 | "district": "", 68 | "zip": "94035", 69 | "lat": 37.386, 70 | "lon": -122.0838, 71 | "timezone": "America/Los_Angeles", 72 | "offset": 5, 73 | "currency": "USD", 74 | "isp": "Google LLC", 75 | "org": "Google LLC", 76 | "as": "AS15169 Google LLC", 77 | "asname": "Google LLC" 78 | } 79 | ``` 80 | 81 | ## 🛠️ Development Setup 82 | 83 | ### Prerequisites 84 | 85 | Get a MaxMind license key (free): 86 | - Sign up: https://www.maxmind.com/en/geolite2/signup 87 | - Generate license key: https://www.maxmind.com/en/accounts/current/license-key 88 | 89 | ### Docker Development 90 | 91 | ```bash 92 | # Build and run 93 | docker-compose up --build 94 | 95 | # Run in background 96 | docker-compose up -d 97 | 98 | # View logs 99 | docker-compose logs -f 100 | 101 | # Stop 102 | docker-compose down 103 | ``` 104 | 105 | 106 | ## 📦 GitHub Container Registry 107 | 108 | Pre-built Docker images with daily updated GeoIP databases are available at: 109 | **[ghcr.io/ntthanh2603/geoip-server](https://github.com/ntthanh2603/geoip-server/pkgs/container/geoip-server)** 110 | 111 | ### Available Tags 112 | 113 | | Tag | Description | Update Frequency | 114 | |-----|-------------|------------------| 115 | | `latest` | Latest stable build | On every push to main | 116 | | `YYMMDD` | Daily snapshot | Daily at 00:00 UTC | 117 | | `main-` | Specific commit | On demand | 118 | 119 | ### Usage 120 | 121 | ```bash 122 | # Latest version 123 | docker pull ghcr.io/ntthanh2603/geoip-server:latest 124 | 125 | # Specific date (e.g., Nov 2, 2025) 126 | docker pull ghcr.io/ntthanh2603/geoip-server:251102 127 | ``` 128 | 129 | ## ⚙️ GitHub Actions 130 | 131 | ### Automated Workflows 132 | 133 | | Workflow | Schedule | Purpose | 134 | |----------|----------|---------| 135 | | **Daily Docker Build** | Daily at 00:00 UTC | Update GeoIP databases & build images | 136 | 137 | ### Setup GitHub Actions 138 | 139 | To enable automated builds: 140 | 141 | 1. Navigate to: **Settings** → **Secrets and variables** → **Actions** 142 | 2. Add repository secrets: 143 | 144 | ``` 145 | Name: GEOIPUPDATE_ACCOUNT_ID 146 | Value: 147 | 148 | Name: GEOIPUPDATE_LICENSE_KEY 149 | Value: 150 | ``` 151 | 152 | 3. Go to **Actions** tab → Select workflow → **Run workflow** 153 | 154 | The workflow will: 155 | - ✅ Verify secrets 156 | - ✅ Download latest GeoIP databases 157 | - ✅ Build Docker image 158 | - ✅ Push to GitHub Container Registry 159 | 160 | ## 📂 Project Structure 161 | 162 | ``` 163 | geoip-server/ 164 | ├── .github/workflows/ # CI/CD workflows 165 | │ └── docker-build.yml # Daily build automation 166 | ├── src/ 167 | │ ├── app.py # FastAPI application 168 | │ ├── configs/ 169 | │ │ └── path_database.py # Database path config 170 | │ ├── controlers/ 171 | │ │ └── geoip_controler.py # API endpoints 172 | │ ├── services/ 173 | │ │ └── geoip_service.py # GeoIP lookup logic 174 | │ └── types/ 175 | │ └── ip_geolocaltion.py # Pydantic models 176 | ├── main.py # Entry point 177 | ├── Dockerfile # Docker configuration 178 | ├── docker-compose.yml # Compose configuration 179 | ├── requirements.txt # Python dependencies 180 | └── .env.example # Environment template 181 | ``` 182 | 183 | ## 🛠️ Technologies 184 | 185 | | Technology | Purpose | 186 | |-----------|---------| 187 | | **FastAPI** | Modern Python web framework | 188 | | **MaxMind GeoLite2** | IP geolocation databases | 189 | | **Pydantic** | Data validation | 190 | | **Docker** | Containerization | 191 | | **GitHub Actions** | CI/CD automation | 192 | 193 | ## 📝 License 194 | 195 | This project uses GeoLite2 data created by MaxMind, available from [MaxMind](https://www.maxmind.com). 196 | 197 | ## 🤝 Contributing 198 | 199 | Contributions are welcome! Please feel free to submit a Pull Request. 200 | 201 | ## 📧 Support 202 | 203 | - **Issues:** [GitHub Issues](https://github.com/ntthanh2603/geoip-server/issues) 204 | - **Discussions:** [GitHub Discussions](https://github.com/ntthanh2603/geoip-server/discussions) 205 | 206 | --- 207 | 208 |
209 | 210 | **[⭐ Star this repo](https://github.com/ntthanh2603/geoip-server)** if you find it useful! 211 | 212 | Made with ❤️ using FastAPI and MaxMind GeoLite2 213 | 214 |
215 | --------------------------------------------------------------------------------