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
- Concept: a computer you rent by the second
- Why do it by hand first
- The seven words you must know
- Instance types and picking a size
- Launch the server
- Connect with SSH
- Prepare the server
- Deploy Next.js by hand
- Keep it alive with PM2
- Prove the IAM role works
- Deploying a new version
- Disks, stop vs terminate, and the bill
- Troubleshooting table
- Interview corner
- Practice task before Lesson 8
- 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.
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
| Word | What it is | Ours |
|---|---|---|
| AMI | Amazon 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 type | The hardware size of the server: how many virtual CPUs (vCPU) and how much memory (RAM). | t3.small = 2 vCPU, 2 GB |
| EBS volume | Elastic 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 pair | Two 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 Group | The firewall you built in Lesson 6. It lists which network traffic may reach the server. | web-sg |
| Instance profile | A 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 IP | A 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.
| Type | vCPU / RAM | Per month (approx.) | Verdict |
|---|---|---|---|
t3.micro | 2 / 1 GB | ~$8 | Fine to run a small app. Too small to run next build, because it runs out of memory. |
t3.small | 2 / 2 GB | ~$16 | Our choice for this course. It builds a normal Next.js app without trouble (with swap). |
t3.medium | 2 / 4 GB | ~$32 | Comfortable for a build, the app and a few background jobs. |
t4g.small | 2 / 2 GB | ~$13 | Same size on ARM (Graviton). About 20% cheaper, but it needs an arm64 image. |
c7i.large | 2 / 4 GB | ~$65 | Compute-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.
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.
| Field | Set it to | Why |
|---|---|---|
| Image (AMI) | Ubuntu Server 24.04 LTS | LTS means long-term support: it gets security fixes for years. Every command in this course assumes it |
| Instance type | t3.small | 2 GB RAM — enough for next build with swap |
| Key pair | Create new, ED25519, .pem | Your way in over SSH (secure remote login). AWS shows the private key only once |
| Network | myapp VPC, a public subnet, web-sg | The network from Lesson 6, so it gets a public IP |
| Storage | 20 GB gp3, encrypted | Encryption 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 profile | myapp-ec2-role | The role from Lesson 5 — AWS access with no keys on disk |
| Advanced → Metadata version | V2 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 |
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
ssh -i ~/.ssh/myapp-key.pem ubuntu@SERVER_IPThe 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:
Update the system
Install the security fixes released since the image was built. Also install
gitand the build tools that some npm packages need. If the kernel (the core of the operating system) is updated, you may need to reboot.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 buildis the step that uses the most memory in the life of your app. A 2 GB swap file is enough on a t3.small.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.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:
pnpm install --frozen-lockfile && pnpm buildInstall exactly the versions in the lockfile (the file that records every package version), then build the app
- 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.
| Event | npm start | pm2 start |
|---|---|---|
| You close the SSH terminal | app dies | keeps running (daemon) |
| App throws an uncaught error | process exits, site down | restarted in ~100 ms, logged |
| Server reboots (patch, crash) | nothing starts, site down | pm2 startup brings it back |
| You deploy new code | kill, start, seconds of downtime | pm2 reload — zero-downtime swap |
| Machine has 2 CPU cores | 1 core used | -i max: one process per core |
pm2 start pnpm --name myapp -- startRun “pnpm start” under PM2. PM2 restarts the app whenever it crashes. (Install PM2 first with: sudo npm install -g pm2)
pm2 startupMake PM2 itself start when the server boots. It PRINTS a sudo command. Copy that line and run it
pm2 saveSave 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.
| Action | Server | Disk | You keep paying for |
|---|---|---|---|
| Stop | Powered off, can be started again | Kept | The disk + the Elastic IP. Not the compute |
| Terminate | Destroyed forever | Deleted (default) | Nothing — but release the Elastic IP yourself |
| Reboot | Restarts, same host | Kept | Same as running |
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
| Item | Approx. per month |
|---|---|
| t3.small, running 24/7 | ~$16 (~₹1,350) |
| 20 GB gp3 disk | ~$2 |
| Elastic / public IPv4 | ~$3.60 |
| Data out to the internet | First 100 GB/month free, then ~$0.109/GB |
| Key pair, instance profile, Security Groups | Free |
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
| Symptom | Likely cause | Fix |
|---|---|---|
ssh: Connection timed out | The Security Group has no port-22 rule for your current IP, or the instance has no public IP or route | Use 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 open | Ubuntu AMIs use the user ubuntu. Run chmod 400 key.pem. Pass the key with -i |
WARNING: UNPROTECTED PRIVATE KEY FILE | Other users can read the .pem file | chmod 400 ~/.ssh/myapp-key.pem |
Build killed / “JavaScript heap out of memory” | The instance does not have enough RAM | Add a 2 GB swap file (below) or use t3.small or larger |
App works on :3000 from the server, not from your laptop | Port 3000 is not open, or the app listens only on 127.0.0.1 | Open 3000 to your IP only. Check with ss -tlnp |
502/blank after a reboot | PM2 did not restart the app | Run pm2 startup, then pm2 save (below) |
Instance was reachable, now the IP changed | Public IPs are released when you stop the server | Attach 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
- Launch the server and deploy your Next.js app as above. Open
http://SERVER_IP:3000from your laptop. - Reboot the instance and confirm the app comes back by itself without you touching it.
- Run
aws sts get-caller-identityon the server. Find the role name in the ARN. Then runaws s3 ls— it should be denied. Which line in the Lesson 5 policy explains why? - Kill the app's process on purpose and watch PM2 start it again. Look at the restart counter in
pm2 status. - 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.
- When you are done for the day, stop the instance. Start it tomorrow and confirm the IP is unchanged.
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
startupandsave, 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