Custom domains for Railway PR environments, the easy way
Railway creates a preview environment for every pull request. This is great for testing changes before they ship. By default, each PR gets a *.up.railway.app URL.
But sometimes you want your own domain instead. Something like pr-234.pr.staging.mydomain.com. It looks cleaner, and it avoids cross-origin and cookie issues that come with the default Railway domain.
This is where things get tricky.
The problem
The obvious idea is to set one wildcard DNS record and be done:
*.pr.staging.mydomain.com CNAME your-railway-target
You would expect every PR domain to just work after that. It does not.
Railway only routes a hostname to a service if that exact domain is attached to a specific service and environment. The wildcard CNAME resolves fine, but Railway returns a 404 for any host it does not know about. On top of that, a custom domain needs a TXT record to verify you own it. A wildcard CNAME cannot answer that.
Each PR is its own environment. So you end up attaching the domain and adding DNS records again for every single PR. That is a lot of manual work, and it does not fit a CI/CD pipeline.
You can already attach the domain from your pipeline with the Railway CLI. The missing piece is the DNS side: that TXT verification record your DNS provider needs for each new PR domain.
The solution: buy the domain on Railway
The clean fix is to buy (or transfer) the domain directly on Railway.
When Railway is your registrar, it also manages your DNS. That changes everything. Railway already knows you own the domain, so there is no TXT step. And when you attach a subdomain to a service, Railway writes the DNS records for you, automatically.
So your pipeline only needs one step: attach the domain. No DNS edits, no verification records, nothing to script on your side.
Here is what it looks like in a GitHub Action:
- name: Attach PR preview domain
run: |
railway domain pr-${{ github.event.number }}.pr.staging.mydomain.com \
--service frontend \
--port 8080
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
That is it. The CLI attaches pr-234.pr.staging.mydomain.com to your service, Railway configures the DNS, and provisions the SSL certificate. The domain is live in moments.
This works with any pipeline that can run a shell command. GitLab CI, CircleCI, a plain script, whatever you use.
Why this is nice
- One command per PR, no DNS work.
- No
TXTverification record to manage. - SSL is handled for you.
- It fits naturally into the pipeline you already have.
Why this is great for Clerk
This setup really shines if you use Clerk for authentication.
Clerk is picky about domains, and for good reason. A production Clerk instance only trusts its own domain. Session cookies are scoped to it, and sign-in redirects are only allowed on it. So a preview deployment on a random *.up.railway.app URL is a stranger to Clerk. Auth simply breaks. You would either have to register every preview URL by hand, or run a separate dev instance.
A predictable subdomain under your real domain fixes this. When your PR lives at pr-234.pr.staging.mydomain.com, it sits under the domain Clerk already trusts. Cookies set on the parent domain carry over, and redirects stay inside allowed territory. Your login flow works in the preview, just like in production, with no extra Clerk config per PR.
So the win is twofold: Railway handles the DNS automatically, and Clerk treats each preview as a first-class citizen of your domain.
Keeping your domain elsewhere
If you keep your domain at an outside registrar, you can still automate this. You would script your DNS provider's API to write the verification record after the attach step. It works, but it is more moving parts. Buying the domain on Railway removes all of that.
For the full details on how Railway handles purchased domains and DNS, see the official Railway domains documentation.
If you run CI a lot, you might also enjoy using the Stripe CLI on a live account or validating Turnstile with Playwright.
Thank you for reading! If you have any questions or feedback, please feel free to contact us at hi@davette.ca.