Server-side GTM on Cloud Run with your own domain: the setup and the failure that shows up in no log
I set up a tagging server on Google Cloud Run, with my own domain and without going through a managed provider. This is the setup in order, what I decided at each step, and the failure that cost me the most time, which wasn’t any of the ones I expected.
What I’m building
A server-type GTM container, deployed on Cloud Run, reachable at sgtm.yourdomain.com. The browser sends GA4 hits to that first-party subdomain instead of to google-analytics.com, and the server forwards them to GA4.
The steps, in the order that matters
1. Provisioning from GTM. When you create the server container, GTM can provision the Cloud Run service for you automatically. It creates the Google Cloud project for you. Mine ended up with an auto-generated name and that’s the one I use; you don’t need to have one beforehand.
2. Region. I chose us-central1, the region with a free egress tier. It’s far from Spain, but latency doesn’t matter: the browser sends an asynchronous beacon and doesn’t wait for the response to render anything.
3. Billing and scaling. Request-based billing (cpu-throttling on), minimum instances at 0 and maximum at 1. With that combination the service charges nothing while nobody uses it and can’t grow out of control. Add a budget alert from day one; mine is €5, with notices at 50, 90 and 100%.
4. Custom domain. Cloud Run lets you map a domain to the service. You need the domain verified in Search Console as a Domain property. Then create a sgtm CNAME pointing to ghs.googlehosted.com. If your DNS is on Cloudflare, leave it as DNS only (no proxy), as I did, so Google can validate the domain and issue the certificate itself.
5. Certificate. Google issues it on its own. The documentation says up to 24 hours; for me it took about an hour. Don’t touch anything in the meantime.
6. Web GTM. On the Google tag, add server_container_url with your subdomain and first_party_collection set to true.
7. Server GTM. A GA4 client (with the default paths) and a GA4 tag that fires on all of the client’s events. Publish both containers.
That’s as far as the guides go. In my case everything was green and not a single real hit arrived.
The failure: the request never left the browser
Direct tests against the server returned 200. gtm.js loaded with 200. The container was published. And the Cloud Run logs showed no visitor traffic, only my manual tests.
The cause wasn’t on the server, which is why it didn’t show up in its logs. My site’s Content-Security-Policy didn’t include https://sgtm.yourdomain.com in connect-src. The browser blocked the fetch to /g/collect before sending it. From the server’s point of view, that request simply doesn’t exist. The error was only in the browser console:
Refused to connect to 'https://sgtm.yourdomain.com/g/collect…'
because it violates the document's Content Security Policy.
It’s an especially treacherous failure because gtm.js does load (it comes from googletagmanager.com, which you usually have allowed) and everything looks like it works. The fix is one line: add your subdomain to connect-src.
How to verify that it really works
A synthetic test against the API proves nothing. To call the setup done:
- Open your site in a fresh window with DevTools open and the console visible.
- Check that there are no CSP errors and that a request to
sgtm.yourdomain.com/g/collectappears with status 200. - Look at the Cloud Run logs: the same request must show up, with your site as the referer.
- Confirm the event in GA4 → Realtime.
If any of the four is missing, the setup isn’t finished.
What to watch afterwards
- That minimum instances stay at 0. An always-on instance costs tens of euros a month; it’s the easiest way to turn a free setup into a paid one without noticing.
- That maximum instances stay capped, so an abnormal spike can’t blow up the bill.
- The budget alert, which is the only thing that warns you if any of the above changes.
I documented it as a case study with the full context: own tagging server on Cloud Run.