Deploying next.js on AWS ElasticBeanstalk
Tags: react, nextjs, aws, elasticbeanstalk
Published: 2020-06-10 06:04:43

[AWS ElasticBeanstalk (EB)](https://aws.amazon.com/elasticbeanstalk/) 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](https://aws.amazon.com/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](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/environment-cfg-staticfiles.html) does not work for Amazon Linux 2.

I deployed a [next.js](https://nextjs.org/) 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`](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/eb-cli3-configuration.html#eb-cli3-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](https://nextjs.org/docs/advanced-features/custom-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](https://steveholgado.com/nginx-for-nextjs/#caching-static-assets-with-nginx). 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](https://nextjs.org/docs/api-reference/next.config.js/compression) 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](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features.rolling-version-deploy.html?icmpid=docs_elasticbeanstalk_console) and configure [monitoring/alarms](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/environments-health.html).