Ship It Like a Pro: Node.js on EC2 with Caddy & systemd
Learn how to deploy your Node.js application on an Ubuntu-based AWS EC2 instance using Caddy as a reverse proxy and systemd for process management.

I'm a mobile/web developer 👨💻 who loves to build projects and share valuable tips for programmers
Follow me for Flutter, React/Next.js, and other awesome tech-related stuff 😉
Introduction
I've been learning a lot about AWS lately, and to be honest, there's a lot to understand and learn. I decided to take a little break and try to implement what I've learned so far. At the same time, I'm working on a project that's still in progress, and I'll be announcing it soon. I have a backend app in Node.js for this project and wanted to deploy it. So, I thought, why not deploy it on AWS to test my knowledge and see if I've learned anything?
A little bit about my Node.js app: I'm using JavaScript, and I have MongoDB running. It's on Atlas in the cloud. I think that's all you need to know. If you have a Node.js application running, you can follow similar steps to make your backend accessible to your frontend application or any third-party consumer.
So, without further ado, let's get started.
Set up an AWS Account
First, we need an AWS account, so I'm assuming you already have one. If not, it's really easy to create one. Go to https://aws.amazon.com and create an account.
It might ask for your credit card information, but don't worry, they won't charge you until you exceed the free-tier limit. In this guide, we won't exceed the free tier, so there's no need to worry. Just add your details, and you're all set.

AWS EC2 Instance
So, we are going to use the AWS EC2 service to deploy our application to AWS. If you're not familiar with EC2, think of it as renting a virtual machine, similar to your laptop or PC. There are many benefits, such as storing data, choosing your operating system, deciding how much computing power you need, selecting the number of CPU cores, RAM, and the type of network you want to use, and more.
You can customize your virtual machine as you like and rent it from AWS, which is the power of the cloud.
What we'll do is copy our local app, with all its files and folders, directly to this virtual machine and then run the app there. The advantage is that anyone with the link can access it, not just us on our local machine.
Creating EC2 Instance
- Simply search for EC2 in the search bar and click on it to open the service page.

Once it's open, you will see a page like this.

- Simply click on a Launch Instance on Dashboard and give it a name.

- It will also ask you to select the operating system image. Choose Ubuntu and scroll down.

- We will use the
t2.microinstance because it is part of the free tier. If you need a higher configuration CPU, you can check other instances and select the one that meets your needs.

- To connect to the instance from our local machine or anywhere else, we need access. To do this, we generate a key pair, which we can use to connect to our AWS instance and launch it in our local environment.

- Click on "Generate Key Pair," give it a relevant name, and make sure to select the RSA key pair type. Finally, create the key pair, and it will download to your local machine.

In the network settings, select:
Allow SSH traffic from - My IP - Because we only want to access the instance from our local machine.
Allow HTTPS traffic from the internet (so our application can access our Node.js server using an endpoint or URL with HTTPS)
Allow HTTP traffic from the internet (so our application can access our Node.js server using an endpoint or URL with HTTP)
That's it. Now, review the summary and launch the instance.
Connecting via SSH
- Once it’s created, click on
Connect to your instance

- This will redirect you to the interface shown below. Here, you can run the instance within AWS Cloud itself and access it, or you can use the SSH Client to access this instance from your local machine, which is what we want.

- Before setting up SSH, remember that we downloaded the
Key-Pair. Let's first place it in the correct location.

- I am using a Mac, and I usually store my SSH keys inside the
.sshfolder located in the home directory. Let's first navigate to the home directory.
cd ~/
- Now, move the key pair from the download folder to the .ssh folder using the
mvcommand below:

- Once it’s done, we are good to go. So let’s follow these instructions.

- Run
chmodA command to change the permission to ensure your key is not publicly viewable.

Now run
ssh -i "<your-key-name>.pem" ubuntu@<your-ip>to connect to the instance we created.

- Type
yesand press Enter

As you can see, we are now inside the instance we created. Isn't it cool that we can access it from our local machine?
Now you can perform any tasks that you would normally do on your local machine.
Installing/Updating Packages
- Now that we have our instance connected, before starting anything, we need to ensure we are using the latest packages in Ubuntu and install any available updates. To do this, we run two commands:
sudo apt update

- Which refreshes package lists (checks for updates) and then
sudo apt upgrade

- Which Installs available updates for installed packages
Installing Node.js in an EC2 instance
Now that we have all the latest packages, we need to install Node.js (or the backend framework/library you need).
To do this, we need to download and run the NodeSource setup script, which configures the system to install Node.js version 20 from their official repository.
Use the command below to do that:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -

Once the NodeSource repository is added, we need to install Node.js and npm (Node.js’s package manager) on our system to use Node.js.
To do this, run the command below
sudo apt-get install -y nodejs

Pushing Local App to EC2
- Now that we have the Node environment set up, we can upload our local Node.js app to this instance. We can do this using the SSH connection we already set up.
rsync -avz --exclude 'node_modules' --exclude '.git' --exclude '.env' \
-e "ssh -i ~/.ssh/your-key.pem" \
. ubuntu@ip-address:~/app
- In simple terms, this command copies the current folder (
.) to the remote server’s~/appdirectory using SSH. It skips unnecessary files likenode_modules,.git, and.envbecause we don't need those.
NOTE: Be sure to replace ‘your-key’ with your SSH key name in the command above, and replace ‘ip-address’ with your actual IP address.
- To execute this command, go to your code directory and run the command above.

If you now run the
lscommand in your instance's home directory, you will see theappfolder has been created.

And if you use
cdto enter it, you will see all your source code.
Setting up Environment (.env) File.
If you have a database running locally, like PostgreSQL or MySQL, you can install those on this instance just like we did for Node.js.
I have MongoDB running on MongoDB Atlas. So, to allow this instance to access the database, we need to add the instance's IP to the whitelist. Simply copy the instance's IP address and paste it into the IP Access List entry.

Now that everything is set up, we need one more file: the
.envfile. Remember, we excluded it when we transferred our files to the instance.So let’s create the
.envfile
cd app/
sudo vim .env # This will create and open .env file
# .env
# paste your environment variables
- You can exit the Vim editor by pressing
esc, then:(colon), typingwq, and pressing enter.

As you can see, we now have the
.envfile in place.Before we run our app, let’s make sure we have downloaded all the required packages by running
npm i

- Now, if we run the app, it should run without any errors.

Configuring Security Rules
- To check if it is running, go to the instance dashboard, select your instance, copy the Public IP address, and paste it into your browser.


- We can't see anything, and it didn't work. Why? Do you remember the security group we set up earlier?

- We haven't specified our IP address and because we are accessing it from our machine we must add our IP address with port numner. So, let's add our IP address and the port number to the inbound rules and see if it works now.

Click on Add Rules and add Custom TCP and specify your IP by selecting My IP.
Now, if you run it, it should work fine. I am hitting
/testthe endpoint, which shows Hello World text.

Running App in Background (systemd)
But here we have two major problems, and I hope you already know what they are.
One big issue is that it's running on PORT
7575and doesn't have a domain name. The second issue is that it's running on our local terminal, so if we close this terminal, we won't be able to access our server using that public IP address.First, let's address the second issue. To fix that, we need to find a way to run the application in the background so that no matter what we do in the terminal, the server will stay up the whole time.
If you want to run your Node app as a background service, we use the
systemdcommand.We need to create a service file. This file tells systemd (Linux’s init system) how to start, monitor, and restart your Node.js app as a background service, similar to how SSH or MongoDB are managed.
Run the command below to create a new service file.
sudo vim /etc/systemd/system/myapp.service
- And paste lines below in it once Vim is opened
[Unit]
Description=Node.js App
After=network.target multi-user.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/app
ExecStart=/usr/bin/npm start
Restart=always
Environment=NODE_ENV=production
EnvironmentFile=/home/ubuntu/app/.env
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
In the [Unit] section, we include our app description and ensure our app starts after networking is ready (so Node can bind to a port).
In the [Service] section, we specify how the app will run by including the working directory, environment, and other settings.
Now run
sudo systemctl daemon-reload
- Which Reloads systemd so it notices the new service file.
sudo systemctl enable myapp.service
- Enables the service to auto-start on boot.
sudo systemctl start myapp.service
- Start it right now (without reboot).

sudo systemctl status myapp.service
- Shows current status, logs, and whether it’s running or failed.

- And now, if you kill your terminal/app and still hit the URL, it should work.


Setting up Reverse Proxy (using Caddy)
The next step is to allow HTTP access directly, without using PORT 7575. Then, we will add a domain name and an SSL certificate.
To keep your actual web server,
localhost:7575, hidden from the world and exposing only the HTTP and HTTPS ports, which are 80 and 443, we use reverse proxies like Nginx or Caddy.A reverse proxy is a server that sits in front of your application servers and manages all incoming client requests before sending them to your backend app, such as your Node.js service.
We use this for security purposes. It allows us to add a firewall before requests reach the actual server. It also enables load balancing, as the reverse proxy can distribute requests to multiple servers.
http://54.86.58.33:7575/
We do
https://api.mydomain.com
To implement this, there are many services like Nginx and Caddy. For this example, we'll use Caddy because it's easy to set up.
Caddy is a modern, lightweight reverse proxy and web server, similar to Nginx but with a major advantage:
Visit this URL and run all the commands.

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
- Once it is done, let’s remove the custom TCP rule we added in the security group for our IP

- And now, if we run
sudo systemctl start caddy
- And then visit the IP address
54.86.58.33Without mentioning any port, you should see a Caddy page

- But what we want is to forward the traffic to our application instead of showing this page. So, we will edit the Caddy file by opening it using the command below:
sudo vim /etc/caddy/Caddyfile

Comment down the
rootandfile_serverline and uncomment thereverse_proxyline and add your port.And now let’s restart Caddy
sudo systemctl restart caddy
- If you again run this URL, you should be able to see your app.

That’s it, now you can use this URL anywhere to access the backend APIs.
You can also set up a custom domain name and attach it to this current IP address (for example, example.com) or something like this using the Route53 service.
Conclusion
Thank you for taking the time to read my blog. I hope you enjoyed it and learned something new. I'll be sharing more about AWS soon as I continue to learn. Keep learning!!
Until then…






