Back to path
BeginnerTaskFlow · Project 3 of 13 ~5h· 6 milestones

Deploy it to a single cloud VM

Continues from the last build: TaskFlow runs as containers locally via docker compose.

Last rung you got TaskFlow running as containers on your laptop with `docker compose up`: nginx serving the React build, the FastAPI backend on :8000, and Postgres, all wired together on a compose network.

Creating and securing an AWS account with a billing alarmLaunching and configuring an EC2 instanceWriting least-privilege security group rulesSSH key management and remote shell accessInstalling Docker and running docker compose on a remote Linux hostCritically evaluating a deployment's failure modes

What you'll build

A publicly reachable TaskFlow running on one EC2 instance via docker compose, fronted by a security group, with a billing alarm guarding your account, and a written, honest list of this design's failure modes that motivates managed services and Infrastructure as Code in the rungs ahead.

See how we teach, before you sign up

You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:

taskflow/compose.prod.yamlyaml
services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
    volumes:
      - ./frontend/dist:/usr/share/nginx/html:ro
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - backend
    restart: unless-stopped

  backend:
    build: ./backend
    environment:
      DATABASE_URL: ${DATABASE_URL}
    expose:
      - "8000"
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

volumes:
  pgdata:

Reading this file

  • "80:80"Maps the instance's port 80 to nginx inside the container, so the security group's HTTP rule actually reaches the app.
  • ./frontend/dist:/usr/share/nginx/html:roMounts your built React files into nginx read-only; you must run the Vite build before starting compose.
  • condition: service_healthyTells the backend to wait until Postgres reports healthy before starting, avoiding crash-on-boot from a not-yet-ready database.
  • pgdata:/var/lib/postgresql/dataStores Postgres data in a named Docker volume so your tasks survive a container restart or recreation.
  • restart: unless-stoppedTells Docker to bring the container back automatically after a crash or a server reboot, unless you stopped it on purpose.

The compose file the EC2 instance runs. Same three services as local, but nginx publishes port 80 to the host so the security group can expose it, and Postgres data lives in a named volume so a container restart does not wipe tasks.

That's 1 of 4 explained code blocks in this single project.

The build, milestone by milestone

  1. 1

    Open an AWS account and set a billing alarm before anything else

    4 guided steps

    Cloud is pay-per-use with no hard stop by default. A billing alarm is your seatbelt: it does not prevent spend, but it tells you the moment something is wrong while the bill is still small. Senior engineers treat 'cost is a first-class signal' as table stakes.

  2. 2

    Launch one EC2 instance with a key pair in eu-west-1

    4 guided steps

    A VM is the lowest-level compute abstraction you will use on this path. Understanding what you get (an OS, a public IP, a disk) and what you do not get (no app, no TLS, no scaling) is the baseline every higher abstraction is measured against.

  3. 3

    Write a least-privilege security group: SSH from your IP, HTTP from the world

    4 guided steps

    Security groups are AWS's stateful instance firewall and your first real least-privilege decision. Opening SSH to 0.0.0.0/0 is how bots find and brute-force your box within minutes. Scoping 22 to your IP while leaving 80 open to the world is the correct, deliberate split for a public web app.

  4. 4

    Install Docker and Docker Compose on the instance

    4 guided steps

    TaskFlow ships as containers, so the host needs a container runtime. Doing this manually shows you exactly why a hand-built server is unreproducible: these are imperative steps in your shell history, not in any file, so the next box you build will be subtly different.

  5. 5

    Deploy TaskFlow and reach it on the public IP

    4 guided steps

    This proves the full path end to end: security group, host, runtime, app, database, and the readiness probe at /healthz that every later rung reuses. It is also the moment the manual-deploy pain becomes concrete, since shipping a change means re-running these steps by hand.

  6. 6

    Name everything wrong with this and write the post-mortem

    4 guided steps

    Engineers grow by being able to articulate why a working thing is still not good enough. A single manually-built VM works and is the right starting point, but it is fragile, insecure-by-omission, and unreproducible. Stating that clearly is what separates 'it runs' from 'it is production-ready'.

What's inside when you start

4 starter files, ready to clone
6 guided milestones
4 code blocks explained line-by-line
6 "is it working?" checks
4 interview questions it prepares you for

You'll walk away with

An AWS account with MFA on root, an IAM admin user, and a confirmed CloudWatch billing alarm at $10
One running EC2 t3.micro in eu-west-1 reachable over SSH with a saved key pair
A security group allowing TCP 22 from your /32 and TCP 80 from the world, with no SSH-from-anywhere rule
TaskFlow serving at http://<public-ip> via docker compose: nginx, FastAPI backend, and Postgres with a persistent volume
compose.prod.yaml, nginx/default.conf, deploy.sh, and .env.prod.example committed to the repo
DEPLOYMENT_NOTES.md listing at least four failure modes, each mapped to the rung that fixes it

This is portfolio-grade. Build it free.

Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.

Start building