Lesson 7 · 20 min read

EC2: Launch Your First Server and Deploy Next.js Manually

SSH in, install Node, clone, .env, build, and keep it alive with PM2 — the manual deploy every later lesson automates.

What you'll learn in this lesson

  1. Concept: a computer you rent by the second
  2. Why do it by hand first
  3. The seven words you must know
  4. Instance types and picking a size
  5. Launch the server
  6. Connect with SSH
  7. Prepare the server
  8. Deploy Next.js by hand
  9. Keep it alive with PM2
  10. Prove the IAM role works
  11. Deploying a new version
  12. Disks, stop vs terminate, and the bill
  13. Troubleshooting table
  14. Interview corner
  15. Practice task before Lesson 8
  16. Conclusion

Concept

EC2 (Elastic Compute Cloud) is an AWS service that gives you a computer to rent, billed by the second. It is a real Linux machine with a CPU, memory and a disk, in an AWS data centre. You get a login and full control. You can install anything, run anything and break anything. One rented machine is called an instance.

This is the moment the whole course has been leading to. Lesson 1 taught you Linux. Lesson 2 taught you the network. Lesson 5 taught you identity, and Lesson 6 built the room where this server will live. Now you put the first server in that room and run your Next.js app on a machine you control.

What Vercel did that you now do

On Vercel, git push built your app, started it, gave it HTTPS, restarted it after a crash and scaled it. On EC2 you do each of these things yourself, one at a time. That is why we do it by hand first. Lessons 9, 14 and 15 then automate steps that you already understand.

Why do it by hand first

Real teams do not log in to servers with SSH to deploy. They use pipelines. A pipeline (CI/CD, short for continuous integration and continuous delivery) is a set of steps that builds, tests and deploys your code automatically. So why learn the manual way?

  • You cannot automate what you do not understand. A pipeline is just this lesson's commands written in a YAML file (a simple text format for settings). When the pipeline breaks at 2 a.m., you log in with SSH and run the steps yourself.
  • Every production problem ends at a server. You read logs, check a process and find a full disk. This lesson teaches those skills.
  • It shows what Vercel hid. Environment variables live in a file. A program must watch your process and restart it. Memory is limited. Disks fill up.

The seven words you must know

WordWhat it isOurs
AMIAmazon Machine Image: a template that a server is created from. It holds the operating system and any pre-installed software.Ubuntu Server 24.04 LTS
Instance typeThe hardware size of the server: how many virtual CPUs (vCPU) and how much memory (RAM).t3.small = 2 vCPU, 2 GB
EBS volumeElastic Block Store volume: the server’s hard disk. It is a separate network disk, so it survives when you stop the server.20 GB gp3
Key pairTwo linked keys used to log in without a password. AWS keeps the public key on the server. You keep the private key in a .pem file.myapp-key.pem
Security GroupThe firewall you built in Lesson 6. It lists which network traffic may reach the server.web-sg
Instance profileA holder that lets a server wear an IAM role (Lesson 5). The console makes one for you when you pick a role.myapp-ec2-role
Elastic IPA fixed public IP address that stays yours when the server stops and starts.13.233.x.x

Instance types and picking a size

An instance type name is a code. In t3.small, t is the family (general purpose, burstable), 3 is the generation and small is the size. A g in the family (t4g, m7g) means an ARM “Graviton” CPU. ARM is a different CPU design. It is cheaper for the same power, but your software must be built for ARM.

TypevCPU / RAMPer month (approx.)Verdict
t3.micro2 / 1 GB~$8Fine to run a small app. Too small to run next build, because it runs out of memory.
t3.small2 / 2 GB~$16Our choice for this course. It builds a normal Next.js app without trouble (with swap).
t3.medium2 / 4 GB~$32Comfortable for a build, the app and a few background jobs.
t4g.small2 / 2 GB~$13Same size on ARM (Graviton). About 20% cheaper, but it needs an arm64 image.
c7i.large2 / 4 GB~$65Compute-optimised: steady, high CPU use (video, heavy APIs).

Prices are on-demand (pay as you go) for Mumbai, running all month. Always check the current pricing page.

What “burstable” means

t instances earn CPU credits while they are idle and spend them when they are busy, like a mobile data plan with rollover. A small web app that idles at 5% CPU is a good fit. If a server runs at 100% for hours, it uses up its credits. T3 instances start in “unlimited” mode by default. They do not slow down, but AWS charges you extra for the surplus credits. (In “standard” mode, they slow down instead.) For constant heavy load, choose the m or c families.

Launch the server

Go to EC2 → Launch instance. The form is long, but only a few fields matter. Leave everything else at its default.

FieldSet it toWhy
Image (AMI)Ubuntu Server 24.04 LTSLTS means long-term support: it gets security fixes for years. Every command in this course assumes it
Instance typet3.small2 GB RAM — enough for next build with swap
Key pairCreate new, ED25519, .pemYour way in over SSH (secure remote login). AWS shows the private key only once
Networkmyapp VPC, a public subnet, web-sgThe network from Lesson 6, so it gets a public IP
Storage20 GB gp3, encryptedEncryption at rest (data is scrambled on the disk) costs nothing and does not slow the server in a way you will notice
Advanced → IAM instance profilemyapp-ec2-roleThe role from Lesson 5 — AWS access with no keys on disk
Advanced → Metadata versionV2 only (token required)IMDSv2 (the safer version of the instance metadata service, explained below) blocks a whole group of attacks that steal the server's credentials
Lose the key and you are locked out

The .pem file downloads once, and AWS never shows it again. Move it to ~/.ssh/ and set its permissions to 400 (only you can read it; see Lesson 1. SSH refuses keys that other users can read). Also back it up in a password manager.

Then give the server a permanent address: EC2 → Elastic IPs → Allocate → Associate with your instance. Without it, the public IP changes every time the server stops and starts, which breaks your DNS record (the entry that links your domain name to the IP). Every public IPv4 address costs $0.005 per hour, whether it is attached or not. So release the Elastic IP when you delete the server.

Connect with SSH

Log in
  • ssh -i ~/.ssh/myapp-key.pem ubuntu@SERVER_IP

    The first time, it asks you to trust the server fingerprint (the server's ID). Type yes. The user name on Ubuntu images is always ubuntu

Tip: add a Host myapp entry to ~/.ssh/config on your laptop. Put the IP, user and key path in it. From then on, ssh myapp is enough.

Prepare the server

A fresh server has nothing on it but Linux. Do these once, in order:

  1. Update the system

    Install the security fixes released since the image was built. Also install git and the build tools that some npm packages need. If the kernel (the core of the operating system) is updated, you may need to reboot.

  2. Add a swap file — the safety net for small servers

    Swap is a part of the disk that the system uses as emergency memory when RAM is full. It is slow, but it turns “process killed” into “build took a bit longer”. next build is the step that uses the most memory in the life of your app. A 2 GB swap file is enough on a t3.small.

  3. Install Node.js

    Install Node 22 LTS from NodeSource (a package source for Node) and enable corepack. Corepack is a Node tool that gives you the pnpm or yarn version your project expects. Node 24 is the newer LTS, so either works. What matters is to use the same Node major version on your laptop, in CI and on the server. Otherwise you will chase “works on my machine” bugs.

  4. Give the server read-only access to your repository

    A private repository needs credentials. Do not copy your personal SSH key onto a server. Instead, make a new key on the server. Add its public half in GitHub → repo → Settings → Deploy keys. A deploy key is an SSH key that works for one repository only. Leave “Allow write access” unchecked. If the server is ever broken into, the attacker can read one repository. They cannot push code or touch your other projects. This is least privilege (Lesson 5) applied to Git.

Deploy Next.js by hand

Clone the repository into your home folder. Create a .env file next to it and set its permissions to 600 (only you can read and write it; Lesson 1). On Vercel these values lived in a dashboard. Here they are in a file. For now the file needs only NODE_ENV=production and PORT=3000. The database comes in Lesson 8. AWS credentials are not in the file on purpose, because the IAM role provides them. Then run:

Build it
  • pnpm install --frozen-lockfile && pnpm build

    Install exactly the versions in the lockfile (the file that records every package version), then build the app

Two common .env mistakes
  • Variables that start with NEXT_PUBLIC_ are copied into the JavaScript at build time. If you change one, you must build again. A restart is not enough.
  • Never commit .env. If it is already in Git, treat every secret in it as leaked and replace it with a new one.

See it in your browser

The app listens on port 3000 (a port is a numbered door on a computer for network traffic), but web-sg does not allow that port. Add a temporary inbound rule for port 3000 with the source My IP. Then http://SERVER_IP:3000 works, but only for you. Delete that rule after Lesson 10, when Nginx takes over ports 80 and 443. Port 3000 must never be public.

Keep it alive with PM2

PM2 is a process manager for Node.js. It starts your app, restarts it if it crashes, and can start it again after a reboot. You need it because pnpm start in your SSH session stops the moment you close the terminal. This is the problem from Lesson 1 again: something other than your shell must own the process.

Eventnpm startpm2 start
You close the SSH terminalapp dieskeeps running (daemon)
App throws an uncaught errorprocess exits, site downrestarted in ~100 ms, logged
Server reboots (patch, crash)nothing starts, site downpm2 startup brings it back
You deploy new codekill, start, seconds of downtimepm2 reload — zero-downtime swap
Machine has 2 CPU cores1 core used-i max: one process per core
Why a process manager exists. Left: a bare `npm start`. Right: the same app under PM2.
PM2 setup — one time
  • pm2 start pnpm --name myapp -- start

    Run “pnpm start” under PM2. PM2 restarts the app whenever it crashes. (Install PM2 first with: sudo npm install -g pm2)

  • pm2 startup

    Make PM2 itself start when the server boots. It PRINTS a sudo command. Copy that line and run it

  • pm2 save

    Save the current process list, so that the server brings myapp back after a boot

Test it: reboot the server, wait a minute, log in again and run pm2 status. If myapp is online, it survives reboots. If not, you skipped startup or save. Day to day you will mostly use pm2 logs, pm2 restart and pm2 status.

Prove the IAM role works

Lesson 5 promised that the server could call AWS without any access keys. Run aws sts get-caller-identity on the server. This command shows who AWS thinks you are. The ARN now says assumed-role/myapp-ec2-role/i-0abc…. You are wearing the role, and there is no credentials file in ~/.aws.

The temporary credentials come from the instance metadata service (IMDS). This is a small web service that every instance can reach at the address 169.254.169.254. The credentials are replaced automatically every few hours. The AWS SDK in your Node app reads them the same way. This is why the S3 upload code in Lesson 5 needed no keys. The role only allows s3:PutObject and s3:GetObject on one bucket. So even a fully hacked app cannot delete your database.

Deploying a new version

Now the loop you will repeat until Lesson 14 automates it. Pull the new code, install, build, then run pm2 reload myapp. Reload is a gentle restart. (For a true zero-downtime reload, PM2 must run your app in cluster mode. In the simple setup here, expect a short gap.) Put the four steps in a small deploy.sh script that stops at the first error. Then you never skip a step at 11 p.m.

Notice what is wrong with this way. Lesson 14 fixes each problem. The build runs on the production server, so it competes with live traffic. A failed build can leave the site broken. There is no easy rollback (going back to the previous version). Only people with SSH access can deploy.

Disks, stop vs terminate, and the bill

The disk

The root disk is an EBS volume. This is a drive that connects to the server over the network. It is billed per GB per month (roughly $0.10 per GB for gp3, so 20 GB costs about $2). gp3 is a type of SSD disk. The disk survives when you stop the server. You can make it bigger later without downtime, but you can never make it smaller.

When something fails for no clear reason, check the free disk space first. When the disk is more than 90% full, builds and logs start to break. The usual causes are logs, node_modules and old builds.

Stop, terminate — and what each costs

Stop switches the server off, like shutting down a PC, and you can start it again later. Terminate deletes the server for good. They are different, so choose carefully.

ActionServerDiskYou keep paying for
StopPowered off, can be started againKeptThe disk + the Elastic IP. Not the compute
TerminateDestroyed foreverDeleted (default)Nothing — but release the Elastic IP yourself
RebootRestarts, same hostKeptSame as running
When you finish studying for the day

Stop the instance. A stopped t3.small costs about $6 a month (disk plus IP) instead of $16. Start it again tomorrow from the console. The Elastic IP means your address does not change.

Everything this lesson costs

ItemApprox. per month
t3.small, running 24/7~$16 (~₹1,350)
20 GB gp3 disk~$2
Elastic / public IPv4~$3.60
Data out to the internetFirst 100 GB/month free, then ~$0.109/GB
Key pair, instance profile, Security GroupsFree

New AWS accounts may also get promotional credits and a free plan for the first six months. Check the Billing console to see what your account has. Do not assume anything is free, and keep the Lesson 5 budget alert on.

Troubleshooting table

SymptomLikely causeFix
ssh: Connection timed outThe Security Group has no port-22 rule for your current IP, or the instance has no public IP or routeUse the Lesson 6 checklist. Update the “My IP” SSH rule to your current IP
Permission denied (publickey)Wrong username, wrong key, or .pem file permissions too openUbuntu AMIs use the user ubuntu. Run chmod 400 key.pem. Pass the key with -i
WARNING: UNPROTECTED PRIVATE KEY FILEOther users can read the .pem filechmod 400 ~/.ssh/myapp-key.pem
Build killed / “JavaScript heap out of memory”The instance does not have enough RAMAdd a 2 GB swap file (below) or use t3.small or larger
App works on :3000 from the server, not from your laptopPort 3000 is not open, or the app listens only on 127.0.0.1Open 3000 to your IP only. Check with ss -tlnp
502/blank after a rebootPM2 did not restart the appRun pm2 startup, then pm2 save (below)
Instance was reachable, now the IP changedPublic IPs are released when you stop the serverAttach an Elastic IP

Interview corner

What is the difference between stopping and terminating an EC2 instance?

Stop switches the machine off. The EBS disk and the Elastic IP stay, and you keep paying for them, but you do not pay for compute. Terminate deletes the instance and, by default, its root disk. Terminate cannot be undone.

How does an application on EC2 call S3 securely?

Attach an IAM role to the instance through an instance profile. The SDK gets short-lived credentials from the instance metadata service by itself. There are no access keys on disk, the permissions are narrow, and the credentials expire.

What is IMDSv2 and why enforce it?

The instance metadata service (169.254.169.254) gives out the role credentials. IMDSv2 is the second version. It first needs a session token, which you get with a PUT request. This stops simple SSRF bugs (server-side request forgery, where an attacker tricks your server into calling an address for them) from reading the credentials with a single GET request.

Your instance’s public IP changed after a restart. Why and how do you fix it?

An auto-assigned public IP is released when you stop the instance, and a new one is given when you start it. Attach an Elastic IP. Better still, put the servers behind a load balancer and point DNS at the load balancer.

The app works, then a reboot leaves the site down. What did you forget?

Nothing starts the process again after boot. With PM2, run pm2 startup and pm2 save. Other options are a systemd service (the Linux way to run background services) or Docker with a restart policy.

On-demand, Reserved, Savings Plans, Spot — when is each right?

On-demand means pay as you go. Use it for short or unpredictable work. Savings Plans and Reserved Instances mean you promise to use a steady amount for one or three years, and in return you pay roughly 30–70% less. Spot means you use spare AWS capacity at a big discount, but AWS can take it back with two minutes of notice. Use Spot for work that can be interrupted and holds no data, such as batch jobs and workers.

Why is running the build on the production server a bad idea?

The build competes with live traffic for CPU and RAM. It can fail halfway and leave a broken release. It is hard to repeat the same way twice, and it gives no rollback. Build once in CI (or in a container image) and ship the finished result, called the artefact. See Lessons 9 and 14.


Practice task before Lesson 8

  1. Launch the server and deploy your Next.js app as above. Open http://SERVER_IP:3000 from your laptop.
  2. Reboot the instance and confirm the app comes back by itself without you touching it.
  3. Run aws sts get-caller-identity on the server. Find the role name in the ARN. Then run aws s3 ls — it should be denied. Which line in the Lesson 5 policy explains why?
  4. Kill the app's process on purpose and watch PM2 start it again. Look at the restart counter in pm2 status.
  5. Add a page to your app, push it, and deploy it by hand. Measure how long the site was unavailable, if at all. Write down two ways this process could go wrong.
  6. When you are done for the day, stop the instance. Start it tomorrow and confirm the IP is unchanged.
Optional stretch

Change the web-sg SSH rule to a different (wrong) IP and try to log in. Watch it hang. Then fix it. Seeing what a Security Group block looks like from the outside helps you recognise every later “timeout”.

Conclusion

You now have a real server running your real app. You also know which parts of a Vercel deploy you replaced.

  • EC2 = a rented Linux computer. You choose the image, the size, the disk and the network.
  • Safe defaults that cost nothing: an encrypted disk, IMDSv2 required, SSH only from your IP, an IAM role instead of keys, and a read-only deploy key.
  • Something must watch your process: use PM2 with startup and save, so the app survives crashes and reboots.
  • Stop it, do not forget it: a running instance bills every second. A stopped one costs only its disk and IP.
  • The manual deploy is the blueprint for every automation to come: Docker (Lesson 9), CI/CD (14), Terraform (15).

The app is running, but it has nowhere to keep data. Next we add a managed PostgreSQL database that sits safely in the private subnet.


End of Lesson 7. Next: Lesson 8 — RDS: Managed PostgreSQL.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series