Setting Up NestJS on AWS Lightsail Steps
Software#ubuntu#nestjs#nginx#pm2#aws#mongodb#mailgun#javascript
Why NestJS?
NestJS is a server-side framework, that can be used to create an API.
NestJS is a javascript framework, I'd say it was more of a proper framework compared to something like Node and Express which would be a very simple way to create an API. You get a lot of functionality out of the box with NestJS, and the code is set up to be structured. It's set up to use TDD and Typescript. However, NestJS is more difficult to use for the first time than a simple Node/Express API would be.
There are many different options for how to code an API. Alternatives would be Node/Express in the javascript universe, or something like Laravel in PHP, or a service such as Google's Firebase. I'd used NestJS before and wanted to use it again.
Why put NextJS on Lightsail?
Whereas normally you might have an automated deployment process with Docker, for testing or as a dev server you might just want a cheap instance of Lightsail to save money. You may also be new to NestJS and appreciate the process of getting it set up manually on an instance. In the AWS ecosystem, Lightsail will be a lot cheaper than an EC2 instance.
What are we showing?
This is mainly about how to set up a NestJS project that you already have running locally, but I've also looked at the code for sending email and connecting to the database briefly for reference.
We'll...
- Get the NestJS up and running on a Lightsail "OS Only" instance with Ubuntu 24.04 using Nginx and PM2.
- Make sure a ReactJS front end can communicate with it.
- Take a quick look at sending email using Mailgun.
- Use MongoDB as a database.
- Have a look at some troubleshooting methods.
Setting up the Ubuntu instance
To get a quick and dirty API set up on Ubuntu 24.04 on AWS Lightsail, here's a quick walkthough.
- Create an "OS Only" Ubuntu 24.04 instance in the Lightsail control panel.
- Log in to instance
sudo apt-get update
sudo apt upgrade -y
sudo apt autoremove
sudo reboot
When you log back in you should see something like this...
Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-1016-aws x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Thu Sep 26 17:53:22 UTC 2024
System load: 0.01
Usage of /: 15.1% of 18.33GB
Memory usage: 48%
Swap usage: 0%
Temperature: -273.1 C
Processes: 109
Users logged in: 0
IPv4 address for ens5: 172.26.13.208
IPv6 address for ens5: 2a05:d01c:78d:5900:f364:2234:b4bc:3deb
* Ubuntu Pro delivers the most comprehensive open source security and
compliance features.
https://ubuntu.com/aws/pro
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
Last login: Thu Sep 26 17:41:53 2024 from 52.94.39.60
Making the NestJs API
Now, make the directory you want to store the API code in. If you have a NestJS project on your computer you can just FTP the files into the new directory.
mkdir api
cd api
- FTP files into "api" directory
NPM 1
Ubuntu is up-to-date, the code is in place.
Now, we need to run the npm install
command, but first we'll need to install NPM...
$ sudo apt install npm -y
$ npm -v
9.2.0
$ sudo npm install -g npm@latest```
$ npm -v
9.2.0
$ sudo reboot
- Log in again
$ npm -v
10.8.3
$ cd api
$ npm install
This resulted in the process gradually getting slower and slower and eventually stopping...
$ npm install
⠦Killed
Add Swap Space to Ubuntu
To fix the error we had with NPM, a quick search and it might be that we don't have any swap space, so let's set that up.
How To Add Swap Space on Ubuntu 22.04
sudo swapon --show
No output shown.
$ free -h
total used free shared buff/cache available
Mem: 416Mi 188Mi 174Mi 2.7Mi 84Mi 228Mi
Swap: 0B 0B 0B
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 19G 3.1G 16G 17% /
tmpfs 209M 0 209M 0% /dev/shm
tmpfs 84M 880K 83M 2% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
efivarfs 128K 3.8K 120K 4% /sys/firmware/efi/efivars
/dev/nvme0n1p16 881M 133M 687M 17% /boot
/dev/nvme0n1p15 105M 6.1M 99M 6% /boot/efi
tmpfs 42M 12K 42M 1% /run/user/1000
$ sudo fallocate -l 1G /swapfile
$ ls -lh /swapfile
-rw-r--r-- 1 root root 1.0G Sep 26 18:22 /swapfile
$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile
Setting up swapspace version 1, size = 1024 MiB (1073737728 bytes)
no label, UUID=98ad9322-669f-4821-b3d1-38445da1a258
$ sudo swapon /swapfile
$ sudo swapon --show
NAME TYPE SIZE USED PRIO
/swapfile file 1024M 0B -2
$ free -h
total used free shared buff/cache available
Mem: 416Mi 189Mi 159Mi 2.7Mi 99Mi 227Mi
Swap: 1.0Gi 0B 1.0Gi
$ sudo cp /etc/fstab /etc/fstab.bak # Backup fstab incase anything goes wrong
$ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # Make the swap file permanent
$ cat /proc/sys/vm/swappiness
60 # 60 is ok for a desktop
$ sudo sysctl vm.swappiness=10 # Set to 10 for a server
vm.swappiness=10
Add this to the bottom of /etc/sysctl.conf...
vm.swappiness=10
$ cat /proc/sys/vm/vfs_cache_pressure
100
$ sudo sysctl vm.vfs_cache_pressure=50
vm.vfs_cache_pressure = 50
Add this to the bottom of /etc/sysctl.conf...
vm.vfs_cache_pressure = 50
NPM 2
Now that we have some swap space, let's try the npm install
again.
If the "node_modules" directory exists, delete it and start again...
rm -rf node_modules/
npm install
npm warn deprecated @humanwhocodes/[email protected]: Use @eslint/object-schema instead
npm warn deprecated @humanwhocodes/[email protected]: Use @eslint/config-array instead
npm warn deprecated [email protected]: This module is not supported, and leaks memory. Do not
use it. Check out lru-cache if you want a good and tested way to coalesce async requests
by a key value, which is much more comprehensive and powerful.
npm warn deprecated [email protected]: This package is no longer supported.
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm warn deprecated [email protected]: Rimraf versions prior to v4 are no longer supported
npm warn deprecated [email protected]: This package is no longer supported.
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm warn deprecated [email protected]: This package is no longer supported.
added 980 packages, and audited 981 packages in 11m
153 packages are looking for funding
run `npm fund` for details
32 high severity vulnerabilities
To address issues that do not require attention, run:
npm audit fix
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
That worked, let's try building the application...
npm run build
> [email protected] build
> nest build
All working so far, let's see if we can get the app connected to the internet...
Nginx
Nginx has to be installed, then configured...
sudo apt install nginx
sudo nano /etc/nginx/sites-available/api.conf
server {
listen 80;
server_name api.website.com;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:3000/;
proxy_set_header Access-Control-Allow-Origin *;
}
}
$ sudo unlink /etc/nginx/sites-enabled/default
$ sudo ln -s /etc/nginx/sites-available/api.conf /etc/nginx/sites-enabled/
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl enable nginx
$ sudo service nginx restart
PM2
To get the API to be always running on port 3000 we can use PM2...
ecosystem.config.js...
module.exports = {
apps: [
{
name: 'api',
script: './dist/src/main.js',
},
],
};
$ sudo npm install pm2@latest -g
$ pm2 completion install
$ pm2 -v
5.4.2
Make ecosystem.config.js file in the root
pm2 start
pm2 logs
pm2 plus #Sign up to view the web dashboard
If there are any problems pm2 logs
should tell you what they are.
CORS
main.ts...
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
cors: true,
});
Also, in the Nginx config file we had...
proxy_set_header Access-Control-Allow-Origin *;
Connecting to a Database
In this example I'm connecting to MongoDB, a service that I don't need to do much setup to so I can get this example working pretty quickly.
I won't go too much into how NestJS and TypeORM work with MongoDB, but the official docs are pretty good...
One thing to bear in mind is to whitelist the IP of the Lightsail instance for your MongoDB database. Trying to connect to a database but not being allowed to because the IP is not whitelisted will generally be a bad thing.
NestJS and communicating with a database
If you haven't used NestJS before it can be quite daunting. I'm including some sample code to give an idea of how it works as even the official docs can be confusing.
To begin with, to test the connection and get some database calls happening pretty quickly I created a user.schema.ts like this, below. I am validating the email field in the Schema to make sure it is a valid email...
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type UserDocument = HydratedDocument<User>;
const validateEmail = function (email: string): boolean {
const re = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
return re.test(email);
};
@Schema()
export class User {
@Prop({
type: String,
required: [true, 'Username required'],
validate: [validateEmail, 'username must be a valid email address'],
unique: [true, 'email address already used, try again'],
})
username: string;
@Prop({
type: String,
required: [true, 'Password required'],
hide: true,
})
password: string;
@Prop({ type: Date })
created_at: Date;
@Prop({ type: Date })
last_logged_in_at: Date;
}
export const UserSchema = SchemaFactory.createForClass(User);
Then, my user.service.ts could be something like this...
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from '../common/schemas/user.schema';
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<User>,
) {}
async getAllUsers(): Promise<User[]> {
return await this.userModel.find().exec();
}
async getUser(username: string): Promise<User> {
return await this.userModel.findOne({ username: username });
}
}
I have the password getting checked in the Validation, for example. If all emails and passwords must be in a certain format, I will not even ask the database if they are in an incorrect format...
import {
IsEmail,
IsOptional,
IsString,
Matches,
MinLength,
} from 'class-validator';
export class CreateUserFormDto {
@IsEmail()
username: string;
@IsString()
@MinLength(8)
@Matches(/\d/, {
message: 'password must contain a number',
})
@Matches(/[a-z]/, {
message: 'password must contain a lowercase letter',
})
@Matches(/[A-Z]/, {
message: 'password must contain an uppercase letter',
})
@Matches(/[`!@#$£%^&*()_+\-=\[\]{};':"\\|,\.<>\/?~]/, {
message: 'password must contain a special character',
})
password: string;
@IsOptional()
created_at: Date;
@IsOptional()
last_logged_in_at: Date;
}
Sending email
Check the correct email connection info using swaks...
sudo apt-get install swaks
What we're doing here is checking that the server, username (--au) and password (--ap) are correct before we try to connect in the app. In this case I am using my Mailgun info, but you should be able to use any email provider, although some may have a more complex log in system.
./swaks --auth \
--server smtp.mailgun.org \
--to [email protected] \
--body 'This is a swaks test' \
--h-Subject 'Swaks Test!' \
--au [email protected] \
--ap 'TheAPIKeyOrPassword'
Once you have the SMTP info that works, choose an email package to use with NestJs... As I am using Mailgun, I tried a couple of different packages before just plumping for a general email sending package...
- Do no use mailgun-js, deprecated.
- Do not use mailgun.js, doesn't seem to work.
- Use nodemailer.
npm install --save @nestjs-modules/mailer nodemailer
npm install --save-dev @types/nodemailer
Then sending an email can be something like this, for example, email.service.ts...
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
@Injectable()
export class MailgunService {
constructor(private readonly mailService: MailerService) {}
async sendNewUserMail(username: string, subject: string, message: string) {
await this.mailService.sendMail({
from: '[email protected]',
to: username,
subject: subject,
text: message,
});
}
}
Example app.module.ts
- config module allows .env to be used
- mongoose module to connect to the mongodb instance
- jwt for the auth methods
- mailer to connect to mailgun
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { MailerModule } from '@nestjs-modules/mailer';
import { JwtService } from '@nestjs/jwt';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
MongooseModule.forRoot(process.env.MONGOOSE_CONNECTION),
MailerModule.forRoot({
transport: {
host: process.env.MAILGUN_DOMAIN,
auth: {
user: process.env.MAILGUN_USER,
pass: process.env.MAILGUN_API_KEY,
},
},
}),
UserModule,
],
controllers: [AppController],
providers: [AppService, JwtService],
})
export class AppModule {}
Troubleshooting a NestJS API
Update pm2 to the latest version
sudo npm install pm2@latest -g
Re-install node modules and clean cache
rm -rf node_modules
rm package-lock.json
npm cache clean --force
npm install
Upgrade NVM
Upgrade NVM to the latest version...
$ nvm -v
10
$ nvm install 14
Debugging
View PM2 logs to see any error messages or console.logs...
pm2 logs
cat /home/ubuntu/.pm2/logs/api-error.log
Or, if you're just using node, you can cat or tail the error logs where you specified.
Is the database connected?
When first setting up NestJs for the first time, something that may not give helpful error messages might be if the database is not connected properly.
For example, I had to whitelist the API instance IP address with the database service for the database to work. Because the database was not connecting successfully I was unable to get the application working at all until the IP was whitelisted.
Node
It's also possible to run the API on port 3000 using node. To run the application and write to the access and error logs, instead of displaying the info on screen, like this...
node dist/main.js > api.log 2> api_error.log
Try/Catch
All the usual methods for troubleshooting can apply. Try/catch can be particularly useful.
Summary
This has been an overview of some of the basic things to look at when initially setting up NestJS on an Ubuntu Lightsail instance. I've included a little bit of code the aid in understanding, but this is mainly looking at the general concepts. While we get the CORS working, connect to the database, and send email, there are plenty of other things to consider and that a fully functioning website will need. The code is to give anyone who is brand new to NestJS and wants to learn some of the concepts an idea to work from.
I have tried to link to other guides I've used here, but if I haven't posted a link I probably have used another guide as a template and made several stackoverflow queries in each section.