Deploy a Dockerized Node.js App on EC2

Launch an EC2 instance and deploy a Dockerized Node.js service to the web.

Introduction

30 Second Summary

Every time you visit a website or use an app, there's a server somewhere running the code that makes it work. When developers want full control over that server, they turn to EC2 instances on AWS.

In this project, you will launch an EC2 instance, configure every setting with intention, deploy a Dockerized Node.js service, troubleshoot a real networking problem, and create professional backups of your setup.

What You'll Build

You'll type your instance's public IP into any browser and see your Node.js service respond live from the cloud, proving that your entire launch configuration, container deployment, and security rules all work together.

By the end of this project, you'll have:

  • A Dockerized Node.js service running on EC2 that responds to HTTP requests from any browser in the world.
  • A custom AMI (golden image) that lets you re-launch your exact configured setup in seconds.
  • Complete understanding of every field on the EC2 launch wizard, so you can explain each choice to a teammate and know when to change the defaults.
  • Secret Mission: Set up a CloudWatch alarm that emails you when CPU utilization spikes above 80%.

Are there any prerequisites?

You'll need an AWS account with console access, an existing SSH key pair (.pem file), and basic familiarity with terminal commands. No additional software installs are required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

You'll launch an EC2 instance with deliberate configuration choices, deploy a Dockerized Node.js service on it, and create backups of your setup. Understanding every launch option now saves you from mystery failures later.

Prepare Your Launch Configuration

The EC2 launch wizard only accepts user data at the moment you create an instance. If you skip it, the only fix is to terminate and start over with a fresh launch. Preparing your script beforehand means Docker installs itself automatically the first time your instance boots.

In this step, you'll create three files on your local machine. A user data script handles Docker installation on boot. A Node.js server and Dockerfile define the container you'll deploy later.

In this step, get ready to:
  • Create a user data script that installs Docker on boot.
  • Write a Node.js server and Dockerfile for your container.
  • Confirm all files are saved and ready for launch.
Understand and create the user data script

EC2 user data is a script that runs once as root on an instance's very first boot. The instance's cloud-init service picks up whatever you paste into the user data field and executes it automatically.

What is EC2 user data?

Think of it as a setup checklist you hand to a brand-new server. The server reads the list the moment it wakes up and installs everything before you even SSH in.

You'll paste your user data script into the Advanced details section of the EC2 launch wizard in the next step.

  • Press the Windows key, type Windows Terminal, and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir ec2-docker-project
cd ec2-docker-project
  • Confirm the folder was created by running:
ls

You should see an empty directory listing, confirming you're inside ec2-docker-project.

  • Create user-data.sh by running this command:
@'
#!/bin/bash
yum update -y
yum install -y docker
systemctl enable --now docker
usermod -a -G docker ec2-user
'@ | Set-Content -Path user-data.sh

What does this script do?

  • yum update -y updates all system packages to their latest versions.
  • yum install -y docker installs Docker from the Amazon Linux 2023 package repository.
  • systemctl enable --now docker starts the Docker service immediately and ensures it starts on every future reboot.
  • usermod -a -G docker ec2-user adds the default user to the Docker group so you can run Docker commands without sudo.
  • Confirm the file was created by running:
ls

You should see user-data.sh listed in the output.

Don't see user-data.sh?

  • Make sure the opening @' is on its own line with nothing after it.
  • The closing '@ must also be on its own line at the very start, with no spaces before it.
  • Confirm you're inside the ec2-docker-project folder by running pwd.

Still stuck? Help me create a file using PowerShell here-strings.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file and move on to the next chunk.

ⓧ I'd like to double check the full code

Your user-data.sh should contain exactly this:

#!/bin/bash
yum update -y
yum install -y docker
systemctl enable --now docker
usermod -a -G docker ec2-user
Create the Node.js app and Dockerfile

Your container needs two files: a Node.js server that responds to HTTP requests and a Dockerfile that packages it into an image. You'll transfer these to your EC2 instance later.

  • Create server.js by running this command:
@'
const http = require('http');

const hostname = '0.0.0.0';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello from Docker on EC2!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
'@ | Set-Content -Path server.js

What does this server do?

  • It creates an HTTP server using Node.js built-in http module.
  • The server listens on port 3000 and responds to every request with the text Hello from Docker on EC2!.
  • Binding to 0.0.0.0 means the server accepts connections from any network interface, not just localhost.
  • Confirm server.js was created by running:
ls

You should see both user-data.sh and server.js listed.

Missing server.js?

  • PowerShell here-strings require the opening @' on its own line with nothing following it.
  • If you see a red error about unexpected tokens, paste the entire block at once rather than line by line.

Need a hand? Help me fix my PowerShell file creation command.

✔️ Awesome, I've got everything!

Great. One more file to go.

ⓧ I'd like to double check the full code

Your server.js should contain exactly this:

const http = require('http');

const hostname = '0.0.0.0';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello from Docker on EC2!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
  • Create the Dockerfile by running this command:
@'
FROM node:20-alpine
WORKDIR /app
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]
'@ | Set-Content -Path Dockerfile

What does this Dockerfile do?

  • FROM node:20-alpine starts from a lightweight Node.js 20 image based on Alpine Linux.
  • WORKDIR /app sets the working directory inside the container.
  • COPY server.js . copies your server file into the container.
  • EXPOSE 3000 documents that the container listens on port 3000.
  • CMD ["node", "server.js"] tells Docker to start the Node.js server when the container launches.
  • Confirm all three files exist by running:
ls

You should see Dockerfile, server.js, and user-data.sh listed.

Missing the Dockerfile?

  • Make sure you typed Dockerfile with a capital D and no file extension.
  • Check that the closing '@ is at the very start of its line with no leading spaces.

Still stuck? Help me troubleshoot creating my Dockerfile in PowerShell.

✔️ Awesome, I've got everything!

All three files are in place. You're ready for the final check.

ⓧ I'd like to double check the full code

Your Dockerfile should contain exactly this:

FROM node:20-alpine
WORKDIR /app
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]
Verify your user data script is ready

Before you move on, confirm the script that will automate your instance setup is correct. You'll paste this content into the launch wizard's Advanced details section in the next step.

  • Before you run the next command, think: what four commands do you expect to see inside the script?
  • Display the contents of user-data.sh by running:
cat user-data.sh

You should see the shebang line #!/bin/bash followed by yum update -y, yum install -y docker, systemctl enable --now docker, and usermod -a -G docker ec2-user.

Where does this script go?

When you launch your EC2 instance in the next step, you'll expand the Advanced details section at the bottom of the launch wizard. There's a User data text box where you'll paste this script.

EC2 runs it exactly once, as root, on the instance's first boot. By the time you SSH in, Docker will already be installed and running.

Your launch files are locked and loaded. Next up, you'll walk through every section of the EC2 launch wizard and make deliberate choices for each field.

Launch the Instance

Your user data script, Node.js server, and Dockerfile are saved locally and ready to go. Now it's time to launch the EC2 instance that will run your container.

The EC2 launch wizard has seven sections, each controlling a different aspect of your instance's behavior. You're going to configure every section with intention instead of blindly accepting defaults. By the end, you'll understand what each field controls and why it matters for your deployment.

In this step, get ready to:
  • Configure every section of the EC2 launch wizard with deliberate settings.
  • Launch the instance and verify it reaches a running state.
  • Confirm your instance has a public IPv4 address assigned.
Open the EC2 launch wizard
  • Sign into the AWS Management Console.
  • In the search bar at the top, type EC2 and select EC2 from the results.
  • Click Launch instance (the orange button near the top of the dashboard).

The launch wizard opens with all seven configuration sections visible on one page.

  • In the Name and tags section, enter a descriptive name like nextwork-docker-instance.
  • In the Application and OS Images (AMI) section, select Amazon Linux 2023.
  • Confirm the architecture shows 64-bit (x86) and the badge says Free tier eligible.

What is an AMI?

An AMI (Amazon Machine Image) is a template that contains the operating system and pre-installed software for your instance. Amazon Linux 2023 is AWS's own Linux distribution, optimized for EC2 and compatible with your user data script's yum commands.

Select your instance type and key pair
  • In the Instance type section, select t3.micro.
  • Confirm the Free tier eligible label appears next to it.
  • In the Key pair (login) section, select your existing key pair from the dropdown.

What does t3.micro give you?

An instance type defines how much CPU, memory, and network bandwidth your instance gets. t3.micro provides 2 vCPUs and 1 GiB of memory. That's more than enough to run a single Docker container serving HTTP responses.

Configure network settings

The network section controls how your instance connects to the internet and who can reach it.

  • In the Network settings section, click Edit.
  • Keep the VPC set to the default VPC.
  • Keep Subnet set to the default subnet.
  • Set Auto-assign public IP to Enable.
  • Under Firewall (security groups), select Create security group.
  • Keep the default SSH rule for Port range 22.
  • Change the Source type to My IP.

Why restrict SSH to your IP?

Setting the source to My IP means only your current network can SSH into the instance. If you left it at 0.0.0.0/0, anyone on the internet could attempt to connect. The security group currently allows SSH access only. You'll adjust firewall rules as needed in later steps.

Configure storage volumes

Storage volumes are the virtual hard drives attached to your instance. You'll keep the default root volume and add a second data volume.

  • In the Configure storage section, confirm the root volume is set to 8 GiB with type gp3.
  • Click Add new volume.
  • Set the new volume's size to 4 GiB and keep the type as gp3.

What is gp3?

gp3 is the current-generation general purpose EBS volume type. It provides 3,000 baseline IOPS and 125 MiB/s throughput at any volume size, without needing burst credits.

Compared to the older gp2 type, gp3 is up to 20% lower in price per GB and decouples performance from volume size. This means a small 8 GiB volume gets the same IOPS as a 500 GiB one.

Set up advanced details

The Advanced details section contains settings that control instance behavior and metadata. This is also where you paste your user data script.

  • Expand the Advanced details section by clicking on it.
  • Set Termination protection to Enable.
  • Set Shutdown behavior to Stop.
  • Under Metadata version, select V2 only (token required).
  • Keep Tenancy set to Shared.
  • Scroll down to the User data field at the bottom of this section.
  • Open your local user-data.sh file, copy the entire contents, and paste them into the User data text box.

What do these advanced settings control?

  • Termination protection prevents accidental deletion of your instance from the console.
  • Shutdown behavior set to Stop means an OS-level shutdown stops the instance (preserving data) instead of terminating it.
  • IMDSv2 (token required) adds a security layer to the instance metadata service, preventing certain types of server-side request forgery attacks.
  • User data runs your script as root on first boot via cloud-init. This is how Docker gets installed automatically without you needing to SSH in and do it manually.
Review and launch your instance
  • Look at the Summary panel on the right side of the page.
  • Confirm it shows your instance type (t3.micro), your key pair name, and 2 storage volumes.
  • Click Launch instance.

A success banner appears at the top of the page with a link to your new instance ID.

  • Click the instance ID link in the success banner (it starts with i-).

Before you look at the instance details, what state do you think the instance is in right now? Do you think it's ready immediately, or does it take time to boot?

  • Watch the Instance state column.
  • Wait for the state to change from Pending to Running.
  • Wait for Status check to show 2/2 checks passed (this can take 2-3 minutes).
  • Click the refresh icon if the status hasn't updated.

Once status checks pass, your instance is fully booted and your user data script has run. Docker is now installed on the instance.

  • In the instance details below, find the Public IPv4 address field and confirm an IP address is assigned.

Note your public IP

Copy your instance's public IPv4 address and save it somewhere handy. You'll use this IP to SSH into the instance and to access your service from a browser in upcoming steps.

Instance stuck in Pending?

Status checks can take 2-5 minutes to complete. Click the refresh icon next to the instance list to poll for updates. If the instance stays in Pending for more than 5 minutes, check that your account's Free Tier limits haven't been exceeded in the Billing dashboard.

help me figure out why my EC2 instance is stuck in Pending state

Your instance is live and Docker is installed. Next up, you'll SSH in, build your container image, and get your Node.js service running inside it.

Connect and Deploy Your Container

Your instance is running and passed its health checks. Now it's time to verify that your user data script did its job and get your containerized app up and running.

In this step, you'll SSH into the instance, confirm Docker is ready, create your app files directly on the server, and launch your container.

In this step, get ready to:
  • Verify Docker was installed automatically by your user data script.
  • Create server.js and Dockerfile on the instance.
  • Build and run your Docker container.
SSH into your instance

SSH is a secure protocol that lets you control a remote server from your local terminal. Your instance is already configured to accept SSH connections on port 22 from your IP address.

  • Open Windows Terminal by pressing the Windows key, typing Terminal, and pressing Enter.
  • Connect to your instance by running this command:
ssh -i [[PEMPATH="path to your .pem file"]] ec2-user@[[EC2="your instance public IPv4 address"]]

What does this command do?

  • ssh starts a secure shell connection to a remote machine.
  • -i specifies the private key file that proves your identity to the instance.
  • ec2-user is the default username on Amazon Linux 2023 instances.

Where do I find my public IPv4 address?

In the EC2 console, click on your instance ID. The Public IPv4 address is displayed in the instance summary at the top of the details page. Copy it from there.

  • When prompted with a host key verification message, type yes and press Enter to accept the connection.

You should see a welcome banner from Amazon Linux and a command prompt ending in $. You're now logged into your EC2 instance.

  • Verify Docker was installed by your user data script by running:
docker --version

You should see a version number printed, confirming Docker is installed.

  • Confirm the Docker service is actively running:
systemctl status docker

Look for active (running) in the output. This confirms your user data script successfully installed and started Docker on first boot.

Docker not found or not running?

  • If you see a "command not found" error, your user data script may not have finished. Wait 1-2 minutes and try again. User data runs on first boot and may still be executing.
  • If the service shows "inactive", start it manually with sudo systemctl start docker.
  • If SSH itself times out, double check that your security group allows inbound SSH (port 22) from your current IP address.

Still having trouble connecting?

Create your app files and build the container

Your instance has Docker ready, but no application to run yet. You'll create the Node.js server and Dockerfile directly on the instance using heredoc commands.

  • Create server.js by running this command:
cat << 'EOF' > server.js
const http = require('http');

const hostname = '0.0.0.0';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello from Docker on EC2!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
EOF

What does this server do?

This is a minimal Node.js HTTP server. It listens on port 3000 and responds to every request with the text "Hello from Docker on EC2!". The hostname 0.0.0.0 means it accepts connections from any network interface, not just localhost.

  • Create the Dockerfile by running this command:
cat << 'EOF' > Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]
EOF

What does this Dockerfile do?

  • FROM node:20-alpine starts from a lightweight Node.js 20 base image.
  • WORKDIR /app sets the working directory inside the container.
  • COPY server.js . copies your server file into the container.
  • EXPOSE 3000 documents that the container listens on port 3000.
  • CMD ["node", "server.js"] tells Docker what command to run when the container starts.
  • Confirm both files were created by running:
ls

You should see server.js and Dockerfile listed in the output.

Files not appearing?

Make sure you copied the entire heredoc command including the EOF line at the end. If EOF is missing, your terminal is still waiting for input (you'll see a > prompt). Type EOF on its own line and press Enter to finish.

Need help with the file creation?

  • Build your Docker image by running:
docker build -t my-node-app .

You'll see Docker download the Node.js base image and build your custom image layer by layer. This takes about 30 seconds on a t3.micro instance.

What does the -t flag do?

The -t my-node-app flag tags (names) your image so you can reference it later. The . at the end tells Docker to look for the Dockerfile in the current directory.

  • Once the build finishes, start your container by running:
docker run -d -p 3000:3000 --name node-service my-node-app

What do these flags mean?

  • -d runs the container in detached mode (in the background).
  • -p 3000:3000 maps port 3000 on the host to port 3000 inside the container.
  • --name node-service gives the container a memorable name instead of a random one.
  • Verify your container is running:
docker ps

You should see a table with one row showing your node-service container with status "Up" and port mapping 0.0.0.0:3000->3000/tcp.

Container not showing in docker ps?

  • Run docker ps -a to see all containers including stopped ones. If your container exited immediately, check the logs with docker logs node-service.
  • If you see a "permission denied" error, try sudo docker ps. If that works, log out and SSH back in so your docker group membership takes effect.

Container still not starting?

Test your service locally on the instance

Your container is running, but you haven't confirmed the Node.js server is actually responding to requests. A quick local test proves the application layer works before you try accessing it from outside.

Before you run the next command, what do you think you'll see? Will the server respond with text, an error, or nothing at all?

  • Test your service from within the instance by running:
curl localhost:3000

You should see:

Hello from Docker on EC2!

Your containerized Node.js service is serving HTTP traffic on the instance. The full deployment chain worked: user data installed Docker, you built an image from your Dockerfile, and the container is responding to requests.

Seeing "Connection refused"?

  • Run docker ps to confirm the container is still running. If it crashed, check docker logs node-service for errors.
  • Make sure the port mapping is 3000:3000. If you accidentally mapped a different port, stop the container with docker rm -f node-service and rerun the docker run command.

Service not responding?

Your container is serving traffic locally on the instance. Next up, you'll try to access it from your browser and discover a real networking problem that every cloud developer hits.

Fix the Networking Problem

Your container is running and responding to local requests. You confirmed it with curl localhost:3000 from inside the instance.

Now for the real test. Can anyone else on the internet actually reach your service? You set up the security group with only an SSH rule on port 22. Your Node.js container listens on port 3000. That means the firewall is blocking every external request to your app.

In this step, get ready to:
  • Experience the networking timeout when trying to reach your service from a browser.
  • Add an inbound rule to open port 3000 in your security group.
  • Verify public access to your Dockerized service from any browser.
Experience the failure

Your container responds perfectly when you curl it from inside the instance. But does that mean the outside world can reach it?

Before you try, make a prediction. Do you think your browser will be able to load the page, or will it fail? Why?

  • Open any browser on your local machine (not the SSH session).
  • In the address bar, type the following URL using your instance's public IP:
http://[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]:3000
  • Press Enter and wait.

The page hangs and eventually times out. No response at all.

Why did it time out?

Your EC2 instance's security group acts as a virtual firewall. Right now it has only one inbound rule: SSH on port 22. There is no rule allowing traffic on port 3000.

Without an inbound rule, the security group silently drops the request. Your browser never gets a response, so it times out. The container is running fine. The network is blocking you.

Why did we set it up this way?

This is a real-world scenario. In production, services often work perfectly on the instance but are unreachable from outside because of missing firewall rules. Diagnosing this gap between local success and external failure is a core skill for anyone deploying services on EC2.

Add an inbound rule for port 3000

The fix is straightforward. You need to tell the security group to allow inbound TCP traffic on port 3000.

  • Switch back to the AWS Management Console in your browser.
  • In the navigation pane on the left, click Instances.
  • Select your running instance by clicking its row.
  • Click the Security tab in the details panel below.
  • Under Security groups, click the security group link (it starts with sg-).

This opens your security group's details page.

  • Click the Inbound rules tab.
  • Click Edit inbound rules.
  • Click Add rule.
  • For Type, select Custom TCP.
  • For Port range, enter 3000.
  • For Source, select Anywhere-IPv4 (0.0.0.0/0). This allows traffic from any IP address.
  • Click Save rules.

What does 0.0.0.0/0 mean?

The CIDR block 0.0.0.0/0 means "any IPv4 address on the internet." For a public demo service this is fine. In production, you might restrict the source to your own IP or a load balancer's security group for tighter control.

Use My IP for tighter security

If you prefer, select My IP in the Source dropdown instead of Anywhere-IPv4. This restricts access to only your current IP address.

Verify public access from your browser

The security group change takes effect immediately. Time to test.

Before you refresh, predict what you expect to see this time. Will it still time out, or will you get a response?

  • Return to the browser tab where your request timed out earlier.
  • Refresh the page (or re-enter the same URL):
http://[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]:3000

You now see Hello from Docker on EC2! displayed in your browser.

You just diagnosed and fixed a real production networking issue. The container was always working. The firewall was the blocker.

Still seeing a timeout?

  • Double-check that the new inbound rule shows Custom TCP with port 3000 and source 0.0.0.0/0 in the Inbound rules tab. If the rule is missing, click Edit inbound rules and add it again.
  • Confirm your container is still running by SSHing back in and running docker ps. If the container stopped, restart it with docker start node-service.
  • Make sure you are using http:// (not https://) in the URL. Your container does not serve HTTPS.

Help me troubleshoot why my EC2 instance is not responding on port 3000 even after adding a security group rule.

Your service is live on the public internet. Anyone with the URL can see your response. Next up, you'll create backups of this working setup so you can recreate it anytime.

Create Snapshots and a Golden AMI

Your Node.js service is live and reachable from the internet. That's a working deployment.

But if this instance crashes or gets terminated, everything disappears. Your Docker image, your app files, your configuration. Professionals protect against this with EBS snapshots and custom AMIs. You also have a second EBS volume sitting idle that needs to be formatted and mounted before it can store any data.

In this step, get ready to:
  • Mount and format the second EBS data volume.
  • Create an EBS snapshot of the root volume as a backup.
  • Build a custom AMI (golden image) that captures your entire setup.
Mount the data volume

When you launched your instance in Step 2, you added a second 4 GiB gp3 volume. It's attached to the instance, but it's like a brand new hard drive. It has no file system and no mount point. You need to format it and mount it before Linux can write files to it.

  • Return to your SSH session with the instance. If your session closed, reconnect by running:
ssh -i [[PEMPATH="path to your .pem file"]] ec2-user@[[EC2_PUBLIC_IP="your instance public IPv4 address"]]
  • List all attached block devices by running:
lsblk

You should see two disks. One is your 8 GiB root volume (with partitions and mount points). The other is your 4 GiB data volume with no mount point listed.

What am I looking for in lsblk?

On Nitro-based instances like t3.micro running Amazon Linux 2023, EBS volumes appear as NVMe devices. Your root volume is typically nvme0n1 and your data volume is typically nvme1n1. Look for the 4 GiB disk with no MOUNTPOINTS entry.

Note the full device path (e.g. /dev/nvme1n1). You'll use it in the next commands.

  • Format the data volume with the XFS file system by running this command (replace the device path with the one you identified from lsblk):
sudo mkfs -t xfs [[DEVICE="/dev/nvme1n1 or your device name from lsblk"]]
  • Create a mount point directory by running:
sudo mkdir /data
  • Mount the formatted volume to /data by running:
sudo mount [[DEVICE="/dev/nvme1n1 or your device name from lsblk"]] /data
  • Verify the volume is mounted by running:
df -h

You should see your device mounted at /data with approximately 4.0 GiB of available space listed.

Don't see /data in the output?

Make sure you used the correct device path from your lsblk output. If the format command failed, double-check the device has no existing mount point or partitions.

If you see a "wrong fs type" error during mount, re-run the mkfs command to ensure the volume was formatted correctly.

help me mount my EBS data volume on EC2

Create an EBS snapshot

A snapshot is a point-in-time backup of an EBS volume. If your root volume ever corrupts or you accidentally break your instance, you can restore from this snapshot.

  • Switch to your browser and open the EC2 console.
  • In the left sidebar, expand Elastic Block Store and click Snapshots.
  • Click Create snapshot.
  • For Resource type, select Volume.
  • In the Volume ID dropdown, select your 8 GiB root volume. You can identify it by the size shown next to the volume ID.
  • In the Description field, enter Root volume snapshot - node docker instance.
  • Click Add tag. Set the Key to Name and the Value to node-docker-root-snap.
  • Click Create snapshot.

You'll see your snapshot appear in the list. Watch the Status column. It starts as pending and transitions to completed within a minute or two.

What is happening during the snapshot?

AWS is copying the data from your EBS volume to S3 storage behind the scenes. The snapshot is incremental. Only the blocks that changed since the last snapshot are copied. Your first snapshot captures the full volume.

Snapshot stuck in pending?

Snapshots of small volumes (8 GiB) typically complete in under two minutes. If yours stays in pending for more than five minutes, click the refresh icon in the top-right of the Snapshots table.

help me with my EBS snapshot stuck in pending state

Create a custom AMI (golden image)

An AMI packages your entire instance. The operating system, Docker, your running container setup, both EBS volumes. You can launch brand new instances from this AMI and get an exact replica of your current setup in seconds.

  • In the left sidebar of the EC2 console, click Instances.
  • Select the checkbox next to your running instance.
  • Click Actions in the top menu bar.
  • Select Image and templates, then click Create image.
  • In the Image name field, enter my-node-docker-golden.
  • Leave all other settings at their defaults. Click Create image.

What does Create image do?

AWS takes a snapshot of every volume attached to the instance and bundles them into a single launchable AMI. Next time you want this exact setup, you choose this AMI from the My AMIs tab in the launch wizard instead of starting from scratch.

Before you check the AMI status, what do you expect to see in the AMI list? What status should it start with, and what should it end with?

  • In the left sidebar, expand Images and click AMIs.
  • Find your my-node-docker-golden AMI in the list. Its status starts as pending and changes to available once the image is ready.

The AMI creation takes a few minutes. Once the status shows available, your golden image is ready. You can now launch identical instances from this AMI anytime.

  • Confirm both backups are in place. Navigate to Elastic Block Store > Snapshots in the sidebar. You should see your snapshot with status completed.
  • Navigate to Images > AMIs. You should see my-node-docker-golden with status available.

AMI still showing pending after 5 minutes?

AMI creation involves snapshotting all attached volumes. With two volumes (8 GiB + 4 GiB), it can take up to 5 minutes. Click the refresh icon in the AMIs table periodically.

If it stays in pending for more than 10 minutes, check that the instance is still in the running state.

help me with my AMI creation on EC2

Your instance is now fully backed up. You have a snapshot for disaster recovery and a golden AMI that lets you spin up clones of this exact setup in seconds. Next up, put your monitoring skills to the test in the Secret Mission.

Secret mission

Set Up a CloudWatch CPU Alarm

Your instance is running perfectly, but what happens when traffic spikes and CPU usage goes through the roof? In production, you need to know before your users do. Set up a CloudWatch alarm that emails you when CPU utilization crosses 80%.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses AWS Free Tier eligible resources, but charges can apply if your Free Tier has expired or you exceed the monthly allowances.

Cost warning

Your EC2 instance (t3.micro) and EBS volumes (12 GiB total gp3) are Free Tier eligible, but only while your account is within Free Tier limits. EBS snapshots cost approximately $0.05/GB-month, and your custom AMI is stored as EBS snapshots at the same rate.

If you leave everything running after Free Tier expires, expect charges for compute, storage, and snapshots. Consider pausing or deleting to avoid surprises.

Resources you used:

  • EC2 instance (t3.micro, running).
  • EBS root volume (8 GiB gp3) and data volume (4 GiB gp3).
  • EBS snapshot of the root volume.
  • Custom AMI (my-node-docker-golden).
  • Security group with SSH and port 3000 rules.
  • CloudWatch alarm (Secret Mission).
  • SNS topic and email subscription (Secret Mission).

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment further with your instance.

  • Your instance continues to use Free Tier hours. Monitor usage in the AWS Billing dashboard.
  • EBS volumes, snapshots, and the AMI remain and count toward storage limits.
  • The CloudWatch alarm and SNS topic stay active within Free Tier allowances (10 alarms, 1,000 email notifications per month).

Pause - I'll come back to this later

Stop the instance to eliminate compute charges while keeping your files and configuration intact.

  • In the EC2 console, select your instance, choose Instance state, then Stop instance.
  • EBS volume charges continue while stopped (approximately $0.08/GB-month for gp3). Your 12 GiB total costs about $0.96/month.
  • Your public IP address will change when you start the instance again. Use the new IP to SSH in next time.
  • The CloudWatch alarm will enter an insufficient-data state while the instance is stopped. It resumes monitoring when you restart.

Delete - I don't want to use this again

Remove all project resources completely. Follow these steps in order.

  • Disable termination protection first. Select your instance, choose Actions, then Instance settings, then Change termination protection. Clear the checkbox and click Save.
  • Terminate the instance. Select your instance, choose Instance state, then Terminate (delete) instance. Confirm the termination. This deletes the root volume if Delete on termination is enabled (the default).
  • Delete the data volume if it was not auto-deleted. Go to EC2, then Elastic Block Store, then Volumes. If the 4 GiB volume still shows as available, select it, choose Actions, then Delete volume.
  • Deregister the AMI. Go to EC2, then Images, then AMIs in the navigation pane. Select your my-node-docker-golden AMI, then choose Actions, then Deregister AMI. When prompted, also delete the associated snapshots that backed the AMI.
  • Delete the standalone EBS snapshot. Go to EC2, then Elastic Block Store, then Snapshots. Select the snapshot you created manually (not the ones from the AMI). Choose Actions, then Delete snapshot.
  • Delete the security group. Go to EC2, then Security Groups in the navigation pane. Select the security group you created for this project, choose Actions, then Delete security groups. You may need to wait a few minutes after instance termination before this succeeds.
  • Delete the CloudWatch alarm (Secret Mission). Open the CloudWatch console. In the navigation pane, choose Alarms, then All alarms. Select the checkbox next to your CPU alarm, choose Actions, then Delete.
  • Delete the SNS topic (Secret Mission). Open the Amazon SNS console. In the navigation pane, choose Topics. Select your topic, then choose Delete. Confirm the deletion when prompted.

That's a Wrap!

That's a Wrap!

Nice work! You just launched a fully configured EC2 instance, deployed a Dockerized Node.js service on it, and made it accessible from the public internet.

Here's what you learned:

  • Configured every section of the EC2 launch wizard with deliberate choices for AMI, instance type, key pair, network, security group, storage, and advanced details.
  • Deployed a Dockerized Node.js service accessible from any browser, and diagnosed a real security group networking problem where your container worked locally but was unreachable from outside.
  • Mounted a second EBS data volume, created a snapshot, and built a custom AMI as a golden image you can re-launch anytime.
  • Set up CloudWatch monitoring with an SNS email alarm that notifies you when CPU utilization spikes above 80% (Secret Mission).

Ready to quiz yourself?