Skip to main content
AnalyticsLearner

Module 6

Self-hosted server-side GTM on a VPS with Docker + Caddy

Deploy the Google server-side GTM tagging container on a VPS using Docker and Caddy as the reverse proxy. The advanced reference for client work, not the architecture this site uses.

sgtmdockercaddyself-hostedvps

Architecture overview

Self-hosting the sGTM tagging server on a VPS gives you a first-party subdomain for the sGTM endpoint (gtm.yourdomain.com) without paying a managed sGTM fee. Caddy handles TLS automatically via Let's Encrypt.

Note: This site (analyticslearner.com) sends conversions directly from the Next.js app via the Measurement Protocol and Meta CAPI rather than running sGTM. This entry is an advanced reference for client engagements where sGTM is the right architectural choice.

VPS requirements

  • 2 vCPU / 4 GB RAM minimum (2 GB swap recommended for next build if co-hosting a Next.js app)
  • Ubuntu 22.04 or 24.04
  • Ports 80 and 443 open
  • DNS: A records for both yourdomain.com and gtm.yourdomain.com pointing to the VPS IP

Docker Compose

# docker-compose.yml
version: '3.8'
 
services:
  sgtm-tagging:
    image: gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable
    restart: unless-stopped
    environment:
      - CONTAINER_CONFIG=${CONTAINER_CONFIG}
      - PORT=8080
    ports:
      - '127.0.0.1:8080:8080' # Only bind to localhost - Caddy proxies externally
 
  sgtm-preview:
    image: gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable
    restart: unless-stopped
    environment:
      - CONTAINER_CONFIG=${CONTAINER_CONFIG}
      - RUN_AS_PREVIEW_SERVER=true
      - PORT=8081
    ports:
      - '127.0.0.1:8081:8081'

Create .env next to docker-compose.yml with:

CONTAINER_CONFIG=your_base64_encoded_container_config_string

Get the container config string from GTM Admin: Admin → Server container → Configure manually.

Caddyfile

# /etc/caddy/Caddyfile
 
gtm.yourdomain.com {
    reverse_proxy localhost:8080
 
    # Optional: log access for debugging
    log {
        output file /var/log/caddy/sgtm-access.log
    }
}
 
gtm-preview.yourdomain.com {
    reverse_proxy localhost:8081
}
 
yourdomain.com {
    reverse_proxy localhost:3000    # Next.js (pm2 / systemd)
}

Caddy fetches and renews TLS certificates automatically. No certbot or manual renewal required.

Systemd service for Next.js

# /etc/systemd/system/nextjs.service
[Unit]
Description=Next.js production server
After=network.target
 
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/yourdomain
ExecStart=/usr/bin/node node_modules/.bin/next start
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3000
 
[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl enable nextjs
sudo systemctl start nextjs

Bring up the stack

# 1. Install Docker and Docker Compose
curl -fsSL https://get.docker.com | sh
sudo apt install docker-compose-plugin -y
 
# 2. Start sGTM containers
cd /var/www/sgtm
docker compose up -d
 
# 3. Verify both containers are running
docker compose ps
 
# 4. Reload Caddy to pick up the config
sudo systemctl reload caddy
 
# 5. Test the sGTM endpoint
curl -I https://gtm.yourdomain.com/healthz
# Expected: HTTP/2 200

Connecting the GTM web container to the sGTM server

In the GTM UI, configure the web container's Google Tag to use the custom sGTM endpoint:

Server container URL: https://gtm.yourdomain.com

Or in the GTM snippet in your HTML:

(function (w, d, s, l, i) {
  w[l] = w[l] || [];
  w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
  var f = d.getElementsByTagName(s)[0],
    j = d.createElement(s),
    dl = l != 'dataLayer' ? '&l=' + l : '';
  j.async = true;
  // Use self-hosted endpoint instead of googletagmanager.com
  j.src = 'https://gtm.yourdomain.com/gtm.js?id=' + i + dl;
  f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-XXXXXXX');

Cost

On a Hetzner CX22 (2 vCPU / 4 GB, EUR 3.29/mo), the full stack (Next.js site + sGTM tagging container + sGTM preview container + Caddy) runs comfortably within the resource budget.

The free Server-Side GTM Base Container includes a pre-built sGTM container configuration ready to deploy with this setup.

Free container for this module

Server-Side GTM Base Container

A production-ready server-side GTM container configuration. Includes a GA4 client, GA4 event tag, and the routing setup needed to receive web container hits at your custom domain.

Get the container →