evanw/esbuild · error

Output directory %q must be contained in serve directory %q

Error message

Output directory %q must be contained in serve directory %q

What it means

Returned by internalContext.Serve when the computed relative path from servedir to AbsOutputDir starts with '..' (i.e. the output directory is outside the serve directory). For the HTTP server to serve build output, the output dir must be inside (or equal to) the serve dir; otherwise the files would be unreachable via URLs. The check tests relPath == '..' or strings.HasPrefix(relPath, '../').

Source

Thrown at pkg/api/serve_other.go:796

		if ctx.args.options.WriteToStdout {
			what := "entry points"
			if len(ctx.args.entryPoints) == 1 {
				what = "an entry point"
			}
			return ServeResult{}, fmt.Errorf("Cannot serve %s without an output path", what)
		}

		// Compute the output path prefix
		if serveOptions.Servedir != "" && ctx.args.options.AbsOutputDir != "" {
			// Make sure the output directory is contained in the "servedir" directory
			relPath, ok := ctx.realFS.Rel(serveOptions.Servedir, ctx.args.options.AbsOutputDir)
			if !ok {
				return ServeResult{}, fmt.Errorf(
					"Cannot compute relative path from %q to %q\n", serveOptions.Servedir, ctx.args.options.AbsOutputDir)
			}
			relPath = strings.ReplaceAll(relPath, "\\", "/") // Fix paths on Windows
			if relPath == ".." || strings.HasPrefix(relPath, "../") {
				return ServeResult{}, fmt.Errorf(
					"Output directory %q must be contained in serve directory %q",
					prettyPrintPath(ctx.realFS, ctx.args.options.AbsOutputDir),
					prettyPrintPath(ctx.realFS, serveOptions.Servedir),
				)
			}
			if relPath != "." {
				outdirPathPrefix = relPath
			}
		}
	}

	// Determine the host
	var listener net.Listener
	network := "tcp4"
	host := "0.0.0.0"
	hostIsIP := true
	if serveOptions.Host != "" {
		host = serveOptions.Host

View on GitHub (pinned to 6ff1d8b0d8)

Solutions

  1. Move outdir to be inside servedir (e.g. servedir='./www', outdir='./www/assets').
  2. Set servedir to a parent that contains outdir.
  3. If outdir must be elsewhere, drop servedir and serve the outdir directly instead.
  4. Verify with a quick path check that outdir is a subdirectory of servedir before Serve().

Example fix

// before
ctx = await api.Context({ entryPoints: ['src/index.js'], outdir: './dist' });
ctx.Serve({ Servedir: './public' }); // dist is outside public -> error

// after
ctx = await api.Context({ entryPoints: ['src/index.js'], outdir: './public/dist' });
ctx.Serve({ Servedir: './public' });
Defensive patterns

Strategy: validation

Validate before calling

const path = require('path');
function isContained(child, parent) {
  const rc = path.resolve(child), rp = path.resolve(parent);
  return rc === rp || rc.startsWith(rp + path.sep);
}
if (!isContained(outdir, servedir)) throw new Error('outdir must be inside servedir');

Try / catch

try { await ctx.Serve({ Servedir }); } catch (e) { if (/must be contained in serve directory/.test(e.message)) { /* move outdir under servedir */ } throw e; }

Prevention

When it happens

Trigger: Serve() with a Servedir and an AbsOutputDir where outdir is a sibling of or above servedir. Rel succeeds but yields a path beginning with '..', so the guard at serve_other.go:795 fires.

Common situations: Setting outdir to '../dist' while servedir is './public'; outdir and servedir as siblings; a default outdir that happens to live outside the served static dir; misconfigured monorepo paths.

Related errors


AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03). Data as JSON: /data/errors/b7b593a50f58efad.json. Report an issue: GitHub.