Deploy a Dockerized API to EC2
Automate deployments of a containerized Express API to AWS EC2.
Introduction
30 Second Summary
Pushing code to a repository is only half the journey. For anyone on the internet to actually use your app, it needs to be running on a server that is always on and always reachable.
In this project, you will build an automated deployment pipeline that takes an Express API from your GitHub repository to a live AWS EC2 instance running inside a Docker container.
What You'll Build
A publicly accessible Express API running inside a Docker container on an EC2 instance, with a GitHub Actions pipeline that automatically deploys your code on every push to main.
By the end of this project, you'll have:
- A running EC2 instance with Docker installed, serving your Express API to the public internet.
- A GitHub Actions workflow that builds, pushes, and deploys your container automatically on every push to main.
- Practical experience with Security Groups, SSH key pairs, and secrets management in a CI/CD pipeline.
- Secret Mission: Configure an Nginx reverse proxy so your API serves on port 80 like a production app.
Are there any prerequisites?
This project assumes basic familiarity with Express.js, Docker, and GitHub Actions. Ideally, you have completed one prior CI/CD project that pushed a Docker image to Docker Hub.
Install AWS CLI and Configure Credentials
Your Express API will live on an AWS EC2 instance. To create and manage that instance from your terminal, you need the AWS Command Line Interface (CLI).
The AWS CLI lets you control AWS services with typed commands instead of clicking through the web console every time. In this step, you'll install it and connect it to your AWS account.
In this step, get ready to:
- Install AWS CLI v2 on your local machine.
- Configure the CLI with your AWS account credentials.
- Confirm your account has EC2 access.
Download and install AWS CLI v2
The AWS CLI v2 is a standalone tool you install on your machine. It gives every future step the ability to talk to AWS without opening a browser.
- Download the AWS CLI v2 installer by opening this link in your browser:
- https://awscli.amazonaws.com/AWSCLIV2.msi
- Run the downloaded AWSCLIV2.msi file.
- Follow the installer prompts, accepting the default settings.
- Once the installer finishes, open a new Command Prompt window.
Why a new Command Prompt?
The installer adds the aws command to your system PATH. Any terminal window that was already open won't know about the change until you close and reopen it.
- Verify the installation by running this command:
aws --version
Check your output against these scenarios:
✔️ I see aws-cli/2.x.x
Your output looks something like aws-cli/2.x.x Python/3.x.x Windows/10. The exact numbers after 2. don't matter as long as the major version is 2.
AWS CLI v2 is installed and ready. Move on to the next substep.
ⓧ I see aws-cli/1.x.x
You have an older version installed. You need to uninstall it first, then install v2.
- Press the Windows key to open your search bar.
- Type Add or remove programs and press Enter.
- Find AWS Command Line Interface in the list, click it, and select Uninstall.
- After uninstalling, download and install the v2 MSI from the link above.
- Open a new Command Prompt and run aws --version again to confirm you see aws-cli/2.x.x.
ⓧ Command not found
The terminal cannot find the aws command. This usually means the installer did not complete or you are using a terminal window that was open before installation.
- Close your current terminal and open a brand new Command Prompt window.
- Run aws --version again.
- If it still fails, re-run the AWSCLIV2.msi installer. Make sure you see "Installation successful" before closing it.
- Open another new Command Prompt and try aws --version one more time.
Configure the AWS CLI with your credentials
The CLI needs to know who you are to make requests on behalf of your AWS account. You'll connect it using the Access Key ID and Secret Access Key from your college AWS account.
- Start the configuration wizard by running this command:
aws configure
What does this command do?
This launches an interactive prompt that asks you for four pieces of information, one at a time. It stores your answers locally so every future aws command knows how to authenticate.
- When prompted for AWS Access Key ID, paste your Access Key ID from your college AWS account.
- When prompted for AWS Secret Access Key, paste your Secret Access Key.
- When prompted for Default region name, type us-east-1.
- When prompted for Default output format, type json.
Keep your keys safe
Never share your Access Key ID or Secret Access Key with anyone. Do not paste them into any file you plan to push to GitHub. The aws configure command stores them in a protected file on your local machine only.
- Verify your credentials are working by running this command:
aws sts get-caller-identity
What should I see?
You should see a JSON response with three fields: UserId, Account (your 12-digit AWS account number), and Arn (your user's Amazon Resource Name). This confirms the CLI can authenticate with your AWS account.
Seeing an error instead of JSON?
If you see Unable to locate credentials, run aws configure again and double-check that you pasted your Access Key ID and Secret Access Key without extra spaces.
If you see InvalidClientTokenId, your Access Key ID may be incorrect. Verify it matches what your college account shows.
Help me debug this error
Verify your account has EC2 access
Before building anything on EC2, you want to confirm your college account actually has permission to use it. This command checks which free-tier instance types are available to you.
- Check your EC2 access by running this command:
aws ec2 describe-instance-types --filters Name=free-tier-eligible,Values=true --query "InstanceTypes[*].[InstanceType]" --output text
What does this command do?
This asks AWS to list all EC2 instance types that are eligible for the Free Tier in your configured region. If your account has EC2 access, you'll see a list of instance types like t2.micro or t3.micro.
In the next step, you'll launch one of these instances to host your API.
Seeing an access denied error?
If you see UnauthorizedOperation, your college account may not have EC2 permissions. Contact your instructor or account administrator to confirm your IAM user has the AmazonEC2ReadOnlyAccess policy attached.
Help me troubleshoot this permissions error
Your AWS CLI is installed and authenticated. Next up, you'll use it to launch a live EC2 instance that will host your Express API.
Launch an EC2 Instance
Your API needs a home on the internet. EC2 gives you a virtual server in the cloud where you can install Docker and run your container.
In this step, you will launch an EC2 instance, configure its firewall rules so the outside world can reach your API, and connect to it via SSH.
In this step, get ready to:
- Launch an EC2 instance with Amazon Linux 2023.
- Configure a Security Group with SSH and API access rules.
- Connect to the instance via SSH.
Configure and launch your EC2 instance
The EC2 launch wizard walks you through naming, choosing an operating system, picking a size, and creating a key pair for secure access.
- In the AWS Management Console, use the search bar at the top and type EC2.
- Select EC2 from the results to open the EC2 dashboard.
- Click Launch instance.
What is an EC2 instance?
An EC2 instance is a virtual machine running in AWS's data centers. You choose its operating system, CPU, and memory. It behaves like a remote computer you access over the network.
- Under Name and tags, enter express-api-server in the Name field.
- Under Application and OS Images (Amazon Machine Image), click Quick Start and select Amazon Linux.
- From the Amazon Machine Image (AMI) dropdown, choose Amazon Linux 2023 AMI (marked Free tier eligible).
- Under Instance type, select t2.micro (marked Free tier eligible).
Why t2.micro?
The t2.micro instance type provides 1 vCPU and 1 GiB of RAM. It is included in the AWS Free Tier for 750 hours per month during the first 12 months of your account. That is more than enough to run a single Docker container with a lightweight Express API.
- Under Key pair (login), click Create new key pair.
- Enter express-deploy-key as the key pair name.
- For Key pair type, select RSA.
- For Private key file format, select .pem.
- Click Create key pair.
- Your browser downloads a file called express-deploy-key.pem. Save it somewhere memorable on your machine (your Downloads folder is fine for now).
Keep your .pem file safe
This file is your only way to SSH into the instance. If you lose it, you cannot reconnect. Do not share it or commit it to Git.
Set up network security and launch the instance
A Security Group acts as a firewall for your instance. You need to allow SSH access for yourself and open port 3000 so the public can reach your API.
- Under Network settings, click Edit.
- Leave Create security group selected.
- For the first rule (SSH, port 22), change Source type to My IP. This restricts SSH access to your current IP address only.
- Click Add security group rule.
- Set Type to Custom TCP.
- Set Port range to 3000.
- Set Source type to Anywhere (0.0.0.0/0). This makes your API publicly accessible.
Why two different source rules?
SSH on port 22 is locked to your IP because it gives full shell access to your server. You do not want anyone else logging in.
Port 3000 is open to the world because that is where your public API will listen. Anyone should be able to send it HTTP requests.
- Under Configure storage, leave the default 8 GB gp3 volume. This is included in the Free Tier.
- Review your settings in the Summary panel on the right.
- Click Launch instance.
You should see a success notification. Click the instance ID link in the notification to open the Instances page.
- Wait for the Instance state column to change from Pending to Running. This takes about 30 seconds.
- Once running, select your instance and find the Public IPv4 address in the details panel at the bottom. Copy it somewhere (a sticky note, a text file, your clipboard).
You will need this IP address several times
You will use the Public IPv4 address to SSH in, to test your API in the browser, and to configure GitHub Actions secrets later. Keep it handy.
Connect to your instance via SSH
Now that the instance is running, you can open a secure shell connection to it using the key pair you downloaded.
- Open your terminal (Command Prompt or PowerShell on Windows, Terminal on macOS).
- Connect to your EC2 instance by running this command:
ssh -i [[PEMPATH="path/to/express-deploy-key.pem"]] ec2-user@[[EC2_IP="your EC2 public IPv4 address"]]
What does this command do?
The -i flag tells SSH which private key file to use for authentication. ec2-user is the default username on Amazon Linux 2023 instances. The address after @ is your instance's public IP.
If the connection succeeds, you will see an Amazon Linux welcome banner and a command prompt ending in $.
Seeing a permissions error on Windows?
Windows may show a warning that says the private key file's permissions are too open. Fix this by running these two commands in PowerShell (replace the filename with your actual path):
icacls .\express-deploy-key.pem /inheritance:r
icacls .\express-deploy-key.pem /grant:r "%username%":"(R)"
These commands remove inherited permissions and grant read-only access to your user. Then try the ssh command again.
If you are still stuck, help me fix my SSH permissions error on Windows.
Connection timed out?
Double-check that your Security Group allows SSH (port 22) from your current IP. If your IP has changed since you created the rule (for example, you switched Wi-Fi networks), update the SSH rule in the EC2 console under Security Groups.
Also confirm you are using the correct Public IPv4 address from the instance details panel.
Still stuck? Help me debug my SSH connection to EC2.
Your EC2 instance is live and you have shell access. Next up, you will install Docker on this server so it can run containers.
Install Docker on EC2
Your EC2 instance is up and you're connected over SSH. Now you need a container runtime on this server so it can actually run your Dockerized API.
In this step, you'll install Docker on the instance, configure permissions so you can run containers without sudo, and verify everything works with a test container.
In this step, get ready to:
- Install and start Docker on your EC2 instance.
- Configure group permissions so you can run Docker commands without sudo.
- Verify Docker works by pulling and running a test container.
Update packages and install Docker
Amazon Linux 2023 ships with Docker available in its core package repositories. You'll update the system packages first, then install and start the Docker service.
- In your SSH session on the EC2 instance, update packages and install Docker by running these commands:
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user
What do these commands do?
- sudo yum update -y updates all system packages to their latest versions.
- sudo yum install -y docker installs the Docker engine from Amazon Linux's package repository.
- sudo service docker start starts the Docker daemon so it can manage containers.
- sudo usermod -a -G docker ec2-user adds your user to the docker group. This lets you run Docker commands without typing sudo every time.
- Wait for each command to complete. The update step may take a minute.
Seeing "Permission denied" errors?
Make sure every command starts with sudo. The ec2-user account needs elevated privileges to install system packages and modify groups.
Help me troubleshoot Docker installation errors on my EC2 instance.
Reconnect to apply group permissions
Linux group changes only take effect when you start a new session. You need to log out and log back in for the docker group membership to activate.
- Log out of the SSH session by running:
exit
- Reconnect to your EC2 instance using the same SSH command from earlier:
ssh -i [[PEMPATH="path\to\express-deploy-key.pem"]] ec2-user@[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]
- Verify Docker is working by running:
docker info
- You should see a wall of output showing Docker's configuration details (server version, storage driver, operating system). No errors means Docker is running and your group permissions are active.
Getting "permission denied" on docker info?
This means the group change hasn't taken effect yet. Make sure you fully disconnected from SSH (typed exit) and reconnected. Simply opening a new terminal tab is not enough. You need to close the SSH connection and start a new one.
Help me fix Docker permission denied errors after adding my user to the docker group.
Test Docker with a container
The final check is to pull and run a real container image. This confirms Docker can reach Docker Hub and execute containers on your instance.
- Run the official Docker test image:
docker run hello-world
- You should see Docker pull the image and then print a message starting with "Hello from Docker!" confirming that your installation is working correctly.
What just happened?
Docker pulled the hello-world image from Docker Hub, created a container from it, and ran the binary inside. The output you see confirms three things: Docker can pull images from the internet, it can create containers, and it can execute processes inside them.
Seeing a network error when pulling the image?
Your EC2 instance might not have internet access. Check that the instance is in a subnet with an Internet Gateway attached and that the route table has a 0.0.0.0/0 route pointing to it. If you used the default VPC during launch, this should already be configured.
Help me troubleshoot Docker network connectivity on my EC2 instance.
Docker is installed and confirmed working on your EC2 instance. Next up, you'll build the Express API on your local machine and create a GitHub Actions workflow that deploys it to this server automatically.
Build the Express API and GitHub Actions Workflow
Your EC2 instance is running and Docker is ready to go. Now it is time to write the code that will actually run on that server.
In this step, you will create a minimal Express API, containerize it with a Dockerfile, and write a GitHub Actions workflow that automatically builds, pushes, and deploys the container to your EC2 instance every time you push to the main branch.
In this step, get ready to:
- Create the Express API with health-check endpoints.
- Write a Dockerfile and GitHub Actions workflow for automated deployment.
- Push to GitHub and verify the pipeline deploys to your EC2 instance.
Create the Express API project
Every deployment pipeline needs something to deploy. You will start by creating a simple Express API with two endpoints, then package it in a Docker container.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir express-ec2-deploy
cd express-ec2-deploy
- Initialize a new Node.js project by running:
npm init -y
What does this command do?
The npm init -y command creates a package.json file with default values. This file defines your project's name, version, and dependencies.
- Install Express by running:
npm install express@5.2.1
Now you need to update package.json so it includes the correct start script and project metadata.
- Open package.json in VS Code and replace its entire contents with this:
{
"name": "express-ec2-deploy",
"version": "1.0.0",
"description": "Express API deployed to EC2 via GitHub Actions",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "5.2.1"
}
}
Why replace the file?
The npm init -y command generates extra fields like keywords and a default test script that you do not need. More importantly, you need a start script because your Dockerfile will use npm start to launch the API.
- Create a file called index.js in the project folder and paste the following code:
const express = require("express");
const app = express();
const PORT = 3000;
// Health-check endpoint for monitoring
app.get("/health", (req, res) => {
res.json({
status: "healthy",
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Root endpoint with deployment info
app.get("/", (req, res) => {
res.json({
message: "Express API deployed via GitHub Actions to EC2",
version: "1.0.0"
});
});
// Start the server on port 3000
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
What does this code do?
This file creates a minimal Express server with two endpoints. The /health route returns the server status, current timestamp, and uptime in seconds. The / root route returns a simple JSON message confirming the deployment.
The /health endpoint is what you will use at the end of this step to confirm your entire pipeline is working.
- Create a file called Dockerfile (no extension) in your project folder with this content:
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
What does this Dockerfile do?
The FROM node:20-alpine line uses a lightweight Node.js base image. WORKDIR /app sets the working directory inside the container.
Copying package.json first and running npm install --production before copying the rest of the code lets Docker cache the dependency layer. If your code changes but dependencies stay the same, Docker skips re-installing them.
- Create a .dockerignore file to keep unnecessary files out of the Docker image:
node_modules
.git
.github
- Save all four files.
- Confirm they exist by running:
ls
You should see index.js, package.json, Dockerfile, .dockerignore, and node_modules in the output.
Don't see all the files?
Files starting with a dot (like .dockerignore) are hidden by default. Run ls -a to show them.
If node_modules is missing, make sure you ran npm install express@5.2.1 earlier.
help me troubleshoot missing project files in my express-ec2-deploy folder
✔️ Awesome, I've got everything!
Great. Make sure you have saved all files before continuing.
ⓧ I'd like to double check the full code
Here is each file in full for comparison.
{
"name": "express-ec2-deploy",
"version": "1.0.0",
"description": "Express API deployed to EC2 via GitHub Actions",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "5.2.1"
}
}
const express = require("express");
const app = express();
const PORT = 3000;
app.get("/health", (req, res) => {
res.json({
status: "healthy",
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
app.get("/", (req, res) => {
res.json({
message: "Express API deployed via GitHub Actions to EC2",
version: "1.0.0"
});
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
node_modules
.git
.github
Create the GitHub Actions workflow
The workflow file tells GitHub exactly what to do every time you push code. It will build your Docker image, push it to Docker Hub, then SSH into your EC2 instance and deploy the new container.
- Create the workflow directory structure by running:
mkdir -p .github/workflows
- Create a file called deploy.yml inside .github/workflows/ and paste this workflow:
name: Deploy to EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
- name: Build and push Docker image
run: |
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/express-ec2-api:latest .
docker push ${{ secrets.DOCKERHUB_USERNAME }}/express-ec2-api:latest
- name: Deploy to EC2
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/express-ec2-api:latest
docker stop express-api || true
docker rm express-api || true
docker run -d -p 3000:3000 --name express-api ${{ secrets.DOCKERHUB_USERNAME }}/express-ec2-api:latest
What does this workflow do?
The workflow triggers on every push to the main branch. It runs four steps in sequence:
- Checks out your repository code.
- Logs into Docker Hub using your stored credentials.
- Builds the Docker image from your Dockerfile and pushes it to Docker Hub.
- Uses appleboy/ssh-action@v1 to SSH into your EC2 instance, pull the new image, stop the old container, and start a new one.
The || true after docker stop and docker rm prevents the workflow from failing on the first deployment when no container exists yet.
- Save deploy.yml.
YAML formatting issues?
YAML is very sensitive to indentation. Make sure you are using spaces (not tabs) and that nested items are indented by exactly 2 spaces from their parent.
help me fix YAML indentation errors in my deploy.yml workflow file
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before continuing.
ⓧ I'd like to double check the full code
Here is the complete workflow file:
name: Deploy to EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
- name: Build and push Docker image
run: |
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/express-ec2-api:latest .
docker push ${{ secrets.DOCKERHUB_USERNAME }}/express-ec2-api:latest
- name: Deploy to EC2
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/express-ec2-api:latest
docker stop express-api || true
docker rm express-api || true
docker run -d -p 3000:3000 --name express-api ${{ secrets.DOCKERHUB_USERNAME }}/express-ec2-api:latest
Push to GitHub and add deployment secrets
Your code is ready locally. Now you need to push it to GitHub and give the workflow the credentials it needs to deploy.
- Go to GitHub in your browser and create a new repository. Name it express-ec2-deploy and leave it empty (no README, no .gitignore).
- Back in your terminal, initialize Git and push your code by running these commands:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/express-ec2-deploy.git"]]
git push -u origin main
Where do I find my repository URL?
After creating the repository on GitHub, you will see a page with setup instructions. Copy the HTTPS URL shown at the top. It looks like https://github.com/your-username/express-ec2-deploy.git.
Do not push yet if you see authentication errors
If git push asks for a password, GitHub no longer accepts account passwords for Git operations. You need a Personal Access Token. Go to GitHub Settings, then Developer settings, then Personal access tokens, and generate a token with repo scope. Use the token as your password when prompted.
The workflow will try to run immediately after you push, but it will fail because the secrets are not configured yet. That is expected.
- In your GitHub repository, click **Settings**.
- In the left sidebar under the **Security** section, select **Secrets and variables**, then click **Actions**.
- Click the **Secrets** tab.
- Click **New repository secret**.
You need to add five secrets, one at a time. For each secret, enter the **Name** and **Secret** value, then click **Add secret**.
- Name: DOCKERHUB_USERNAME with your Docker Hub username as the value.
- Name: DOCKERHUB_TOKEN with your Docker Hub access token as the value.
- Name: EC2_HOST with your EC2 instance's Public IPv4 address as the value.
- Name: EC2_USERNAME with ec2-user as the value.
- Name: EC2_SSH_KEY with the entire contents of your express-deploy-key.pem file as the value.
How to copy your .pem file contents
Open the .pem file in a text editor (like Notepad or VS Code) and select all the text, including the -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- lines. Paste the entire content into the Secret field.
What is a Docker Hub access token?
A Docker Hub access token is a credential that lets the workflow push images to your Docker Hub account without using your password. Generate one in Docker Hub under Account Settings, then Security, then New Access Token.
- After adding all five secrets, go to the **Actions** tab in your repository.
- Click the failed workflow run, then click **Re-run all jobs** to trigger it again with the secrets now available.
Wait for the workflow to complete. This typically takes 1-2 minutes.
- Once the workflow shows a green checkmark, open your browser and visit:
http://[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]:3000/health
You should see a JSON response with status set to healthy, a timestamp, and an uptime value.
Workflow failed or can't reach the API?
If the workflow fails at the SSH step, double-check that your EC2_SSH_KEY secret contains the full .pem file content (including the BEGIN and END lines) and that EC2_HOST matches your instance's current public IP.
If the workflow succeeds but you cannot reach the API in your browser, verify that your Security Group allows inbound traffic on port 3000 from 0.0.0.0/0.
help me debug why my GitHub Actions workflow is failing to deploy to EC2
Your API is live and your deployment pipeline is fully automated. Every future push to main will build, push, and deploy a new version with zero manual steps. Next up, you will take this to the next level in the Secret Mission by adding an Nginx reverse proxy.
Secret mission
Add an Nginx Reverse Proxy
Your API works, but it's running on port 3000. Production APIs serve traffic on port 80, the standard HTTP port. In this secret mission, you'll configure Nginx as a reverse proxy on your EC2 instance so users can access your API without specifying a port number.
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. Your EC2 instance is covered under the AWS Free Tier (750 hours/month for 12 months on legacy accounts), but leaving it running will consume those hours.
Cost warning
Your t2.micro instance is free while within the Free Tier's 750 hours/month limit. If your Free Tier has expired or you exceed 750 hours, you will be charged standard EC2 rates.
Stop or terminate the instance when you are not actively using it to avoid unexpected charges.
Resources you used:
- EC2 instance (express-api-server, t2.micro) with attached 8 GB gp3 EBS volume.
- Security Group (SSH port 22, Custom TCP port 3000, HTTP port 80).
- Key Pair (express-deploy-key) and local .pem file.
- GitHub Actions repository secrets (DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, EC2_HOST, EC2_USERNAME, EC2_SSH_KEY).
- Docker Hub image (express-ec2-api).
- Local project folder.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with the pipeline.
- Your instance is free for up to 750 hours/month under the Free Tier. Stop it from the EC2 console when you are not using it to conserve hours.
- Your Docker Hub image, GitHub Actions secrets, and local project files will remain available for future pushes.
Pause - I'll come back to this later
Stop the instance to free up compute hours while keeping all your data intact. You can start it again later from the EC2 console.
- In the EC2 console, select your express-api-server instance.
- Click Instance state, then Stop instance.
- Confirm by clicking Stop.
What happens when you stop?
You are not charged for compute while the instance is stopped. A small EBS storage charge may apply, but the 8 GB gp3 volume is within the Free Tier's 30 GB allowance.
Your public IPv4 address will change when you restart. Update the EC2_HOST GitHub Actions secret with the new IP after restarting.
Delete - I don't want to use this again
Remove all project resources permanently. This cannot be undone.
Terminate the EC2 instance
- Open the EC2 console at https://console.aws.amazon.com/ec2/.
- In the navigation pane, choose Instances.
- Select express-api-server.
- Click Instance state, then Terminate (delete) instance.
- Click Terminate (delete) to confirm.
What about the EBS volume?
The 8 GB gp3 root volume is deleted automatically when you terminate the instance (the default "Delete on Termination" setting is enabled for root volumes attached at launch).
Delete the Security Group
- In the EC2 console navigation pane, choose Security Groups (under Network & Security).
- Select the Security Group you created for this project.
- Click Actions, then Delete security groups.
- Confirm the deletion.
Delete the Key Pair
- In the EC2 console navigation pane, choose Key Pairs (under Network & Security).
- Select express-deploy-key.
- Click Actions, then Delete.
- Confirm the deletion.
Delete the local .pem file
Delete the express-deploy-key.pem file from wherever you saved it on your machine.
Remove GitHub Actions secrets
- In your GitHub repository, go to Settings, then Secrets and variables, then Actions.
- Click the trash icon next to each secret (DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, EC2_HOST, EC2_USERNAME, EC2_SSH_KEY) and confirm each deletion.
Delete the Docker Hub image
- Log into Docker Hub at https://hub.docker.com.
- Navigate to your express-ec2-api repository.
- Click Settings, then scroll down and click Delete Repository.
- Confirm the deletion.
Delete the local project folder
Delete your local project folder from your Desktop by running the appropriate command for your OS:
macOS
rm -rf ~/Desktop/express-ec2-deploy
Windows
rmdir /s /q %USERPROFILE%\Desktop\express-ec2-deploy
That's a Wrap!
That's a Wrap!
Nice work! You just deployed a live, publicly accessible Express API to AWS EC2 with a fully automated CI/CD pipeline.
Every push to your main branch now triggers a build, push, and deploy sequence that lands your code on real cloud infrastructure in under a minute.
You've learned how to:
- Provisioned and configured an AWS EC2 instance from scratch with Security Groups, SSH key pairs, and Docker installed on a remote Linux server.
- Built and containerized an Express API with a production-ready Dockerfile and deployed it as a running container on cloud infrastructure.
- Created a fully automated GitHub Actions CI/CD pipeline that builds, pushes, and deploys to live infrastructure on every push, with secrets managed securely.
- Secret Mission: Configured Nginx as a reverse proxy so the API serves on port 80 like a production app.
Ready to quiz yourself?