Back to Blog

NestJS Static Assets 404 After Deploying to Render

Published: July 28, 2026
NestJS Static Assets 404 After Deploying to Render

NestJS static assets 404 after deploying to Render almost always traces back to one thing: a path that happened to work locally because of exactly where you were running the app from, not because the configuration was actually correct. The images, CSS, or static files load without a hitch on your machine, and the instant the same code runs on Render, every one of them comes back 404 — with no error suggesting your ServeStaticModule setup itself is wrong.

Short answer: check whether the path you're passing to ServeStaticModule.forRoot() is resolved relative to __dirname (which points at wherever the compiled file happens to sit, and shifts between environments) instead of something more stable, and confirm the static folder you're pointing at actually made it into Render's build output in the first place — nest build only compiles TypeScript, it doesn't copy asset folders for you.

Yellow block letters spelling 'error' on a vibrant pink background, representing the 404 that shows up only after deployment

Why NestJS Static Assets 404 on Render but Work Fine Locally

The default pattern most NestJS starters ship with looks like this:

TypeScript
1// app.module.ts — path resolution that only works from the exact directory you happened to build in
2import { Module } from '@nestjs/common';
3import { ServeStaticModule } from '@nestjs/serve-static';
4import { join } from 'path';
5
6@Module({
7  imports: [
8    ServeStaticModule.forRoot({
9      rootPath: join(__dirname, '..', 'public'),
10    }),
11  ],
12})
13export class AppModule {}

__dirname resolves to wherever the compiled file physically lives, and that location depends on your build output structure — how deep dist/ nests things, whether you're running via ts-node locally versus the compiled dist/main.js in production. Locally, this often lines up by coincidence. On Render, the build step and directory layout aren't guaranteed to match your local assumptions, and the moment the relative path is off by even one folder level, join(__dirname, '..', 'public') quietly resolves to somewhere that doesn't have your files — no error, just a 404 for everything.

(If you've been re-checking your ServeStaticModule import and decorator syntax for the tenth time — it's fine. The decorator was never the problem. The path underneath it was pointing at a folder that exists on your laptop and nowhere else.)

The Fix: Resolve From a Stable Location, and Verify the Build Actually Copies It

Two separate things need to be true, and people usually only fix one:

TypeScript
1// app.module.ts — resolve from the process's working directory instead of the compiled file's location
2import { Module } from '@nestjs/common';
3import { ServeStaticModule } from '@nestjs/serve-static';
4import { join } from 'path';
5
6@Module({
7  imports: [
8    ServeStaticModule.forRoot({
9      rootPath: join(process.cwd(), 'public'),
10    }),
11  ],
12})
13export class AppModule {}

process.cwd() reflects the directory the Node process was actually started from — typically wherever node dist/main.js gets invoked — which tends to be far more consistent across local dev, CI, and Render than __dirname's relationship to the compiled file's nesting depth.

A 'Wrong Way' traffic sign amidst green trees, representing a build path pointing somewhere your static assets never actually shipped to

That alone doesn't help if the public folder never made it into the build output to begin with. nest build compiles TypeScript — it has no idea your images and static HTML exist unless you tell it to carry them along:

JSON
1// package.json — make sure the build step actually copies non-TS assets into dist/
2{
3  "scripts": {
4    "build": "nest build && cp -r public dist/public",
5    "start:prod": "node dist/main.js"
6  }
7}

Check both. A correctly resolved path pointing at a folder that was never copied 404s exactly the same way as a wrong path pointing at a folder that does exist.

A tidy office desk with organized binders and papers, representing a build output structure worth double-checking before assuming a path is correct

Checking Render's Actual Build Output

Before redeploying and hoping, Render's own deployment docs are worth a direct read for exactly what your build command produces — but the faster check is opening Render's shell for the running service and looking at the directory yourself: does dist/public (or wherever you've configured rootPath to point) actually exist, and does it contain the files you expect? If it's empty or missing entirely, the fix is in your build script, not in ServeStaticModule's configuration.

The Opinion Part

Here's the pattern worth naming plainly, because it's the same lesson underneath most of the "works locally, breaks on deploy" bugs in this genre: any path built from __dirname, any environment variable assumed to be set, any file assumed to exist because it does on your laptop — these are local-environment assumptions wearing the disguise of application logic. A defect like this costs a comment in code review to fix; it costs an afternoon of "is Render's build broken?" theories to find after a real deploy, because the error message tells you nothing about which of your assumptions was wrong. Treat any disk-relative path in a deployable service the same way you'd treat a hardcoded environment variable — something to double-check explicitly for every environment it'll actually run in, not just the one you're staring at right now.

Conclusion

If NestJS static assets are 404ing on Render, it's not your ServeStaticModule decorator and it's not a Render outage — it's a path assumption that only ever held in one specific local directory structure. Resolve the root path from something stable like process.cwd(), confirm your build step actually copies the asset folder into dist, and check Render's own shell to verify the files are really there before you touch the config again.

If you've already fixed this one and you're now chasing why the deployed service reports itself as unhealthy for an unrelated reason, our Render health check guide covers that specific failure mode — and if you're setting up the Docker build for this app for the first time, our multi-stage build guide covers exactly this class of "did the build actually include what I think it included" question from the container side.

Copy the folder, fix the path, redeploy — and enjoy the small, specific satisfaction of a 404 that turns back into an image.

Frequently Asked Questions

Almost always because the path passed to ServeStaticModule.forRoot() was resolved relative to something that changes between environments — commonly __dirname, which points at wherever the compiled file happens to sit, or a public folder that was never actually copied into the build output. Locally, you're usually running from a consistent location that happens to make the path work by accident; Render's build and run steps don't guarantee the same directory shape.

Resolve it from process.cwd() rather than __dirname where possible, since the working directory a process is started from tends to be more predictable across environments than the compiled file's own location. More importantly, confirm the static folder you're pointing at actually exists in your build output — nest build only compiles TypeScript by default, it doesn't automatically copy non-TS asset folders.

Yes, directly. If your build command is just nest build (or tsc), any folder that isn't TypeScript — images, a public directory, static HTML — won't be copied into dist unless your build script explicitly copies it, or nest-cli.json is configured to treat it as an asset. Whatever rootPath you configure has to point at a folder that genuinely exists after the build step Render actually runs.

NestJS can serve them fine via ServeStaticModule for most SaaS use cases — you don't need a separate static site service just to fix a 404. A dedicated static host or CDN becomes worth it at higher traffic volumes for caching and edge delivery, but that's a scaling decision, not the fix for this specific bug.

The same root cause — a path assumption that only held in one specific directory structure — can affect anything else in your app that reads from disk relative to __dirname, not just ServeStaticModule. If you're also seeing unrelated file-read failures in production, check for the same __dirname-relative-path pattern elsewhere in the codebase.

Portrait of Umar Farooq

About Umar Farooq

Umar Farooq is the founder and lead engineer of Codify SaaS. He builds B2B SaaS products and web applications on modern TypeScript stacks and enterprise Java, and writes code-first guides drawn from real production work — the schema decisions, the migrations that almost went wrong, and the performance fixes that actually moved the numbers. When he recommends an approach, he shows the code and explains the trade-offs.

Read full bio