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.

Search for a command to run...
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.

No comments yet. Be the first to comment.
Lessons from Code, Content, and Consistency

Reliability basics: Outbox pattern + why it matters

Introducing Kafka/Redpanda + move to event-driven workflow

Connect Orders ↔ Inventory (first working service-to-service flow)

Inventory service + gRPC + proto contracts

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.
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.

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.

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



t2.micro instance 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.


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.
Connect to your instance

Key-Pair. Let's first place it in the correct location.
.ssh folder located in the home directory. Let's first navigate to the home directory.cd ~/
mv command below:

chmod A 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.

yes and 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.
sudo apt update

sudo apt upgrade

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

rsync -avz --exclude 'node_modules' --exclude '.git' --exclude '.env' \
-e "ssh -i ~/.ssh/your-key.pem" \
. ubuntu@ip-address:~/app
.) to the remote server’s ~/app directory using SSH. It skips unnecessary files like node_modules, .git, and .env because 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.

If you now run the ls command in your instance's home directory, you will see the app folder has been created.


And if you use cd to enter it, you will see all your source code.
.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 .env file. 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
esc, then : (colon), typing wq, and pressing enter.
As you can see, we now have the .env file in place.
Before we run our app, let’s make sure we have downloaded all the required packages by running
npm i






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 /test the endpoint, which shows Hello World text.

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 7575 and 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 systemd command.
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
[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
sudo systemctl enable myapp.service
sudo systemctl start myapp.service

sudo systemctl status myapp.service



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

sudo systemctl start caddy
54.86.58.33 Without mentioning any port, you should see a Caddy page
sudo vim /etc/caddy/Caddyfile

Comment down the root and file_server line and uncomment the reverse_proxy line and add your port.
And now let’s restart Caddy
sudo systemctl restart caddy

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.
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…
