HSTS on Railway sounds simple: add a header, force HTTPS, done. In practice, the right place to set it depends on how you deploy, whether you use Railway’s edge, and how much control you actually have over redirects and custom domains.
If you run production apps on Railway, HSTS is usually worth enabling. But it’s one of those headers that can absolutely hurt you if you switch it on carelessly, especially with preload or a long max-age before your subdomains are ready.
Here’s the practical comparison guide I wish more hosting docs included.
What HSTS does on Railway
HSTS stands for HTTP Strict Transport Security. It tells browsers:
- only use HTTPS for this site
- never downgrade to HTTP for a period of time
- optionally apply the rule to subdomains
- optionally request inclusion in browser preload lists
A typical header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains
On Railway, this matters because many apps sit behind Railway’s proxy or edge networking. Your app may receive traffic over an internal connection while the public-facing request is HTTPS. That can create confusion if you’re thinking only at the app layer.
HSTS is enforced by the browser, not Railway. Railway just needs to make sure your site is reachable over valid HTTPS so the browser can learn and keep that policy.
The main ways to do HSTS on Railway
There are really three common patterns:
- Set HSTS in your app
- Set HSTS in a reverse proxy like Nginx or Caddy
- Rely on an external CDN or proxy in front of Railway
Each one has tradeoffs.
Option 1: Set HSTS in your app
This is the most common setup for Railway deployments. If you’re running Express, Fastify, Next.js custom server, Django, Rails, or Go, you can usually add the header directly in app code.
Pros
- Easy to ship with your app
- Version-controlled with your code
- Works without adding extra infrastructure
- Good fit if Railway is your only edge layer
Cons
- Easy to misconfigure when TLS terminates before the app
- Framework redirect logic can break if
X-Forwarded-Protoisn’t trusted - Harder to standardize across multiple services
Example: Express with Helmet
import express from "express";
import helmet from "helmet";
const app = express();
// Trust Railway's proxy so req.secure works correctly
app.set("trust proxy", 1);
app.use(
helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: false,
})
);
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(301, `https://${req.headers.host}${req.originalUrl}`);
}
next();
});
app.get("/", (req, res) => {
res.send("Hello from Railway");
});
app.listen(process.env.PORT || 3000);
That trust proxy line is not optional in most proxy-based deployments. Without it, Express may think the request is plain HTTP and keep redirecting in circles or fail to detect HTTPS correctly.
If you want to sanity-check the final response headers after deploy, run a free security headers scan at headertest.com. I use tools like that mostly to catch dumb mistakes like a missing header on redirects or staging accidentally inheriting production policy.
Best use case
Use app-level HSTS if you have a single app on Railway and want the simplest deploy path.
Option 2: Set HSTS in Nginx or Caddy inside Railway
Some teams run a containerized reverse proxy in front of the app service. That gives cleaner control over redirects, headers, caching, and static assets.
Pros
- Centralized security header policy
- Consistent behavior across app frameworks
- Better if multiple apps sit behind one proxy
- Easier to reason about redirect behavior
Cons
- More moving parts
- More maintenance inside Railway
- Can be redundant if another proxy already sits in front
- Easy to accidentally override or duplicate headers
Example: Nginx
server {
listen 8080;
server_name _;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
The always flag matters. Without it, some responses won’t include the header, especially error responses. That leaves weird gaps.
Best use case
Use a reverse proxy if you already need one for operational reasons. I wouldn’t add Nginx just for HSTS alone unless your app framework makes headers painful.
Option 3: Set HSTS at a CDN or external proxy in front of Railway
A lot of production Railway apps sit behind Cloudflare, Fastly, or another edge provider. In that setup, the edge is often the best place for HSTS.
Pros
- Header is guaranteed at the public edge
- Easier to manage across many apps
- Can pair with edge redirects and TLS enforcement
- Good for multi-service architectures
Cons
- Splits config away from app code
- Debugging gets annoying fast
- Risk of conflicting behavior between CDN and app
- Teams forget the app still needs to behave correctly over forwarded HTTPS
Best use case
Use edge-level HSTS if the CDN is your true front door and you already manage redirects and certificates there.
Railway-specific gotchas
Railway itself doesn’t make HSTS dangerous. The danger comes from how easy it is to enable the header before your domain setup is fully mature.
1. Custom domains need to be solid first
Don’t enable long-lived HSTS until:
- your custom domain has valid HTTPS
- redirects work correctly
- apex and
wwwbehavior is settled - any important subdomains are also HTTPS-ready if you plan to use
includeSubDomains
If blog.example.com or api.example.com still has broken TLS, includeSubDomains will come back to bite you.
2. Preload is a one-way door for a while
Preload means asking browser vendors to hardcode your domain as HTTPS-only. Great for hardened production. Terrible for domains that still change shape.
To qualify, you generally need:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
My advice: don’t preload a Railway-hosted domain until you’ve been stable for a while and you know every subdomain is covered.
3. Preview environments should not inherit production HSTS blindly
Railway makes spinning up preview or ephemeral environments easy. That’s great for shipping, but bad if you slap the same HSTS policy on every hostname.
A long max-age on temporary or test domains is pointless at best and painful at worst.
I usually do something like this:
const isProduction = process.env.NODE_ENV === "production";
app.use((req, res, next) => {
if (isProduction) {
res.setHeader(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains"
);
}
next();
});
You can also use a shorter max-age first, like:
Strict-Transport-Security: max-age=300
Then ramp up:
- 5 minutes
- 1 day
- 1 month
- 1 year
That phased rollout is boring, which is exactly why it works.
Pros of enabling HSTS on Railway
Stronger HTTPS enforcement
This is the whole point. Once a browser sees the header, it stops trying plain HTTP for future requests.
Protection against downgrade attacks
Without HSTS, a first-hop downgrade is still possible in some situations. HSTS cuts off a whole class of sloppy transport behavior.
Cleaner security posture for production apps
If you care enough to use HTTPS, you should usually care enough to tell browsers to stick with it.
Good fit for modern Railway apps
Most Railway production apps already assume HTTPS at the edge. HSTS aligns with that model.
Cons of enabling HSTS on Railway
You can lock users into a broken setup
If you deploy a bad certificate or misroute a domain after a long max-age, users may be stuck until the policy expires.
Subdomains become your problem
includeSubDomains is great when you fully control the namespace. It’s risky when old forgotten subdomains still exist.
Debugging local or staging behavior gets weird
Browsers cache HSTS aggressively. You can “fix” your config and still see failures because your browser remembers the old rule.
Preload is easy to regret
I’m repeating this because people still rush into it. Preload is not a badge. It’s a commitment.
My recommendation
For most Railway deployments, I’d do this:
- enable HTTPS on the real custom domain first
- add HSTS in the app or at your edge proxy
- start with a short
max-age - avoid
includeSubDomainsuntil you’ve audited subdomains - avoid
preloadunless your domain is mature and boring
A safe production starting point looks like:
Strict-Transport-Security: max-age=86400
Then later:
Strict-Transport-Security: max-age=31536000; includeSubDomains
And only much later, if you truly need it:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
The short comparison
App-level HSTS
Best for: simple Railway apps
Pros: easy, code-based, portable
Cons: proxy trust issues, framework-specific quirks
Reverse-proxy HSTS
Best for: teams already using Nginx/Caddy
Pros: centralized, consistent
Cons: extra complexity, another layer to maintain
CDN/edge HSTS
Best for: production setups with Cloudflare or similar
Pros: enforced at the real edge, scalable
Cons: config sprawl, harder debugging
If you’re on Railway and want the least painful path, set HSTS where you already control HTTPS behavior. For a single app, that’s usually the app itself. For a platform-style setup, it’s usually the edge.
Just don’t treat HSTS like a checkbox. It’s one of the few headers that can keep hurting after you’ve changed your mind.