Give this skill to your AI — it will guide you through the end-to-end lifecycle of deploying and managing Node.js/Next.js web applications on AWS EC2 using Ubuntu. The workflow encompasses infrastructure provisioning via Terraform, server configuration (Caddy, systemd), project deployment via rsync, DNS management with Route 53, and ongoing maintenance using journalctl.
To follow this tutorial and execute this skill, the user will need:
CRITICAL AI INSTRUCTION: When starting this skill, you must proactively guide the human to answer the following questions before generating any code or executing commands:
us-west-2)t3.micro)Once the user answers the questions:
templates/main.tf as a base or write a new one based on the user's answers.aws_instance resource.output block to display the public IP of the EC2 instance.terraform init, terraform fmt, terraform validate, terraform plan, and terraform apply.connect.sh) and make it executable so that for future steps like deployment, the user can easily reference this file to connect to the EC2 instance:#!/bin/bash
ssh -i <path-to-key-pair.pem> ubuntu@<public-ip>chmod +x connect.sh. The user can then connect anytime by simply running ./connect.sh.If the user wants to temporarily pause their work, they can stop the EC2 instance to save costs without destroying it.
aws ec2 stop-instances --instance-ids <instance-id>.aws ec2 start-instances --instance-ids <instance-id>.connect.sh script will naturally become outdated. After starting the instance, you MUST update the connect.sh file with the new public IP address (you can retrieve the new IP by running terraform refresh to update the state, followed by reading the updated output, or by using the AWS CLI).If the user is unsatisfied with the previously created EC2 instance or no longer needs it, you can destroy the infrastructure.
CRITICAL AI INSTRUCTION: Before destroying any infrastructure, you MUST confirm with the user again to ensure they really want to delete the instance. Do not proceed until you have received explicit permission.
You can offer the user two methods to destroy the infrastructure:
aws_instance resource block in main.tf.outputs.tf (e.g., public IP or DNS).terraform apply. Terraform will plan to destroy only the removed resources.terraform destroy to tear down all resources managed by the current configuration.Reference: Terraform AWS Destroy Tutorial
Connect to the EC2 instance using the connect.sh script.
CRITICAL NOTE: The following commands MUST be executed directly on the remote EC2 instance after you have successfully connected via SSH, NOT on your local machine.
Update the system packages on the EC2 instance:
sudo apt update
sudo apt upgradeInstall Node.js on the EC2 instance:
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejsCRITICAL AI INSTRUCTION: Before transferring files to EC2, you must identify the user's project stack and guide them through the correct build/preparation process. IMPORTANT: Everything must be based on the user's specific project. You must fully discuss the deployment approach with the user before proceeding. The steps outlined below are only suggestions; the user's actual project requirements always take the highest priority:
package.json has the correct start script.systemd so the app runs in the background.next.config.ts (or .js) to include output: 'standalone'.npm run build locally..next/standalone folder will be the main deployment artifact.public folder and .next/static folder into .next/standalone/public and .next/standalone/.next/static respectively, as the standalone build doesn't include them automatically.node server.js inside the standalone folder on the server. Advise the user to use systemd so the app runs in the background.npm run build locally.dist or build folder contains all needed assets.Nginx, or a simple Node.js static server like serve.build.sh) containing all the necessary build and preparation commands discussed and agreed upon with the user.chmod +x <script-name>.sh. The user can then execute the steps anytime by simply running ./<script-name>.sh.cd) to the directory containing the build artifacts (e.g., the .next/standalone folder, or another appropriate build directory depending on the project stack).rsync command to securely transfer the files to the EC2 instance, excluding unnecessary files like .git and .env.Command Example:
cd <build-artifact-directory>
rsync -avz --exclude '.git' --exclude '.env' \
-e "ssh -i <path-to-key-pair.pem>" \
. ubuntu@<public-ip>:~/app(Note: You can usually extract the SSH key path, user (ubuntu), and public IP information from the previously generated connect.sh script.)
CRITICAL NOTE: The following commands MUST be executed directly on the remote EC2 instance after you have successfully connected via SSH, NOT on your local machine.
sudo vim /etc/app.env
sudo chmod 600 /etc/app.env
sudo chown ubuntu:ubuntu /etc/app.envCRITICAL NOTE: The following commands MUST be executed directly on the remote EC2 instance after you have successfully connected via SSH, NOT on your local machine.
Open vim to create the service file:
sudo vim /etc/systemd/system/myapp.serviceEnter the following content (please note that this might need adjustment based on your specific project):
[Unit]
Description=Node.js App
After=network.target multi-user.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/app
ExecStart=/usr/bin/node server.js
Restart=always
Environment=NODE_ENV=production
EnvironmentFile=/etc/app.env
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.targetThen, reload systemd, enable, and start the service:
sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl start myapp.serviceCRITICAL NOTE: The following commands MUST be executed directly on the remote EC2 instance after you have successfully connected via SSH, NOT on your local machine.
Install Caddy:
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
sudo apt update
sudo apt install caddy
sudo systemctl start caddyConfigure Caddy:
sudo vim /etc/caddy/CaddyfileThen, write the following configuration (adjust the domain and proxy port based on the project):
# The Caddyfile is an easy way to configure your Caddy web server.
#
# Unless the file starts with a global options block, the first
# uncommented line is always the address of your site.
#
# To use your own domain name (with automatic HTTPS), first make
# sure your domain's A/AAAA DNS records are properly pointed to
# this machine's public IP, then replace ":80" below with your
# domain name.
# **CRITICAL AI INSTRUCTION:** Automatically replace <your-domain.com> below with the domain name the user requested in Step 8 (or ask them for it).
<your-domain.com> {
# Set this path to your site's directory.
# root * /usr/share/caddy
# Enable the static file server.
# file_server
# Another common task is to set up a reverse proxy:
reverse_proxy localhost:3000
# Or serve a PHP site through php-fpm:
# php_fastcgi localhost:9000
}
# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfileFinally, restart Caddy to apply the changes:
sudo systemctl restart caddyCRITICAL AI INSTRUCTION: If the user has a domain managed by AWS Route 53, you can help them update the DNS record to point to the new EC2 instance's public IP address. This should be executed on the user's local machine using the AWS CLI.
If the user doesn't know their Hosted Zone ID, you can find it using:
aws route53 list-hosted-zones-by-name --dns-name <domain-name>Use the change-resource-record-sets command to create or update (UPSERT) the DNS record. Replace <hosted-zone-id>, <subdomain.domain.com>, and <ec2-public-ip> with the actual values.
aws route53 change-resource-record-sets \
--hosted-zone-id <hosted-zone-id> \
--change-batch '{
"Comment": "Update A record to new EC2 instance",
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "<subdomain.domain.com>",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "<ec2-public-ip>"
}
]
}
}
]
}'Note: DNS propagation may take a few minutes to take effect.
CRITICAL NOTE: The following commands MUST be executed directly on the remote EC2 instance after you have successfully connected via SSH, NOT on your local machine.
To view the application logs managed by systemd, you can use journalctl.
View all logs for the service:
sudo journalctl -u myapp.serviceTail the logs (follow the output in real-time):
sudo journalctl -fu myapp.service