← All articles

Configuration Recipes for Running NestJS on AWS Elastic Beanstalk in a Monorepo

Copy-paste Elastic Beanstalk setup for a monorepo NestJS API—Procfile, ebextensions, nginx, prebuild hooks, ebignore, and environment layout with rationale for each file.

Published
Configuration Recipes for Running NestJS on AWS Elastic Beanstalk in a Monorepo cover image

AWS Elastic Beanstalk (EB) is a PaaS that provisions EC2, ALB, and autoscaling when you hand it application code. For a quick start it really is "drop it in"—but running a NestJS API in production surfaces several points where defaults hurt: timezone, memory, nginx body size, and more.

This article collects the configuration I settled on running NestJS on EB, as a recipe book you can copy. Each file explains what it fixes and why—hopefully a foundation for anyone standing up Node/NestJS on EB.

Overview

First, a map of where things live. My repo is a monorepo: NestJS API in api/, Ionic frontend in app/. EB should receive only the API, so the bundle root is the repo root and all EB config files sit there too.

.
├── Procfile
├── .ebignore
├── .ebextensions/
│   ├── command.config
│   └── swap.config
├── .platform/
│   ├── nginx/conf.d/upload_size.conf
│   └── hooks/prebuild/01_api_build.sh
├── .elasticbeanstalk/
│   └── config.yml
├── api/   ← NestJS
└── app/   ← Frontend (not sent to EB)

Deploy from my dev machine with eb deploy.

% eb deploy

Platform assumption: Node.js 24 running on 64bit Amazon Linux 2023/6.11.3. From here I walk each file with why it exists.

Procfile — declare how the process starts

On EB's Node platform, a Procfile at the repo root tells EB to run the web: process as the application. My API lives in api/, so I cd there before start.

web: cd api && npm run start:prod

start:prod is node dist/main. nginx in front reverse-proxies to this web process (the port NestJS listens on), so the app just listens on a port. In a monorepo with the API in a subdirectory, this one line is the first stumbling block—nail it first.

.ebextensions — instance initialization

.ebextensions/*.config runs commands and options during instance setup. I put two adjustments here that bite if left alone.

Set timezone to JST

AL2023 defaults to UTC. new Date() and logs are all UTC, which confuses a JST-oriented service in operation. I align to JST up front.

# .ebextensions/command.config
commands:
  set_time_zone:
    command: ln -f -s /usr/share/zoneinfo/Japan /etc/localtime

Swap file to avoid OOM

As described later, I run on small arm64 instances for cost. Deploy installs and traffic spikes can exhaust memory and kill processes. A 1 GB swap file reduces "sometimes it dies" a lot.

# .ebextensions/swap.config
commands:
  000_create_file:
    test: test ! -e /swapfile
    command: dd if=/dev/zero of=/swapfile bs=1M count=1024 && chmod 600 /swapfile
  001_mkswap:
    command: mkswap /swapfile
    ignoreErrors: true
  002_swapon:
    command: swapon /swapfile
    ignoreErrors: true
  003_fstab:
    test: test `grep -c swap /etc/fstab` -eq 0
    command: echo "/swapfile  swap   swap    defaults   0 0" >> /etc/fstab

Important: write idempotently. .ebextensions commands run on every deploy; naive scripts fail on the second run ("file exists", "swap line already in fstab"). Use test before create/append; ignoreErrors: true on mkswap/swapon so already-on swap does not stop deploy. Safe to redeploy any number of times.

.platform — inject nginx and deploy hooks

.platform on AL2+ platforms injects nginx config and deploy hooks. I override EB's auto-generated nginx where needed.

Raise upload size limit

Default nginx on EB returns 413 Request Entity Too Large for moderately large uploads. File-upload APIs hit this first. Raise the limit explicitly.

# .platform/nginx/conf.d/upload_size.conf
client_max_body_size 60M;

For slow endpoints (heavy aggregation, external API calls), extend proxy timeouts too. That avoids hard-to-debug cases where the app finished but nginx cut the connection first.

client_max_body_size 60M;
proxy_read_timeout 900;
proxy_connect_timeout 900;
proxy_send_timeout 900;
send_timeout 900;

Install dependencies in a prebuild hook

Scripts in .platform/hooks/prebuild/ run before app deploy (staging phase). I install API dependencies here.

#!/bin/bash
# .platform/hooks/prebuild/01_api_build.sh
set -xe
cd /var/app/staging/api

node -v
npm -v

sudo -u webapp npm ci
# sudo -u webapp env NODE_OPTIONS="--max-old-space-size=2048" npm run build

During deploy the app expands under /var/app/staging, so I enter api/ there and run npm ci as webapp. The last npm run build is commented intentionally: building Nest on a small instance often OOMs, so I ship prebuilt artifacts. To build on-instance, uncomment and raise heap with --max-old-space-size as shown.

Why prebuild instead of the start command

I could install in the start command: web: cd api && npm ci && npm run start:prod. I deliberately use prebuild instead.

EB treats Procfile web: as the long-lived main process, monitors it, and restarts on failure. Traffic flows only after that process is healthy (listening and responding). Mixing npm ci adds install time to "time until healthy."

That matters on instance replacement: rolling deploy, autoscale scale-out, spot reacquisition. EB waits for health before adding an instance to the load balancer. Install in the start command delays health checks and lengthens reduced-capacity windows during cutover.

Prebuild hooks run after source expansion and before web: starts—before health checks apply. They also run on every new instance from autoscale, not only deploy. Dependencies ready in prebuild means web: is "start the already-installed app" and responds quickly. EB restarts web: on failure; keeping install out of start avoids npm ci on every restart.

Think of it as pushing heavy one-time install work from the start command (counted toward health) into prebuild (not counted)—a division of labor.

.ebignore — exclude junk from the bundle

eb deploy zips the whole repo, but .ebignore excludes by its rules (separate from .gitignore). I drop frontend app/ and local scripts the API does not need so only api/ and EB config upload—lighter bundle, faster deploy.

app/
scripts/
env/
node_modules/
.git/
.idea/

# Elastic Beanstalk Files
.elasticbeanstalk/*.cfg.yml
.elasticbeanstalk/*.global.yml
.elasticbeanstalk/app_versions

I exclude node_modules/ because prebuild runs npm ci on the instance. Shipping local node_modules risks native module mismatches (local vs arm64 instance).

.elasticbeanstalk/config.yml — tie CLI to environment

The eb CLI remembers which application and environment to operate via .elasticbeanstalk/config.yml. Generated by eb init; branch-defaults maps branches to environments.

branch-defaults:
  main:
    environment: ●●●●-api-v3   # Replace with your environment name
global:
  application_name: ●●●●-api    # Replace with your application name
  default_ec2_keyname: ●●●●
  default_region: ap-northeast-1
  include_git_submodules: true
  sc: git
  workspace_type: Application

With sc: git, eb deploy bundles the committed tree—avoiding accidental deploy of uncommitted work-in-progress to production. Recommended for team operation.

Environment layout itself (eb config snapshot)

Instance type, autoscale, ALB—"environment-side" settings can live in .ebextensions, but I save snapshots with eb config save for high reproducibility when recreating environments via eb config. Reference points from my production setup:

Item Setting
Platform Node.js 24 / Amazon Linux 2023 (64bit Amazon Linux 2023/6.11.3)
Load balancer Application Load Balancer, TLS termination on HTTPS (443) (ACM cert + TLS 1.3 policy)
Instances t4g.small / t4g.medium (arm64 Graviton). Spot mixed (minimum one on-demand, portion of excess on spot for cost)
Autoscale Min 3 / Max 5, scale on CPUUtilization
Deploy policy Rolling, BatchSize: 1 (one instance at a time for near-zero downtime)
Managed updates Enabled (auto minor at night, instance refresh)
Other Enhanced health reporting, SNS notifications, DisableIMDSv1: true

In short: three to five small arm64 instances with spot, TLS at ALB, rolling replacement. The opening swap config is insurance to run those cheap instances stably—it works together with this layout.

Summary

Configuration for running NestJS on Elastic Beanstalk in production, by role:

  • Procfilecd api && npm run start:prod to start Nest in a subdirectory
  • .ebextensions … JST timezone and 1 GB swap; write idempotently because commands run every deploy
  • .platform/nginxclient_max_body_size and proxy timeouts to block 413 and timeouts
  • .platform/hooks/prebuildnpm ci on staging to prepare dependencies
  • .ebignore … drop frontend and node_modules; send only api/ lightly
  • .elasticbeanstalk / eb config … CLI-to-environment binding, instance/ALB/autoscale layout

EB's first step is easy; production-ready shape needs these adjustments. Use this recipe as a base to preempt timezone, memory, and upload size traps you would hit later. I hope it helps others building the same layout.

See you next time.