knowing the geolocation of your site's users is handy thing. maybe you want to force your canadian users into a degraded, second-rate version of your ecommerce site, or maybe you want to redirect people from brazil to a frontend you ran through google translate, or maybe you just want to block the netherlands because you hate the dutch. there are reasons.
traditionally, this gets done by calling a third-party geolocation api. you gotta fiddle with api keys and manage rate limits and write a bunch of code. or... we could just let nginx do it all for us.
in this post we're going to go over how to do ip geolocation for country and city in nginx and get that data into our web app where we can use it. all of this was written for ubuntu-like systems runing nginx 1.18.0
.
doing geolocation in your httpd
test if have the necessary nginx
module
geopip lookup in nginx is done by the http-geoip2
module. on ubuntu-like systems this module usually comes pre-installed, although ymmv.
to test if our nginx has http-geoip2
installed, we can run:
nginx -V 2>&1 | sed -n "/http-geoip2/p"
this just takes the output of nginx's version data and tests if http-geoip2
is listed in it. if our nginx has the module, we will see output that looks something like.
nginx version: nginx/1.18.0 (Ubuntu)
built with OpenSSL 3.0.2 15 Mar 2022
TLS SNI support enabled
configure arguments: --with-cc-opt='-g -O2 <snip> --with-threads --add-dynamic-module=/build/nginx-d8gVax/nginx-1.18.0/debian/modules/http-geoip2 <snip>
if we don't have the module, the output will be blank.
get the maxmind databases
maxmind is technically a purveyor of anti-fraud software, but the thing they're really known for is compiling and distributing ip-to-location databases. these are made available as either plain ol' csvs or in a 'custom but open' binary format that has an associated c library for speedy searches. we're going to use the binary format.
let's install the c library (and associated executable) first. on ubuntu-like systems this is just an apt
away:
sudo add-apt-repository ppa:maxmind/ppa
sudo apt update
sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin
on non-ubuntu-based distros, we'll be stuck with compiling it from source like it's the nineties.
getting the databases themselves is actually more work. first, we have to sign up for an account on maxmind and jump through the email verification and 2fa login hoops.
once we have our account and have signed in, on our 'account summary' page there will be a text link that reads 'download databases'. clicking that leads to a list of downloadables. the ones we want are:
GeoLite2 City
GeoLite2 Country
make sure you get the format 'GeoIP2 Binary (.mmdb)'. these are tarballs. download them to the server where you are running nginx.
untarring the archives is straightforward:
sudo tar -zxvf GeoLite2-City_20240604.tar.gz
sudo tar -zxvf GeoLite2-Country_20240604.tar.gz
next, we will make a directory to store the mmdb
files. i like to put these things in /var
, which is maybe contentious since the os doesn't write to them, but they are, technically, databases. and databases go in /var
.
sudo mkdir /var/maxmind
we then put just the mmdb
files in there.
sudo mv ./GeoLite2-City_20240604/GeoLite2-City.mmdb /var/maxmind
sudo mv ./GeoLite2-Country_20240604/GeoLite2-Country.mmdb /var/maxmind
we now have our geolocation databases and the library to query it.
fun fact: you can lookup ip locations on the command line
along with the c library for mmdb, which nginx needs, we also installed mmdblookup
, an executable that can query the geoip database. it takes two arguments: the --file
where the database lives, and the --ip
we want to lookup.
mmdblookup --file /var/maxmind/GeoLite2-City.mmdb --ip <some ip address>
that should spit out a small moutain of information about the ip location in a format that looks like json, but is not actually json.
if we want to narrow our output, we can specify a path to the desired data in the command. for instance, looking at the not-quite-json output, we see there is a city
object which contains a names
object which has a value keyed at en
for the english name of the city. we can get just that english city name by adding the path.
mmdblookup --file /var/maxmind/GeoLite2-City.mmdb --ip <some ip address> city names en
configure nginx.conf
to do the actual lookup
nginx will do our geolocation lookup automatically, but first we need to configure it so it knows things like where we stored those mmdb
databases and what data we want to extract from them. we'll do this in the http
block of our nginx.conf
file.
##
# GeoIp country
##
geoip2 /var/maxmind/GeoLite2-Country.mmdb {
$geoip2_data_country_name country names en;
$geoip2_data_country_code country iso_code;
}
##
# GeoIp city
##
geoip2 /var/maxmind/GeoLite2-City.mmdb {
$geoip2_data_city_name city names en;
}
here, we have the geoip2
directive entered twice for two different databases: the country one and the city one. both of these directives will be run.
the body of each of those blocks, the stuff in the curly braces, is variable assignments. if we recall, when we were using mmdblookup
in the 'fun fact' section above, we could pass a path to the value we wanted in the record. here we're taking a variable name, ie $geoip2_data_city_name
and assigning it to the value at the path city names en
for the record for the current ip address. of course we can use any variable name and any valid path we want here, but city name and, country name and iso code are probably the most useful.
this is all that's required for nginx to extract geolocation data for an ip.
note: the complete nginx.conf
file is at the bottom of this post, because providing snippets without context is Not Cool.
configure our virtual host to make our geodata available
of course extracting geolocation data is only useful if we can get it into our script.
we're going to do that by taking the variables geoip2
made in our nginx.conf
file and setting them as fastcgi_params
in the location
block of our virtual host. here's an example
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
###
# geoip2 variables
fastcgi_param COUNTRY_CODE $geoip2_data_country_code;
fastcgi_param COUNTRY_NAME $geoip2_data_country_name;
fastcgi_param CITY_NAME $geoip2_data_city_name;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}
if you've ever looked at an nginx configuration for a php site before, this location
block should look somewhat familiar. the important addition here, of course, is where we call fastcgi_param
and pass it two arguments: first, the name of the fastcgi
paramater we want to declare and, second, the name of the geoip2
variable we set in nginx.conf
.
get location data in one line of php
here's a fun fact about fastcgi_param
s: they're available in php's $_SERVER
array.
with that in mind, let's write a one line php script (not including the opening tag) that will display the country name of the user:
<?php
print $_SERVER['COUNTRY_NAME'];
if we serve this script on our geolocation-enabled nginx, behold: we will see the country name associated with the user's ip address.
addendum 1: the complete nginx.conf
the complete nginx.conf used in the examples above is available as a gist.
addendum 2: the complete virutal host configuration
the virtual hosts file used for the examples with the following elements removed
server_name
root
php version
access log name
error log name
๐ this post was originally written in the grant horwood technical blog