DEV Community

Cover image for I built a complete EC2 deployment — and didn't ship it
Anas Khaled
Anas Khaled

Posted on

I built a complete EC2 deployment — and didn't ship it

My production backend runs on Vercel. This is a post about the EC2 deployment I built for it anyway, what building it taught me, and why choosing not to use it was the right engineering decision.

The workload

I'm the sole engineer on egypto, an AI assistant app for Arabic speakers, live on Google Play. The Flutter client talks to a Node.js/Express backend, and that backend has what I'd call a persistent-process shape:

  • A chat request opens a Server-Sent Events connection and holds it open while the backend consumes an upstream model stream, parses partial chunks, and forwards incremental output to the client
  • The same process keeps Redis-backed state
  • It accepts multipart image and audio uploads
  • It runs scheduled prompt-generation work

Each of those concerns is solvable on a per-request-function platform. All four at once is where it gets interesting: streaming needs the right integration and duration configuration, Redis and scheduled work want to move into separate managed services, and uploads meet platform-specific body handling. None of it is impossible. All of it moves state and lifecycle management out of your process and into platform boundaries.

Before the app had users, I wanted to know what the alternative actually looked like. Not in a diagram — deployed, health-checked, receiving traffic.

What I built

A deliberately small target: one t3.micro running Amazon Linux 2023, Docker Compose running the Express backend and a Redis sidecar. Supabase stayed as managed Postgres; Firebase Admin and the LLM providers stayed as managed dependencies.

Terraform provisions everything: VPC, public subnet, Elastic IP, security group, IAM roles, Secrets Manager resources, CloudWatch log group, an encrypted 30 GiB EBS volume, and the instance itself.

The first working pipeline was GitHub Actions over SSH. It worked, and I didn't like it — it needed an inbound SSH path and a long-lived private deploy key sitting in GitHub. So I rebuilt it: GitHub OIDC issues short-lived AWS credentials, the deployment goes through Systems Manager, port 22 is closed by default, the instance pulls its runtime environment from Secrets Manager at startup, and the workflow waits on /health before accepting a release.

What broke

This is the part a hosted platform never shows you, and it's why the exercise was worth doing:

The root volume was too small. The Amazon Linux image I selected needed more disk than the default allocation. Trivial to fix, invisible until you own the disk.

Container startup outran the health check. On a t3.micro, cold container startup exceeded the first health-check window, so a perfectly good deployment reported as failed. Sizing health-check timing against your actual instance class is a real decision, not a default.

Missing Firebase Admin credentials crashed startup. The backend initialized Firebase Admin eagerly, so a secrets misconfiguration took the whole process down before it could serve anything. I made initialization lazy — protected routes initialize it on first use — which turned a boot-time crash into a scoped runtime error.

Redis wasn't at localhost. Inside the Compose network, the backend has to address Redis by its service name. Obvious in retrospect, and exactly the kind of thing that works on your laptop and fails on the instance.

Four small failures, each one a piece of operational knowledge that Vercel had been quietly handling for me.

Why production is still Vercel

Here's the honest part: there is no before-and-after latency or stream-completion metric in this post, because there were no users when I built this, and I recorded none. I'm not going to invent a benchmark to justify the work.

What I can say is what EC2 costs: operating system patching, capacity and disk management, monitoring, rollback, and availability all become my responsibility. The topology I built is a single low-cost instance — deliberately not highly available, because a load balancer and multiple instances should be justified by real traffic and real availability requirements, not by how a portfolio looks.

At the app's current scale, Vercel's managed operations win. The streaming works there with the right function duration configuration. The moment the workload's persistent-process shape starts fighting the platform — connection limits, duration ceilings, the operational cost of the managed services that replaced my Redis sidecar — the EC2 path is built, tested, reproducible from Terraform, and waiting.

What I'd tell you to take from this

Building infrastructure you don't ship sounds like waste. It isn't, for three reasons:

  1. You learn where your platform's magic is. I now know exactly which operational problems Vercel absorbs, because I hit each one myself.
  2. You buy a real option. Migration is no longer a research project; it's a Terraform apply and a DNS change.
  3. You practice the decision, not just the build. The hardest part wasn't the pipeline — it was concluding, with the work already done, that the simpler platform was still correct.

The full Flutter client is open source at github.com/AnasKhaled1876/egypto_ai, including the SSE decoder that handles the client side of the streaming story.

Top comments (0)