Self-Hosted Google Fonts with nginx

4 min read 657 words Source

Google Fonts routes your visitors’ IPs through fonts.googleapis.com and fonts.gstatic.com on every page load — there’s no opt-out if you use the hosted CSS directly. I built a small nginx Docker image that sits in front of Google’s font infrastructure, rewrites the CSS on the fly, and serves everything from your own domain — visitors never connect to Google directly.

The image is published on Docker Hub as sbstjn/fonts; sources are on Codeberg.

How It Works

Google Fonts involves two separate hosts: fonts.googleapis.com serves the CSS stylesheet that lists the font faces, and fonts.gstatic.com serves the actual font binary files (woff2, etc.). The CSS contains URLs like //fonts.gstatic.com/s/dmsans/…. If you point a visitor at the CSS but let them fetch the font files directly from gstatic, you’ve only moved half the problem.

google-fonts-proxy handles both hosts:

  • CSS endpoint (/css2) — proxied to fonts.googleapis.com/css2. nginx rewrites the response body with sub_filter to replace every //fonts.gstatic.com/ with //your-host/, so the stylesheet now points at your proxy for font files too.
  • Font files (/s/…) — proxied to fonts.gstatic.com/s/…. Binary pass-through; no rewriting needed.

The nginx template handles both locations:

location = ${FONTS_CSS_LOCATION} {
    proxy_pass https://fonts.googleapis.com/css2;
    proxy_set_header Host fonts.googleapis.com;
    proxy_set_header Accept-Encoding "";

    proxy_hide_header Cache-Control;
    proxy_hide_header Expires;
    proxy_hide_header ETag;
    proxy_hide_header Last-Modified;
    add_header Cache-Control "public, max-age=31536000, immutable" always;

    sub_filter_types text/css;
    sub_filter '//fonts.gstatic.com/' '//${FONTS_PUBLIC_HOST}${FONTS_PATH_PREFIX}/';
    sub_filter_once off;
}

location ^~ ${FONTS_STATIC_LOCATION_PREFIX}/ {
    proxy_pass ${FONTS_STATIC_PROXY_PASS};
    proxy_set_header Host fonts.gstatic.com;

    proxy_hide_header Cache-Control;
    proxy_hide_header Expires;
    add_header Cache-Control "public, max-age=31536000, immutable" always;
}

The template variables (${FONTS_CSS_LOCATION}, ${FONTS_PUBLIC_HOST}, etc.) are substituted at container start by the official nginx entrypoint’s envsubst step, driven by environment variables.

Getting Started

Build and run the image locally. Set FONTS_PUBLIC_HOST to the hostname clients will use to reach the proxy — this is the value written into the rewritten CSS:

$ > docker build -t google-fonts-proxy .

$ > docker run \
    -p 80:80 \
    -e FONTS_PUBLIC_HOST=fonts.example.com \
    google-fonts-proxy

With the container running, point your site’s font stylesheet at the proxy instead of Google directly. The URL format is identical to the Google Fonts CSS API:

<!-- Before: direct to Google -->
<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700&display=swap"
/>

<!-- After: through your proxy -->
<link
  rel="preconnect"
  href="https://fonts.example.com"
  crossorigin
/>
<link
  rel="stylesheet"
  href="https://fonts.example.com/css2?family=DM+Sans:wght@400;700&display=swap"
/>

The proxy fetches the CSS from Google, rewrites the font file URLs to point at itself, and the browser fetches fonts from your domain — never touching Google’s infrastructure.

The Docker Hub image is available without building locally:

$ > docker pull sbstjn/fonts

Environment Variables

The container is configured via four environment variables:

VariableDefaultPurpose
FONTS_PUBLIC_HOSTfonts.example.comHostname written into rewritten CSS
FONTS_PUBLIC_PATH/URL path prefix. / serves /css2 and /s/…. /fonts shifts everything under /fonts/css2 and /fonts/…
FONTS_PORT80Port nginx listens on
FONTS_REDIRECT_HOME(unset)If set, GET / returns 301 to this URL

Deploying with OpenTofu on Scaleway

I deploy this image as a Scaleway Serverless Container with minimal resources — the OpenTofu configuration is straightforward:

resource "scaleway_container" "fonts" {
  name         = "fonts"
  namespace_id = scaleway_container_namespace.main.id
  registry_image  = "${scaleway_container_namespace.main.registry_endpoint}/fonts:latest-amd64"
  port         = 80
  cpu_limit    = 100
  memory_limit = 128
  min_scale    = 1
  max_scale    = 1
  timeout      = 10

  http_option = "redirected"
  privacy     = "public"
  deploy      = true

  environment_variables = {
    FONTS_PUBLIC_HOST = "fonts.example.com"
    FONTS_REDIRECT_HOME = "https://example.com/"
  }
}

Push the image to your Scaleway container registry before applying. The container registry and namespace setup is covered in Deploy Serverless Containers to Scaleway with OpenTofu.

That’s It! 🎉

google-fonts-proxy routes all font traffic through your own infrastructure. Google only ever sees the proxy server’s IP — client IPs stay private and your visitors get the same fonts without any change to the user experience.

The image is available on Docker Hub (sbstjn/fonts) and the sources are on Codeberg.