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:
Procfile…cd api && npm run start:prodto start Nest in a subdirectory.ebextensions… JST timezone and 1 GB swap; write idempotently because commands run every deploy.platform/nginx…client_max_body_sizeand proxy timeouts to block 413 and timeouts.platform/hooks/prebuild…npm cion staging to prepare dependencies.ebignore… drop frontend andnode_modules; send onlyapi/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.