How Do I Fix Next.js – ERROR Build Directory is not Writeable on EC2

When I first deployed my Next.js app with a custom server.js to AWS Elastic Beanstalk (EB) running 64bit Amazon Linux 2, I hit a pretty frustrating wall. Everything seemed fine after creating the application and environment with the eb-cli, but the EB dashboard quickly turned from Degraded to Severe.

When I connected to the instance with eb ssh and ran npm run build, I was greeted with this error:

> Build error occurred
Error: > Build directory is not writeable. https://err.sh/zeit/next.js/build-dir-not-writeable

It took me a while, but I eventually figured out what went wrong, and in this post, I’ll walk you through the problem, my code setup, the fix, and some extra production-ready improvements I added along the way.

My Project Setup

Here’s the starting point: a custom Next.js server using Express, and a package.json with some useful scripts.

server.js

const express = require("express");
const next = require("next");

const port = process.env.PORT || 3000;
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

async function start() {
  await app.prepare();
  const server = express();

  // Health check for EB/ALB
  server.get("/health", (_req, res) => res.status(200).send("OK"));

  // Simple version endpoint
  const version = process.env.APP_VERSION || "0.1.0";
  server.get("/version", (_req, res) => res.json({ version }));

  // Let Next handle everything else
  server.all("*", (req, res) => handle(req, res));

  const httpServer = server.listen(port, () => {
    console.log(`> Server listening on http://localhost:${port}`);
  });

  // Graceful shutdown
  const shutdown = () => {
    console.log("Shutting down...");
    httpServer.close(() => process.exit(0));
    setTimeout(() => process.exit(1), 10000);
  };
  process.on("SIGTERM", shutdown);
  process.on("SIGINT", shutdown);
}

start().catch((err) => {
  console.error("Server failed to start:", err);
  process.exit(1);
});

package.json

{
  "name": "webreader-client",
  "version": "1.0.0",
  "scripts": {
    "dev": "nodemon --exec babel-node server.js",
    "build": "next build",
    "start": "node server.js",
    "verify:write": "node scripts/preflight.js",
    "deploy": "npm run verify:write && next build"
  },
  "dependencies": {
    "express": "^4.19.2",
    "next": "13.5.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.2"
  }
}

scripts/preflight.js

To prevent myself from wasting time on permissions issues, I also added this script:

const fs = require("fs");
const path = require("path");

const targets = [".", ".next", "node_modules"].map((p) => path.resolve(p));

for (const target of targets) {
  try {
    const testPath = fs.existsSync(target) ? target : path.dirname(target);
    fs.accessSync(testPath, fs.constants.W_OK);
    console.log(`[OK] Writable: ${testPath}`);
  } catch (e) {
    console.error(`[ERR] Not writable: ${target}`);
    process.exit(1);
  }
}

Now I can run npm run verify:write to confirm my directories are writable before deploying.

Why the Error Happened

Here’s what actually caused the problem:

  • Next.js build process → writes output to the .next/ directory.
  • Elastic Beanstalk deployment flow → copies app code to /var/app/staging, then symlinks it to /var/app/current.
  • EB runtime user → runs as webapp, not root.

The problem? I created the .next/ folder with sudo, which made it owned by root. When next build ran under the webapp user, it couldn’t write to the .next folder, so the build crashed.

The quick fix was:

sudo chown -R webapp:webapp /var/app/staging /var/app/current

But the better fix was: don’t create .next manually with sudo. Let Next.js handle it.

Making Elastic Beanstalk Deployment Production Friendly

To avoid permission headaches, I added a postdeploy hook so everything runs as the correct user during deployment.

.platform/hooks/postdeploy/01-build.sh

#!/bin/bash
set -xe

APP_DIR="/var/app/current"

# Ensure correct ownership
chown -R webapp:webapp "$APP_DIR"

# Install and build as webapp
sudo -u webapp bash -lc "cd '$APP_DIR' && npm ci && npm run deploy"

Then I made it executable:

chmod +x .platform/hooks/postdeploy/01-build.sh

This way, the build runs cleanly under webapp.

Managing Processes: Procfile vs PM2

I learned the hard way that running pm2 start ... inside npm run deploy is a bad idea — it conflicts with EB’s lifecycle. Instead, I used a Procfile:

Simple (no PM2):

web: node server.js

With PM2 Runtime (safer in production):

web: npx pm2-runtime ecosystem.config.js

ecosystem.config.js

module.exports = {
  apps: [
    {
      name: "webreader-client",
      script: "server.js",
      instances: "max",
      exec_mode: "cluster",
      env: { NODE_ENV: "production" }
    }
  ]
};

Extra Feature I Added

Along the way, I added some extra practical functionality:

  • /health endpoint → EB/ALB health checks pass smoothly.
  • /version endpoint → quick way to verify which build is running.
  • Graceful shutdown → so rolling updates don’t kill in-flight requests.
  • npm run verify:write → catches permission issues before deploy.
  • Postdeploy hook → ensures builds run as the right user.

Final Thought

When I first saw “Build directory is not writeable”, I thought something was broken with Next.js. In reality, it was just a simple permissions mismatch between root and the webapp user on Elastic Beanstalk. By fixing ownership, moving the build process into a postdeploy hook, and cleaning up how I managed processes with EB, I turned a nasty error into a solid, production-ready setup.

Related blog posts