Just returning from an intense gaming streak featuring Resident Evil and Marvel's Spiderman 2, culminating with the completion of Marvel's Spiderman Remastered, it's been quite the challenge. Having conquered Resident Evil 3 Remake, I've found a renewed interest in platinum achievements for more games, especially after a two-month job hunt.
Now, let's dive into the main topic – an approach to releasing a web stack to production, employing the following technologies:
Vite
(Frontend)Express.js
(Backend)Postgres
(Database)AWS EC2
(VPS instance)pm2
(Task Runner)github
(Source Control)nginx
(Web server)
For part 1, we will deploy manually, while part 2 will cover the use of Jenkins
for automated deployment. I felt the content might be extensive for a single post, so I split it into two parts.
Deploying web stack to AWS EC2 Ubuntu server
Illustration of the Interactions between Components using nginx
as the Web Server:
1. Setup EC2 instance
Below is a video to demonstrate how to setup EC2
instance, then we can SSH
into it to do other installations
2. Install and Configure PostgreSQL
Update packages
sudo apt update && sudo apt upgrade -y
Install PostgreSQL
sudo apt install postgresql postgresql-contrib -y
Postgres installation will have automatically created a postgres user on Ubuntu as well to allow local connection. this can be verified by running the command:
ubuntu@ip-172-31-18-43:~$ sudo cat /etc/passwd | grep -i postgres
postgres:x:115:123:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
To connect to Postgres, switch to the postgres
user and run psql
:
ubuntu@ip-172-31-18-43:~$ sudo -i -u postgres
postgres@ip-172-31-18-43:~$ psql
psql (14.9 (Ubuntu 14.9-0ubuntu0.22.04.1))
Type "help" for help.
postgres=#
Our Backend connects to database by default postgres
user, so we don't need to create another user. But if you need to you can:
postgres@ip-172-31-18-43:~$ createuser --interactive
Enter name of role to add: steam
Shall the new role be a superuser? (y/n) y
Verify the new steam
user was created successfully:
postgres@ip-172-31-18-43:~$ psql
psql (14.9 (Ubuntu 14.9-0ubuntu0.22.04.1))
Type "help" for help.
postgres=# \du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
steam | Superuser, Create role, Create DB | {}
postgres=#
Right now the postgres
user in Postgres does not have a password associated with it. We will need to add a password:
ubuntu@ip-172-31-18-43:~$ sudo -i -u postgres
postgres@ip-172-31-18-43:~$ psql
psql (14.9 (Ubuntu 14.9-0ubuntu0.22.04.1))
Type "help" for help.
postgres=# ALTER USER postgres PASSWORD 'password';
ALTER ROLE
3. Clone github code to server
Find a place to store your application code. In this example in the ubuntu
home directory a new directory called apps
will be created. Within the new apps directory another directory called ec2-app
. Feel free to store your application code anywhere you see fit
ubuntu@ip-172-31-18-43:~$ cd ~
ubuntu@ip-172-31-18-43:~$ mkdir apps
ubuntu@ip-172-31-18-43:~$ cd apps
ubuntu@ip-172-31-18-43:~/apps$ mkdir ec2-app
Additional steps need to be taken in order to clone github repository with SSH
.
Navigate to .ssh
folder in ~
path
cd ~/.ssh
Generate key, enter as prompt, remember to leave passphrase
empty since we will use this for github-actions
later and you can't enter password when actions run the commands
ubuntu@ip-172-31-18-43:~/.ssh$ ssh-keygen -t rsa -b 4096 -C "[email protected]"
Generating public/private rsa key pair.
Enter file in which to save the key (/home/ubuntu/.ssh/id_rsa): github-actions
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in github-actions
Your public key has been saved in github-actions.pub
Create a config
file in .ssh
folder if it is not existed
cd ~/.ssh
sudo nano config
Add configuration to use github-actions
as default key when we pull from repository
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/github-actions
Start the ssh-agent
in the background
eval "$(ssh-agent -s)"
Agent pid 17446
Navigate to .ssh
folder again, then open generated .pub
cat github-actions.pub
ssh-rsa xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx [email protected]
Copy content then close editor with Shift + : => :q
Add the key under Github Setting page tab SSH and GPG keys -> New SSH key
4. Install Node
Follow detail steps in:
https://github.com/nodesource/distributions/blob/master/README.md#nodejs
Download and import the Nodesource GPG key
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
Create deb repository
NODE_MAJOR=21
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
Run Update and Install
sudo apt-get update
sudo apt-get install nodejs -y
Check node
and npm
version
node -v
v21.1.0
npm -v
10.2.0
Run npm install
in each app folder for Frontend and Backend
cd ~/apps/ec2-app/aws-ec2-sample-fe
npm install && npm run build && npm run dev
➜ Local: http://localhost:4173/
➜ Network: use --host to expose
➜ press h to show help
cd ~/apps/ec2-app/aws-ec2-sample-be
npm install && npm start
Server running on port 3001
Connected to the database
5. Install and Configure NGINX
Install and enable nginx
sudo apt install nginx -y
sudo systemctl enable nginx
We can check if nginx
works properly by accessing Public IPv4 address
which you can get in EC2 instance detail.
Navigate to /etc/nginx/sites-available
cd /etc/nginx/sites-available
There should be a server block called default
ubuntu@ip-172-31-18-43:/etc/nginx/sites-available$ ls
default
The default server block is what will be responsible for handling requests that don't match any other server blocks. Right now if you navigate to your server ip, you will see a pretty bland html page that says NGINX is installed. That is the default server block in action.
We will need to configure a new server block for our website in /etc/nginx/sites-available
folder, best practice to name server file as our domain name, so let's create steambyte.dev
file since I have one spare
domain name from Cloudflare
.
cd /etc/nginx/sites-available
sudo touch steambyte.dev
Open steambyte.dev
and modify it:
server {
listen 80;
listen [::]:80;
server_name www.steambyte.dev steambyte.dev
location / {
# Frontend server
proxy_pass http://127.0.0.1:4173/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /api {
# Backend server
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Enable the new site
Enable the Server Block by creating a symbolic link from your configuration file in sites-available
to the sites-enabled
directory to enable the server block:
sudo ln -sf /etc/nginx/sites-available/steambyte.dev /etc/nginx/sites-enabled/
Test configuration by verify that your nginx
configuration is correct by running:
sudo nginx -t
Finally, reload nginx
to apply the new configuration:
systemctl restart nginx
It may require you to enter ubuntu
password
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to restart 'nginx.service'.
Authenticating as: Ubuntu (ubuntu)
Password:
Since we have not set on for the account, let's do it by switching to root
ubuntu@ip-172-31-18-43:/etc/nginx/sites-available$ sudo su -
root@ip-172-31-18-43:~# passwd ubuntu
New password:
Retype new password:
passwd: password updated successfully
Let's restart nginx
again
systemctl restart nginx
Check nginx
running status:
ubuntu@ip-172-31-18-43:/etc/nginx/sites-enabled$ systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2023-11-09 03:25:34 UTC; 4min 11s ago
Docs: man:nginx(8)
Process: 27916 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
Process: 27917 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
Main PID: 27918 (nginx)
Tasks: 2 (limit: 1121)
Memory: 2.5M
CPU: 27ms
CGroup: /system.slice/nginx.service
├─27918 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
└─27919 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" >
Nov 09 03:25:34 ip-172-31-18-43 systemd[1]: Starting A high performance web server and a reverse proxy server...
Nov 09 03:25:34 ip-172-31-18-43 systemd[1]: Started A high performance web server and a reverse proxy server.
6. Install and Configure pm2
We use a process manager like pm2
to handle running our frontend and backend from background. It will also be responsible for restarting the App if/when it crashes.
sudo npm install pm2 -g
Start Frontend and Backend with pm2
# Navigate to frontend folder
pm2 start npm --name fe -- start
┌────┬───────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼───────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ fe │ default │ N/A │ fork │ 6764 │ 22s │ 0 │ online │ 0% │ 66.1mb │ ubuntu │ disabled │
└────┴───────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Then do the same for in backend folder
pm2 start npm --name be -- start
┌────┬───────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼───────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 1 │ be │ default │ N/A │ fork │ 6814 │ 0s │ 0 │ online │ 0% │ 19.8mb │ ubuntu │ disabled │
│ 0 │ fe │ default │ N/A │ fork │ 6764 │ 22s │ 0 │ online │ 0% │ 66.1mb │ ubuntu │ disabled │
└────┴───────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
To ensure that PM2 processes start automatically upon an EC2 instance reboot, you can set PM2 as a startup script. On Ubuntu, you can use the following steps:
First, save the current PM2 configuration:
pm2 save
Then, generate the startup script:
ubuntu@ip-172-31-18-43:~/apps/ec2-app/aws-ec2-sample-be$ pm2 startup
[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu
Then run the script
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu
Finally, to ensure your current processes are resurrected on startup, run:
pm2 save
This will set up PM2 to restart your Node.js applications managed by PM2 in case of any server reboots.
Make sure to replace the placeholders with the actual paths and adjust the configurations according to your specific requirements and file structures.
7. Config Cloudflare DNS to EC2 instance
Copy Public IPv4 address
from your EC2 instance
Open DNS Configuration
for your domain, then add 2 record for example.com
and www
which point to the IPv4 we just copied from EC2
8. Enable Firewall (optional)
sudo ufw status
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable
sudo ufw status
9. Enable SSH with Let's Encrypt (optional)
Nowadays almost all websites use HTTPS exclusively. Let's use Let's Encrypt to generate SSL certificates and also configure NGINX to use these certificates and redirect http traffic to HTTPS.
The step by step procedure is listed at: https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx.html
Install Certbot
sudo snap install --classic certbot
Prepare the Certbot command
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Get and install certificates using interactive prompt
sudo certbot --nginx
10. Go Live
Live site at https://steambyte.dev/ showcasing basic functionalities, emphasizing our demonstration of the AWS EC2
deployment workflow in this post. Stay tuned for our next post, focusing on automatic deployment, eliminating the need for manual pm2
restarts during deployments.