Why I worked on this
I run a handful of small web services on my Proxmox box—nothing fancy, just a handful of Docker containers behind Traefik. One evening I noticed a steady trickle of login attempts on a private service that should never see traffic from outside my country. The requests came from IPs that MaxMind pinned to places I’ve never been. I already had fail2ban running, but I wanted to cut the noise earlier, at the edge, before the request even reached the application. Geo-blocking seemed like the cleanest way to do it.
My real setup or context
- Traefik 3.0 running in a Docker container on a single Proxmox VM.
- Services declared in separate
compose.ymlfiles on the same custom external network calledtraefik-proxy. - Let’s Encrypt wildcard cert handled by Traefik via Cloudflare DNS challenge.
- I already used labels for routing; I wanted to keep the geo-block config in the same style—no separate Apache, no pfSense, just Traefik.
What worked (and why)
1. Picked the nscuro plugin
I tried the traefik-plugin-geoblock because it’s a pure Go plugin and doesn’t need Pilot anymore. The static config lives in my traefik.yml:
experimental:
plugins:
geoblock:
moduleName: github.com/nscuro/traefik-plugin-geoblock
version: v0.11.0
2. Mounted the GeoIP2 database
MaxMind’s free “GeoLite2-Country” mmdb is enough. I created a folder on the host:
mkdir -p /opt/traefik/geoip
Downloaded the mmdb from my MaxMind account (you need one, but it’s free) and placed it there. Then bind-mounted it into the Traefik container:
volumes: - /opt/traefik/geoip:/geoip:ro
3. Declared the middleware in a file provider
I keep dynamic config in /opt/traefik/dynamic/geoip.yml:
http:
middlewares:
geo-allow-de:
plugin:
geoblock:
enabled: true
databaseFilePath: /geoip/GeoLite2-Country.mmdb
allowedCountries: ["DE"]
allowPrivate: true
disallowedStatusCode: 403
The file provider is loaded by adding this line to traefik.yml:
providers:
file:
directory: /dynamic
watch: true
Bind-mounted that folder too:
volumes: - /opt/traefik/dynamic:/dynamic:ro
4. Attached the middleware with a label
On any service that should be Germany-only I just add:
labels: - traefik.http.routers.myapp.middlewares=geo-allow-de@file
Reload the container and Traefik picks it up within seconds. Requests from elsewhere get a plain 403, no further logs in the app.
5. Verified with a VPN
I connected to a server in the Netherlands, hit the endpoint, and got 403. Disconnected, hit again from my German IP, and the page loaded. That was enough confirmation for me.
What didn’t work
- First I forgot the
allowPrivate: trueflag and my own LAN IPs (192.168.x.x) started receiving 403s. Took me a moment to realise the plugin treats private ranges as “unknown country” unless explicitly allowed. - I initially tried to stuff the middleware definition into a Docker label as a multi-line string. Traefik swallowed it without error but never applied the plugin. File provider is the only way that actually worked.
- The plugin caches the mmdb on start-up; if you swap the file you must restart the Traefik container. A simple config hot-reload isn’t enough.
Key takeaways
- Geo-blocking at the reverse-proxy layer is straightforward once you accept that Traefik plugins need a file provider—labels alone won’t do.
- MaxMind’s free country database is accurate enough for casual use; updates arrive monthly and I automate the download with a cron job that restarts Traefik afterwards.
- Always whitelist private ranges unless you want to lock yourself out while debugging from the couch.
- The plugin returns a clean 403, so failed requests never hit your application logs—exactly the noise reduction I wanted.