loading...

Deploying next.js on AWS ElasticBeanstalk

mweibel profile image Michael Weibel ・4 min read

AWS ElasticBeanstalk (EB) is a service to deploy applications in a simple manner.
AWS EB has quite a range of features. It allows you to configure rolling deployment, monitoring, alerting, database setup, etc. It's generally much easier to use than doing it from scratch.

As with all such systems, this comes at a cost: you initially don't know a lot about the system and figuring out what's wrong might be difficult.
Additionally, AWS EB recently switched to Amazon Linux 2. This new version has a different way to deploy than the previous version "Amazon Linux AMI". As a result, lots of articles and StackOverflow questions/answers are outdated.
The documentation on AWS itself could be a lot better, too. It's not always clear to which version the docs refer to. For example serving static files does not work for Amazon Linux 2.

I deployed a next.js app on AWS EB recently and learned a few tricks. Here's a quick summary of them.

NODE_ENV

To configure the correct NODE_ENV when building and running the application on AWS EB, place the following contents in the folder .ebextensions/options.config:

option_settings:
  aws:elasticbeanstalk:application:environment:
    NODE_ENV: production

.ebignore

.ebignore allows to ignore files when deploying the repository archive using the EB CLI. The format is just like .gitignore and if .ebignore is not present, the deployment uses .gitignore instead. Usually there are certain things which should be in git but not in the deployed archive, hence the need for a .ebignore file.
Here's my example .ebignore:

# dependencies
node_modules/

# repository/project stuff
.idea/
.git/
.gitlab-ci.yml
README.md

# misc
.DS_Store

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# non prod env files
.env.development
.env.test

PORT env variable

Like many other systems, AWS EB exposes the PORT environment variable to specify on which port the app should listen on. If you don't customize the server, ensure to adjust your npm start script in package.json as follows:

"start": "next start -p $PORT"

Using yarn instead of npm

In case you have issues with dependencies not installed correctly (read: weird deployment issues you don't have locally), it might be because you use yarn instead of npm. AWS EB uses by default npm to install your dependencies. If you use yarn, the repository usually has a yarn.lock file instead of a package-lock.json. Here's how to "switch" to yarn instead:

# place in .platform/hooks/prebuild/yarn.sh

#!/bin/bash

# need to install node first to be able to install yarn (as at prebuild no node is present yet)
sudo curl --silent --location https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum -y install nodejs

# install yarn
sudo wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo
sudo yum -y install yarn

# install
cd /var/app/staging/

# debugging..
ls -lah

yarn install --prod

chown -R webapp:webapp node_modules/ || true # allow to fail

Ensure to specify the correct node.js version in the path of the curl command.

"switch" is in quotes because after predeploy eb engine will still run npm install. However it seems to work quite well regardless.
I'd recommend: If you can avoid it, use npm.

Serving static files via nginx

It makes sense to serve static files directly via nginx. This avoids unnecessary load on the node.js server and nginx is generally much faster in serving static content.
Place the following file in .platform/nginx/conf.d/elasticbeanstalk/static.conf:

root /var/app/current/public;

location @backend {
  proxy_pass http://127.0.0.1:8080;
}

location /images/ {
  try_files $uri @backend;

  # perf optimisations
  sendfile           on;
  sendfile_max_chunk 1m;
  tcp_nopush         on;
  tcp_nodelay        on;
}
# add more folders as you need them, using as similar location directive

Additionally you could add caching for the /_next/static path - feel free to try it out. I didn't do it yet to avoid too many changes at once.

GZIP compression

Enabling GZIP Content-Encoding on nginx level requires you to override the default nginx.conf. Find the default nginx.conf in /etc/nginx/nginx.conf, copy the contents to .platform/nginx/nginx.conf and replace gzip off; to gzip on;.
Here's the current (June 2020) example:

#Elastic Beanstalk Nginx Configuration File

user                    nginx;
error_log               /var/log/nginx/error.log warn;
pid                     /var/run/nginx.pid;
worker_processes        auto;
worker_rlimit_nofile    32153;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    include       conf.d/*.conf;

    map $http_upgrade $connection_upgrade {
        default     "upgrade";
    }

    server {
        listen        80 default_server;
        access_log    /var/log/nginx/access.log main;

        client_header_timeout 60;
        client_body_timeout   60;
        keepalive_timeout     60;
        gzip                  on; # CHANGED(mw): enable gzip compression
        gzip_comp_level       4;
        gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;

        # Include the Elastic Beanstalk generated locations
        include conf.d/elasticbeanstalk/*.conf;
    }
}

Finally, disable gzip compression in next.js to avoid double compressing and reduce load on the node.js server.

Deployment

Run, in the following order:

$ npm run build
$ eb deploy

Logging/Debugging

Here's a bunch of important files/directories. You might need sudo to see/read those paths.

Path Directory
/etc/nginx/ Nginx configurations
/var/app/current Deployed application files
/var/app/staging Only during deployment
/opt/elasticbeanstalk Binaries, Configs, ... from AWS EB itself
/var/proxy/staging Nginx staging deployment config
/var/log/eb-engine.log Deployment log
/var/log/web-stdout.log App stdout log
/var/log/nginx Nginx log

Other settings

Ensure to configure your AWS EB setup in the web console as well. Setup rolling deployments and configure monitoring/alarms.

Discussion

markdown guide