[{"data":1,"prerenderedAt":832},["ShallowReactive",2],{"blog-how-to/deploy-railway-securely":3},{"id":4,"title":5,"body":6,"category":805,"date":806,"dateModified":806,"description":807,"draft":808,"extension":809,"faq":810,"featured":808,"headerVariant":817,"image":818,"keywords":819,"meta":820,"navigation":275,"ogDescription":821,"ogTitle":818,"path":822,"readTime":823,"schemaOrg":824,"schemaType":825,"seo":826,"sitemap":827,"stem":828,"tags":829,"twitterCard":830,"__hash__":831},"blog/blog/how-to/deploy-railway-securely.md","How to Deploy Railway Securely (2026)",{"type":7,"value":8,"toc":796},"minimark",[9,18,21,35,40,114,118,215,219,491,495,537,541,656,660,687,696,700,706,709,715,718,730,768,787,792],[10,11,12,13,17],"p",{},"Railway is popular with AI-generated apps because it handles most of the deployment complexity. The problem: that convenience creates blind spots. Our scans see Railway apps regularly where the OpenAI key is in a ",[14,15,16],"code",{},"VITE_OPENAI_KEY"," variable (compiled into the browser bundle) and the database URL is only accessible via the public internet.",[10,19,20],{},"Six steps fix the most common issues before you go live.",[22,23,24],"tldr",{},[10,25,26,27,30,31,34],{},"Railway secures the infrastructure; you secure the application. The two highest-risk gaps are secrets with ",[14,28,29],{},"VITE_"," or ",[14,32,33],{},"NEXT_PUBLIC_"," prefixes (they end up in your client bundle) and databases not using Railway's private network. Fix those two and you've eliminated the most common findings.",[36,37,39],"h2",{"id":38},"step-1-audit-your-env-var-prefixes","Step 1: Audit Your Env Var Prefixes",[41,42,44,47,99,111],"step",{"number":43},"1",[10,45,46],{},"Before you push anything, search your codebase for variables that should stay secret but are named with a client-visible prefix:",[48,49,54],"pre",{"className":50,"code":51,"language":52,"meta":53,"style":53},"language-bash shiki shiki-themes github-light github-dark","# Find VITE_ and NEXT_PUBLIC_ vars in your source\ngrep -r \"VITE_\\|NEXT_PUBLIC_\" src/ --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\"\n","bash","",[14,55,56,65],{"__ignoreMap":53},[57,58,61],"span",{"class":59,"line":60},"line",1,[57,62,64],{"class":63},"sJ8bj","# Find VITE_ and NEXT_PUBLIC_ vars in your source\n",[57,66,68,72,76,80,83,86,89,91,94,96],{"class":59,"line":67},2,[57,69,71],{"class":70},"sScJk","grep",[57,73,75],{"class":74},"sj4cs"," -r",[57,77,79],{"class":78},"sZZnC"," \"VITE_\\|NEXT_PUBLIC_\"",[57,81,82],{"class":78}," src/",[57,84,85],{"class":74}," --include=",[57,87,88],{"class":78},"\"*.ts\"",[57,90,85],{"class":74},[57,92,93],{"class":78},"\"*.tsx\"",[57,95,85],{"class":74},[57,97,98],{"class":78},"\"*.js\"\n",[10,100,101,102,30,104,106,107,110],{},"Any ",[14,103,29],{},[14,105,33],{}," variable gets compiled into your JavaScript bundle at build time. It's not a runtime secret. Any visitor can find it in browser DevTools under Sources or by running ",[14,108,109],{},"strings dist/assets/index.js | grep -E \"sk-|Bearer\"",".",[10,112,113],{},"The rule: if it's a real secret (API key, database password, webhook signing key), rename it to drop the prefix and only read it in server-side code.",[36,115,117],{"id":116},"step-2-move-secrets-to-railway-variables","Step 2: Move Secrets to Railway Variables",[41,119,121,129,132,156,204],{"number":120},"2",[10,122,123,124,128],{},"Open your service in Railway's dashboard and go to ",[125,126,127],"strong",{},"Variables",". Add each secret there with a plain name (no framework prefix).",[10,130,131],{},"Railway provides two useful environment variables automatically:",[133,134,135,150],"ul",{},[136,137,138,141,142,145,146,149],"li",{},[14,139,140],{},"RAILWAY_ENVIRONMENT",": set to ",[14,143,144],{},"production",", ",[14,147,148],{},"staging",", etc. depending on your environment. Use this for conditional logic instead of hard-coding environment checks.",[136,151,152,155],{},[14,153,154],{},"RAILWAY_SERVICE_NAME",": the name of your service, useful for logging.",[48,157,159],{"className":50,"code":158,"language":52,"meta":53,"style":53},"# What you want in Railway Variables tab\nOPENAI_API_KEY=sk-...\nSTRIPE_SECRET_KEY=sk_live_...\nDATABASE_URL=postgresql://...  # from the Connect tab\n",[14,160,161,166,179,190],{"__ignoreMap":53},[57,162,163],{"class":59,"line":60},[57,164,165],{"class":63},"# What you want in Railway Variables tab\n",[57,167,168,172,176],{"class":59,"line":67},[57,169,171],{"class":170},"sVt8B","OPENAI_API_KEY",[57,173,175],{"class":174},"szBVR","=",[57,177,178],{"class":78},"sk-...\n",[57,180,182,185,187],{"class":59,"line":181},3,[57,183,184],{"class":170},"STRIPE_SECRET_KEY",[57,186,175],{"class":174},[57,188,189],{"class":78},"sk_live_...\n",[57,191,193,196,198,201],{"class":59,"line":192},4,[57,194,195],{"class":170},"DATABASE_URL",[57,197,175],{"class":174},[57,199,200],{"class":78},"postgresql://...",[57,202,203],{"class":63},"  # from the Connect tab\n",[10,205,206,207,210,211,214],{},"Never commit a ",[14,208,209],{},".env"," file with real credentials. Use ",[14,212,213],{},".env.example"," with placeholder values for documentation.",[36,216,218],{"id":217},"step-3-move-api-calls-server-side","Step 3: Move API Calls Server-Side",[41,220,222,225,228,481],{"number":221},"3",[10,223,224],{},"If your frontend is calling OpenAI, Stripe, or any service that requires a secret key, create a server-side proxy route. The client calls your proxy, your proxy calls the external service.",[10,226,227],{},"Here's a minimal Express proxy for OpenAI:",[48,229,233],{"className":230,"code":231,"language":232,"meta":53,"style":53},"language-typescript shiki shiki-themes github-light github-dark","// src/server/routes/ai.ts\nimport express from \"express\";\nimport OpenAI from \"openai\";\n\nconst router = express.Router();\nconst openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\nrouter.post(\"/chat\", async (req, res) => {\n  const { message } = req.body;\n  const completion = await openai.chat.completions.create({\n    model: \"gpt-4o-mini\",\n    messages: [{ role: \"user\", content: message }],\n  });\n  res.json({ reply: completion.choices[0].message.content });\n});\n\nexport default router;\n","typescript",[14,234,235,240,257,271,277,298,322,327,368,388,410,422,434,440,458,464,469],{"__ignoreMap":53},[57,236,237],{"class":59,"line":60},[57,238,239],{"class":63},"// src/server/routes/ai.ts\n",[57,241,242,245,248,251,254],{"class":59,"line":67},[57,243,244],{"class":174},"import",[57,246,247],{"class":170}," express ",[57,249,250],{"class":174},"from",[57,252,253],{"class":78}," \"express\"",[57,255,256],{"class":170},";\n",[57,258,259,261,264,266,269],{"class":59,"line":181},[57,260,244],{"class":174},[57,262,263],{"class":170}," OpenAI ",[57,265,250],{"class":174},[57,267,268],{"class":78}," \"openai\"",[57,270,256],{"class":170},[57,272,273],{"class":59,"line":192},[57,274,276],{"emptyLinePlaceholder":275},true,"\n",[57,278,280,283,286,289,292,295],{"class":59,"line":279},5,[57,281,282],{"class":174},"const",[57,284,285],{"class":74}," router",[57,287,288],{"class":174}," =",[57,290,291],{"class":170}," express.",[57,293,294],{"class":70},"Router",[57,296,297],{"class":170},"();\n",[57,299,301,303,306,308,311,314,317,319],{"class":59,"line":300},6,[57,302,282],{"class":174},[57,304,305],{"class":74}," openai",[57,307,288],{"class":174},[57,309,310],{"class":174}," new",[57,312,313],{"class":70}," OpenAI",[57,315,316],{"class":170},"({ apiKey: process.env.",[57,318,171],{"class":74},[57,320,321],{"class":170}," });\n",[57,323,325],{"class":59,"line":324},7,[57,326,276],{"emptyLinePlaceholder":275},[57,328,330,333,336,339,342,344,347,350,354,356,359,362,365],{"class":59,"line":329},8,[57,331,332],{"class":170},"router.",[57,334,335],{"class":70},"post",[57,337,338],{"class":170},"(",[57,340,341],{"class":78},"\"/chat\"",[57,343,145],{"class":170},[57,345,346],{"class":174},"async",[57,348,349],{"class":170}," (",[57,351,353],{"class":352},"s4XuR","req",[57,355,145],{"class":170},[57,357,358],{"class":352},"res",[57,360,361],{"class":170},") ",[57,363,364],{"class":174},"=>",[57,366,367],{"class":170}," {\n",[57,369,371,374,377,380,383,385],{"class":59,"line":370},9,[57,372,373],{"class":174},"  const",[57,375,376],{"class":170}," { ",[57,378,379],{"class":74},"message",[57,381,382],{"class":170}," } ",[57,384,175],{"class":174},[57,386,387],{"class":170}," req.body;\n",[57,389,391,393,396,398,401,404,407],{"class":59,"line":390},10,[57,392,373],{"class":174},[57,394,395],{"class":74}," completion",[57,397,288],{"class":174},[57,399,400],{"class":174}," await",[57,402,403],{"class":170}," openai.chat.completions.",[57,405,406],{"class":70},"create",[57,408,409],{"class":170},"({\n",[57,411,413,416,419],{"class":59,"line":412},11,[57,414,415],{"class":170},"    model: ",[57,417,418],{"class":78},"\"gpt-4o-mini\"",[57,420,421],{"class":170},",\n",[57,423,425,428,431],{"class":59,"line":424},12,[57,426,427],{"class":170},"    messages: [{ role: ",[57,429,430],{"class":78},"\"user\"",[57,432,433],{"class":170},", content: message }],\n",[57,435,437],{"class":59,"line":436},13,[57,438,439],{"class":170},"  });\n",[57,441,443,446,449,452,455],{"class":59,"line":442},14,[57,444,445],{"class":170},"  res.",[57,447,448],{"class":70},"json",[57,450,451],{"class":170},"({ reply: completion.choices[",[57,453,454],{"class":74},"0",[57,456,457],{"class":170},"].message.content });\n",[57,459,461],{"class":59,"line":460},15,[57,462,463],{"class":170},"});\n",[57,465,467],{"class":59,"line":466},16,[57,468,276],{"emptyLinePlaceholder":275},[57,470,472,475,478],{"class":59,"line":471},17,[57,473,474],{"class":174},"export",[57,476,477],{"class":174}," default",[57,479,480],{"class":170}," router;\n",[10,482,483,484,487,488,490],{},"Your frontend calls ",[14,485,486],{},"/api/chat"," with the message. The ",[14,489,171],{}," stays on the server, never reaches the browser.",[36,492,494],{"id":493},"step-4-use-private-networking-for-your-database","Step 4: Use Private Networking for Your Database",[41,496,498,505,516,524,530],{"number":497},"4",[10,499,500,501,504],{},"Railway gives every service a private hostname that ends in ",[14,502,503],{},".railway.internal",". Database traffic over this address never hits the public internet and doesn't count against your egress.",[10,506,507,508,511,512,515],{},"In your database service, open the ",[125,509,510],{},"Connect"," tab and copy the ",[125,513,514],{},"Private URL",". It looks like:",[48,517,522],{"className":518,"code":520,"language":521},[519],"language-text","postgresql://postgres:password@postgres.railway.internal:5432/railway\n","text",[14,523,520],{"__ignoreMap":53},[10,525,526,527,529],{},"Set that as ",[14,528,195],{}," in your backend service's variables. Your backend and database must be in the same Railway project for the private network to route correctly.",[10,531,532,533,536],{},"The public URL (",[14,534,535],{},"*.rlwy.net",") still works, but routing database connections through it exposes your database to the internet. Use private networking in production.",[36,538,540],{"id":539},"step-5-add-security-headers","Step 5: Add Security Headers",[41,542,544,551,567,646,653],{"number":543},"5",[10,545,546,547,550],{},"Railway doesn't inject security headers automatically. Add them in your application with the ",[14,548,549],{},"helmet"," middleware for Node.js apps:",[48,552,554],{"className":50,"code":553,"language":52,"meta":53,"style":53},"npm install helmet\n",[14,555,556],{"__ignoreMap":53},[57,557,558,561,564],{"class":59,"line":60},[57,559,560],{"class":70},"npm",[57,562,563],{"class":78}," install",[57,565,566],{"class":78}," helmet\n",[48,568,570],{"className":230,"code":569,"language":232,"meta":53,"style":53},"// src/server/index.ts\nimport express from \"express\";\nimport helmet from \"helmet\";\n\nconst app = express();\napp.use(helmet());\n// Helmet sets: X-Frame-Options, X-Content-Type-Options,\n// Strict-Transport-Security, Content-Security-Policy, and others\n",[14,571,572,577,589,603,607,621,636,641],{"__ignoreMap":53},[57,573,574],{"class":59,"line":60},[57,575,576],{"class":63},"// src/server/index.ts\n",[57,578,579,581,583,585,587],{"class":59,"line":67},[57,580,244],{"class":174},[57,582,247],{"class":170},[57,584,250],{"class":174},[57,586,253],{"class":78},[57,588,256],{"class":170},[57,590,591,593,596,598,601],{"class":59,"line":181},[57,592,244],{"class":174},[57,594,595],{"class":170}," helmet ",[57,597,250],{"class":174},[57,599,600],{"class":78}," \"helmet\"",[57,602,256],{"class":170},[57,604,605],{"class":59,"line":192},[57,606,276],{"emptyLinePlaceholder":275},[57,608,609,611,614,616,619],{"class":59,"line":279},[57,610,282],{"class":174},[57,612,613],{"class":74}," app",[57,615,288],{"class":174},[57,617,618],{"class":70}," express",[57,620,297],{"class":170},[57,622,623,626,629,631,633],{"class":59,"line":300},[57,624,625],{"class":170},"app.",[57,627,628],{"class":70},"use",[57,630,338],{"class":170},[57,632,549],{"class":70},[57,634,635],{"class":170},"());\n",[57,637,638],{"class":59,"line":324},[57,639,640],{"class":63},"// Helmet sets: X-Frame-Options, X-Content-Type-Options,\n",[57,642,643],{"class":59,"line":329},[57,644,645],{"class":63},"// Strict-Transport-Security, Content-Security-Policy, and others\n",[10,647,648,649,652],{},"If you're using a Python backend (FastAPI or Flask), the equivalent is the ",[14,650,651],{},"secure"," package or adding headers manually in middleware.",[10,654,655],{},"For a React/Vue/Svelte frontend deployed on Railway (not Netlify), you can also add headers via a custom server file or a reverse proxy config.",[36,657,659],{"id":658},"step-6-scan-before-going-live","Step 6: Scan Before Going Live",[41,661,663,666,684],{"number":662},"6",[10,664,665],{},"Before you point a real domain at your Railway app, run a CheckYourVibe scan on the Railway URL. It checks for:",[133,667,668,671,674,681],{},[136,669,670],{},"API keys present in your JS bundle",[136,672,673],{},"Missing security headers",[136,675,676,677,680],{},"CORS configured too broadly (",[14,678,679],{},"Access-Control-Allow-Origin: *"," on an API that handles auth)",[136,682,683],{},"Exposed database connection info",[10,685,686],{},"Run the scan on your staging environment while it's still safe to fix things. Findings on a live app are harder to rotate because real users may already have the leaked credentials cached.",[688,689,690],"warning-box",{},[10,691,692,695],{},[125,693,694],{},"One common trap:"," Railway services default to \"Public\" exposure. If you have an internal API that only your frontend service should reach, set it to Private in the service settings. Public means anyone on the internet can send requests to it.",[36,697,699],{"id":698},"cross-service-communication-inside-railway","Cross-Service Communication Inside Railway",[10,701,702,703,110],{},"When your frontend and backend are separate Railway services in the same project, use Railway's private network for service-to-service calls. Your backend gets an internal hostname: ",[14,704,705],{},"backendservice.railway.internal",[10,707,708],{},"Set it as an environment variable with a Railway reference in your frontend service:",[48,710,713],{"className":711,"code":712,"language":521},[519],"API_URL=${{backend.RAILWAY_PRIVATE_DOMAIN}}\n",[14,714,712],{"__ignoreMap":53},[10,716,717],{},"This way the connection never leaves Railway's internal network.",[719,720,721],"tip-box",{},[10,722,723,726,727,729],{},[125,724,725],{},"Confirming your setup:"," After deploying, open your Railway service logs and look for the startup message. A well-configured Node.js app should log which environment it's running in (",[14,728,140],{},") and confirm it connected to the database without errors. If the database connection fails silently, it usually means you're using the public URL but the database has IP restrictions, or you pasted the private URL but forgot to set it in the right service.",[731,732,733,740,750,756,762],"faq-section",{},[734,735,737],"faq-item",{"question":736},"Is Railway secure by default?",[10,738,739],{},"Railway handles infrastructure security: automatic SSL, isolated containers, encrypted-at-rest variables. Application security is your job. The most common issues we see in Railway apps are secrets baked into client bundles via VITE_ or NEXT_PUBLIC_ prefixes, and database URLs accessible over the public internet instead of through Railway's private network.",[734,741,743],{"question":742},"How do I use Railway private networking for my database?",[10,744,745,746,749],{},"In your Railway project, open your database service and copy the Private URL from the Connect tab. It looks like ",[14,747,748],{},"postgresql://user:pass@postgres.railway.internal:5432/railway",". Use that URL in your backend's DATABASE_URL variable instead of the public URL. Your backend service and database must be in the same Railway project for private networking to work.",[734,751,753],{"question":752},"Can Railway environment variables be leaked?",[10,754,755],{},"Railway stores variables encrypted and never exposes them in logs or the UI after you set them. The leak risk is in your code: if you use VITE_ or NEXT_PUBLIC_ variable names, those values get compiled into your JavaScript bundle at build time and shipped to every browser. Use plain variable names (no framework-specific prefix) and only read them in server-side code.",[734,757,759],{"question":758},"What security headers should I set on Railway?",[10,760,761],{},"At minimum: X-Frame-Options (prevents clickjacking), X-Content-Type-Options (prevents MIME sniffing), and Content-Security-Policy (controls which resources load). In a Node.js app, installing the helmet package adds all of these in one middleware call. Railway doesn't inject headers automatically, so you need to set them in your application code.",[734,763,765],{"question":764},"Do I need HTTPS on Railway?",[10,766,767],{},"Railway provides HTTPS automatically for your railway.app subdomain and for custom domains you configure. You don't need to set up SSL yourself. What you do need to ensure is that your app redirects HTTP to HTTPS and that you're not making outbound calls to http:// endpoints from your server-side code.",[769,770,771,777,782],"related-articles",{},[772,773],"related-card",{"description":774,"href":775,"title":776},"Step-by-step fix for Railway apps with leaked API keys: find the leak, rotate, scrub git history, and move secrets server-side.","/blog/how-to/fix-railway-api-key-exposure","Fix Railway API Key Exposure",[772,778],{"description":779,"href":780,"title":781},"How to add, manage, and secure env vars in Railway: shared variables, references, secrets, and the dashboard walkthrough.","/blog/how-to/railway-env-vars","Railway Environment Variables Guide",[772,783],{"description":784,"href":785,"title":786},"15-item checklist covering environment variables, database access, service exposure, and everything else to verify before deploying to Railway.","/blog/checklists/railway-security-checklist","Railway Security Checklist",[788,789],"cta-box",{"href":790,"label":791},"/","Start Free Scan",[793,794,795],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":53,"searchDepth":67,"depth":67,"links":797},[798,799,800,801,802,803,804],{"id":38,"depth":67,"text":39},{"id":116,"depth":67,"text":117},{"id":217,"depth":67,"text":218},{"id":493,"depth":67,"text":494},{"id":539,"depth":67,"text":540},{"id":658,"depth":67,"text":659},{"id":698,"depth":67,"text":699},"how-to","2026-06-13","Six steps to deploy your Railway app without exposing API keys or databases. Covers env var auditing, private networking, server-side secrets, and security headers.",false,"md",[811,812,814,815,816],{"question":736,"answer":739},{"question":742,"answer":813},"In your Railway project, open your database service and copy the Private URL from the Connect tab. It looks like postgresql://user:pass@postgres.railway.internal:5432/railway. Use that URL in your backend's DATABASE_URL variable instead of the public URL. Your backend service and database must be in the same Railway project for private networking to work.",{"question":752,"answer":755},{"question":758,"answer":761},{"question":764,"answer":767},"yellow",null,"railway deploy securely, railway security, railway environment variables security, railway private networking, railway secrets, railway production checklist",{},"Deploy Railway securely in 6 steps. Audit env vars, enable private networking, proxy API calls server-side, and scan before launch.","/blog/how-to/deploy-railway-securely","7 min read","[object Object]","HowTo",{"title":5,"description":807},{"loc":822},"blog/how-to/deploy-railway-securely",[],"summary_large_image","_1DV-0sjLUHBPyMJRWKS6YPrDUlCVNTNsVYMI2qwDP0",1784736388769]