Deploy a React Inventory App to EKS
Deploy a React inventory app to Amazon EKS with FastAPI, PostgreSQL, and CI/CD.
Introduction
30 Second Summary
A stock count can look correct until two orders claim the same last item. The real test is whether the system keeps its promise under pressure.
In this project, you will build StockGuard, a React inventory dashboard backed by FastAPI plus PostgreSQL. You will move the same application from local Docker containers to Amazon EKS through a secure GitHub Actions release pipeline.
What You'll Build
From one public dashboard, you can reserve stock safely while an automated release updates two healthy cloud replicas behind the scenes.
By the end of this project, you'll have:
- An interactive inventory dashboard where you can view live stock levels, reserve units, and see quantities update immediately.
- A transaction-safe reservation flow that rejects oversized requests without allowing inventory to fall below zero.
- A container-to-cloud release path that runs locally before serving the same application through a public load balancer backed by two replicas.
- Secret Mission: trigger a failed Kubernetes rollout deliberately, diagnose the broken release, and recover the live application with a rollback.
Are there any prerequisites?
You need a Linux computer with internet access plus permission to install software. You also need GitHub access plus an AWS account authorized to create the project's cloud resources.
Before We Start
This checkpoint turns the StockGuard plan into a commitment before the hands-on work begins. You are committing to four connected layers: a React inventory dashboard, a transaction-safe FastAPI API backed by PostgreSQL, a containerized Kubernetes deployment, plus a recoverable CI/CD release pipeline.
Prepare the Linux Toolchain
StockGuard will move from your Linux workstation into a Kubernetes cluster. Every later build depends on the same local toolchain reaching Amazon Web Services correctly.
A missing command can stop a fast build before the application work begins. This step verifies Visual Studio Code, Git, Docker Engine, Docker Compose, AWS CLI, kubectl, eksctl, plus GitHub.
In this step, get ready to:
- Verify your editor, version control tools, plus container tools.
- Prepare the pinned AWS and Kubernetes command-line tools.
- Create the StockGuard repository structure on your Desktop.
Verify the editor and container tools
Version output proves that each command is available. A test container proves that Docker can execute a workload through its running daemon.
- Check that Visual Studio Code is available by running this command:
code --version
What does this check prove?
This command prints the installed Visual Studio Code version. It also confirms that the editor command is available from your terminal.
- Check Git, Docker Engine, plus Docker Compose by running this command:
git --version && docker --version && docker compose version
What do these checks prove?
- The Git check confirms that local version control is available.
- The Docker check confirms that the container command is installed.
- The Docker Compose check confirms that multi-container projects can use the Compose plugin.
Choose the tab that matches your terminal output. Use the missing-tools path only for commands that failed.
✔️ Every command prints a version
Your editor, Git installation, plus container commands are available. The next check proves that Docker can start a container.
- Test the Docker daemon by running this command:
sudo docker run hello-world
What does this container test prove?
Docker downloads a small test image. It starts a container that prints a confirmation message before exiting.
You should see a confirmation message from the test container. Your workstation can now run containers.
ⓧ One or more commands are missing
Install only the missing tools. Each official guide provides packages for supported Linux distributions.
- Install Visual Studio Code from the official Linux setup guide if the editor check failed.
- Install Git from the official Linux installation page if the Git check failed.
- Confirm the Visual Studio Code installation by running this command:
code --version
What should this show?
The command should print the installed Visual Studio Code version. A version confirms that your terminal can find the editor.
- Confirm the Git installation by running this command:
git --version
What should this show?
The command should print the installed Git version. That output confirms the package is available.
Ubuntu may request your administrator password during the Docker installation. Its package manager may also ask you to confirm the changes.
- Configure Docker's official Ubuntu package repository by running these commands:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
What does this setup do?
- The first commands install the tools needed to download signed packages.
- The key allows Ubuntu to verify packages published by Docker.
- The repository entry gives the package manager access to Docker Engine plus its Compose plugin.
- Install Docker Engine plus Docker Compose by running this command:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
What does this installation include?
This installs the Docker daemon, its command-line client, the container runtime, the build plugin, plus the Compose plugin.
- Prove that Docker can start a container by running this command:
sudo docker run hello-world
What should this prove?
Docker should download the test image. The container should print a confirmation message before exiting.
Still missing a local tool?
- Confirm that you used the installation guide matching your Linux distribution.
- Close your terminal after installing Visual Studio Code. Start a new terminal so it reloads the available commands.
- Start the Docker service if the client is installed but the container test cannot reach the daemon.
Ask for help with my Linux tool installation.
Install the AWS and Kubernetes command-line tools
Cloud deployment uses three separate commands. AWS CLI authenticates requests to AWS. kubectl controls Kubernetes resources. eksctl creates the Amazon EKS cluster.
- Check the current AWS CLI, kubectl, plus eksctl versions by running this command:
aws --version && kubectl version --client && eksctl version
Which versions does StockGuard need?
- AWS CLI must use major version v2.
- kubectl must use version v1.37.
- eksctl must use version 0.230.0.
✔️ All required versions are installed
AWS CLI reports major version v2. kubectl reports v1.37. eksctl reports 0.230.0.
Your cloud command-line tools are ready for authentication.
ⓧ I see an older version
Update every older tool before continuing. Version alignment prevents client compatibility problems during cluster work.
- Install the current AWS CLI v2 release for your user by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash
What does this installer do?
This downloads the official AWS CLI installer. It installs the current AWS CLI v2 release for your user.
- Install kubectl for x86-64 Linux by running these commands:
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
What does this kubectl install do?
The first command downloads the current stable kubectl binary for x86-64 Linux. The second command installs it as an executable available across the system.
Use the official kubectl Linux guide if your computer uses a different architecture.
- Install the current eksctl release by running these commands:
ARCH=$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/')
PLATFORM=$(uname -s)_$ARCH
curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_$PLATFORM.tar.gz"
curl -sL "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_checksums.txt" | grep $PLATFORM | sha256sum --check
tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz
sudo install -m 0755 /tmp/eksctl /usr/local/bin && rm /tmp/eksctl
What does this eksctl install do?
- The architecture check selects the correct Linux release archive.
- The checksum check confirms that the downloaded archive matches the published release.
- The final commands extract eksctl into a temporary location. They install the executable into the system command path.
- Confirm the updated versions by running this command:
aws --version && kubectl version --client && eksctl version
What should the update confirm?
You should now see AWS CLI major version v2, kubectl v1.37, plus eksctl 0.230.0.
ⓧ One or more commands are missing
Install each missing command from its official release channel. The checks at the end confirm the required versions.
- Install AWS CLI v2 for your user by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash
What does this installer do?
This downloads the official AWS CLI installer. It installs AWS CLI v2 for your user.
- Install kubectl for x86-64 Linux by running these commands:
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
What does this kubectl install do?
The download retrieves the current stable kubectl binary for x86-64 Linux. The install command makes it available from your terminal.
Use the official kubectl Linux guide if your computer uses a different architecture.
- Install the current eksctl release by running these commands:
ARCH=$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/')
PLATFORM=$(uname -s)_$ARCH
curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_$PLATFORM.tar.gz"
curl -sL "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_checksums.txt" | grep $PLATFORM | sha256sum --check
tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz
sudo install -m 0755 /tmp/eksctl /usr/local/bin && rm /tmp/eksctl
What does this eksctl install do?
These commands select your Linux architecture. They verify the downloaded release before installing eksctl into the system command path.
- Confirm every installation by running this command:
aws --version && kubectl version --client && eksctl version
What should the installation confirm?
You should see AWS CLI major version v2, kubectl v1.37, plus eksctl 0.230.0.
Still seeing a missing command?
- Start a new terminal after installation so your shell reloads its command path.
- Check that the installation completed without a checksum failure.
- Confirm that your Linux architecture matches the kubectl download.
Ask for help with my missing cloud command.
Credentials deserve extra care. Use only the access method supplied by your AWS account owner.
- Obtain credentials for an identity authorized to create the project resources.
- Configure access-key credentials by running this command if your account uses access keys:
aws configure
What does AWS CLI ask for?
- Enter your AWS access key ID at the first credential prompt.
- Enter your AWS secret access key at the second credential prompt.
- Enter us-east-1 as the default region.
- Enter json as the default output format.
Use your organization's documented sign-in flow if it provides temporary or centrally managed credentials.
Before you check, which AWS identity do you expect the configured credentials to return?
- Confirm the active AWS identity by running this command:
aws sts get-caller-identity
What does this identity check prove?
The response identifies the current AWS account plus the active user or role. Confirm that it is the identity authorized for this lab.
Your credentials stay out of the output. The response contains identity metadata only.
That is the cloud access checkpoint complete. Your terminal can authenticate to the intended AWS account.
Create and clone the StockGuard repository
The GitHub repository becomes the shared source for application code plus deployment automation. Cloning it to your Desktop gives Visual Studio Code a local workspace with Git already connected.
- Open the GitHub new repository page in your browser.
- Sign in to your GitHub account if prompted.
- Select your account from the Owner menu.
- Enter stockguard in the Repository name field.
- Choose Private as the repository visibility.
- Set Add README to On.
- Click Create repository.
What did you create?
The repository now stores StockGuard's tracked files on GitHub. Its initial README gives Visual Studio Code a commit to clone.
- Start Visual Studio Code from your Linux application menu.
- Press Ctrl+Shift+P to open the Command Palette.
- Enter Git: Clone in the Command Palette.
- Select Clone from GitHub.
- Complete the browser sign-in if Visual Studio Code requests GitHub authentication.
- Select the stockguard repository.
- Select your Desktop as the parent folder for the clone.
- Select Open when Visual Studio Code finishes cloning.
What does cloning connect?
Visual Studio Code opens the local stockguard folder. Git also records the GitHub repository as the remote source.
- Confirm that you trust the cloned workspace if Visual Studio Code asks.
- Open a new integrated terminal by pressing Ctrl+Shift+`.
- Create the required project directories by running these commands:
mkdir backend frontend k8s .github infra
mkdir frontend/src .github/workflows
How is the repository organized?
- The backend directory holds the FastAPI application.
- The frontend/src directory holds the React source files.
- The k8s directory holds Kubernetes resources.
- The .github/workflows directory holds GitHub Actions workflows.
- The infra directory holds infrastructure support files.
- Check the Explorer sidebar for backend, frontend/src, k8s, .github/workflows, plus infra.
Missing a directory in Explorer?
- Confirm that the terminal prompt is inside the cloned stockguard folder.
- Refresh the Explorer sidebar if the directory list has not updated.
- Expand frontend plus .github to reveal their nested directories.
Ask for help with my StockGuard folder structure.
Before the final check, do you expect every command to return successfully from the same terminal?
- Run the complete toolchain verification command:
git --version && docker --version && docker compose version && aws --version && kubectl version --client && eksctl version
What does the final tool check cover?
This checks every command required for local source control, container work, AWS access, plus Amazon EKS management. Each successful segment allows the next check to run.
You should see a version for every tool. kubectl should report v1.37. eksctl should report 0.230.0.
- Verify the active AWS account one final time by running this command:
aws sts get-caller-identity
What should the AWS response confirm?
The response should identify the intended AWS account plus an authorized user or role. No access key or secret key should appear.
You now have a version-verified toolchain, working container execution, authenticated AWS access, plus a cloned StockGuard workspace.
Your Linux workstation is ready. Next, you will build the transaction-safe inventory API inside the prepared backend workspace.
Build the FastAPI Backend
The Linux toolchain is ready. Now StockGuard needs an API that can store inventory and protect each reservation.
Overlapping requests could otherwise sell the same stock twice. This step uses FastAPI with PostgreSQL to check the available quantity inside the same database update that performs the reservation.
In this step, get ready to:
- Define the pinned backend dependencies and FastAPI routes.
- Configure PostgreSQL and the API with Docker Compose.
- Test valid reservations and rejected oversized requests through the interactive API documentation.
Define the database-backed API
The backend dependencies stay pinned so every container installs the same versions. The API code then owns the database connection and every inventory change.
- In the Explorer sidebar in VS Code, select the backend folder.
- Click the New File icon beside the folder name.
- Enter requirements.txt as the file name.
- Add the pinned backend dependencies by pasting this code:
fastapi==0.141.1
uvicorn==0.53.0
psycopg[binary]==3.3.6
What do these dependencies provide?
- FastAPI 0.141.1 defines the API routes and validates request bodies.
- Uvicorn 0.53.0 runs the FastAPI application inside the container.
- Psycopg 3.3.6 connects Python to PostgreSQL. The binary extra supplies the packaged database driver.
- Save backend/requirements.txt.
The saved file now lists all three backend dependencies in the backend folder.
Dependency file in the wrong folder?
Confirm that VS Code shows requirements.txt inside the backend folder. Move the file there if it appears beside backend.
Ask for help if the folder structure still looks different: Help me place requirements.txt inside the backend folder in my stockguard repository.
✔️ Awesome, I've got everything!
Your dependency file is saved inside backend.
ⓧ I'd like to double check the full code
Compare your dependency file with this complete version.
fastapi==0.141.1
uvicorn==0.53.0
psycopg[binary]==3.3.6
The API needs one reusable connection function before it can initialize inventory. Environment-based configuration lets Docker Compose provide the database address when the container starts.
- Select the backend folder in the VS Code Explorer sidebar.
- Click the New File icon beside the folder name.
- Enter main.py as the file name.
- Add the imports and database connection foundation by pasting this code:
from contextlib import asynccontextmanager
import os
import psycopg
from psycopg.rows import dict_row
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
DATABASE_URL = os.environ["DATABASE_URL"]
def connect():
return psycopg.connect(DATABASE_URL, row_factory=dict_row)
How does the connection work?
- The imports provide the API framework and request validation. They also provide PostgreSQL access.
- The DATABASE_URL value comes from the container environment.
- The connect() function returns rows as dictionaries. That shape can be sent back as JSON responses.
- Save backend/main.py.
VS Code now shows main.py inside backend without an unsaved indicator.
Seeing Python import warnings?
The dependencies run inside the API container later in this step. Your editor may underline these imports when the same packages are absent from the Linux host.
Check the copied names against backend/requirements.txt. Help me compare my backend imports with my requirements file.
The database must create its table before any route can read inventory. Seed rows make the API useful as soon as the service starts.
- Place your cursor below the connect() function in backend/main.py.
- Add the database initialization function by pasting this code:
def initialize_database():
with connect() as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS inventory (
sku TEXT PRIMARY KEY,
name TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity >= 0)
)
"""
)
connection.executemany(
"""
INSERT INTO inventory (sku, name, quantity)
VALUES (%s, %s, %s)
ON CONFLICT (sku) DO NOTHING
""",
[
("KB-101", "Mechanical Keyboard", 12),
("MS-204", "Wireless Mouse", 18),
("HD-330", "USB-C Hub", 8),
],
)
How is the inventory initialized?
- The table constraint prevents PostgreSQL from storing a negative quantity.
- The first database start inserts three demonstration products.
- The conflict rule preserves existing quantities when the API restarts.
- Save backend/main.py.
The file now contains the three starting products with quantities of 12, 18, and 8.
Seed data looks misaligned?
Check that every product tuple stays inside the list passed to executemany(). Confirm that the closing brackets match the reference code.
Use this prompt for a focused review: Help me check the brackets and indentation in initialize_database().
FastAPI can run initialization through its application lifespan. The next section also adds CORS rules for the local dashboard that arrives in the next step.
- Place your cursor below the initialize_database() function.
- Add the application lifespan and read routes by pasting this code:
@asynccontextmanager
async def lifespan(app: FastAPI):
initialize_database()
yield
app = FastAPI(title="StockGuard API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["GET", "POST"],
allow_headers=["content-type"],
)
class Reservation(BaseModel):
sku: str
quantity: int
@app.get("/api/health")
def health():
return {"status": "ok"}
@app.get("/api/inventory")
def inventory():
with connect() as connection:
return connection.execute(
"SELECT sku, name, quantity FROM inventory ORDER BY sku"
).fetchall()
What does this API section provide?
- The lifespan function initializes the database when FastAPI starts.
- The middleware permits the future local React dashboard to send read and reservation requests.
- The Reservation model describes the required request fields.
- The health route confirms that the API process responds. The inventory route returns the current products in SKU order.
- Save backend/main.py.
The saved file now defines the application and its two read endpoints.
Route decorators showing errors?
Confirm that app = FastAPI(title="StockGuard API", lifespan=lifespan) appears before both route decorators. Check that each decorator begins at the left edge of the file.
Ask for help with the structure if needed: Help me check the FastAPI app and route order in backend/main.py.
The reservation route must make its decision inside PostgreSQL. One atomic update checks the available quantity before it subtracts stock.
- Place your cursor below the inventory() function.
- Add the reservation route by pasting this code:
@app.post("/api/reservations")
def reserve(request: Reservation):
if request.quantity < 1:
raise HTTPException(status_code=400, detail="Quantity must be positive")
with connect() as connection:
row = connection.execute(
"""
UPDATE inventory
SET quantity = quantity - %s
WHERE sku = %s AND quantity >= %s
RETURNING sku, name, quantity
""",
(request.quantity, request.sku, request.quantity),
).fetchone()
if row is None:
raise HTTPException(status_code=409, detail="Not enough stock")
return row
Why is the update atomic?
The parameterized SQL statement checks quantity >= %s inside the same update that subtracts stock. PostgreSQL performs that operation as one transaction-safe decision.
A request that cannot reserve stock returns HTTP 409. The stored quantity stays unchanged.
- Save backend/main.py.
Your backend now has health, inventory, and reservation routes in one saved application file.
Reservation function indented incorrectly?
Keep the quantity check and database context inside reserve(). Keep the final insufficient-stock check aligned with the database context.
Use this prompt to inspect the function: Help me find an indentation problem in reserve().
✔️ Awesome, I've got everything!
Your complete FastAPI application is saved in backend/main.py.
ⓧ I'd like to double check the full code
Compare your file with this complete version.
from contextlib import asynccontextmanager
import os
import psycopg
from psycopg.rows import dict_row
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
DATABASE_URL = os.environ["DATABASE_URL"]
def connect():
return psycopg.connect(DATABASE_URL, row_factory=dict_row)
def initialize_database():
with connect() as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS inventory (
sku TEXT PRIMARY KEY,
name TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity >= 0)
)
"""
)
connection.executemany(
"""
INSERT INTO inventory (sku, name, quantity)
VALUES (%s, %s, %s)
ON CONFLICT (sku) DO NOTHING
""",
[
("KB-101", "Mechanical Keyboard", 12),
("MS-204", "Wireless Mouse", 18),
("HD-330", "USB-C Hub", 8),
],
)
@asynccontextmanager
async def lifespan(app: FastAPI):
initialize_database()
yield
app = FastAPI(title="StockGuard API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["GET", "POST"],
allow_headers=["content-type"],
)
class Reservation(BaseModel):
sku: str
quantity: int
@app.get("/api/health")
def health():
return {"status": "ok"}
@app.get("/api/inventory")
def inventory():
with connect() as connection:
return connection.execute(
"SELECT sku, name, quantity FROM inventory ORDER BY sku"
).fetchall()
@app.post("/api/reservations")
def reserve(request: Reservation):
if request.quantity < 1:
raise HTTPException(status_code=400, detail="Quantity must be positive")
with connect() as connection:
row = connection.execute(
"""
UPDATE inventory
SET quantity = quantity - %s
WHERE sku = %s AND quantity >= %s
RETURNING sku, name, quantity
""",
(request.quantity, request.sku, request.quantity),
).fetchone()
if row is None:
raise HTTPException(status_code=409, detail="Not enough stock")
return row
Configure the local database and API
Docker Compose describes how the two local services run together. The database keeps its files in a named volume while the API receives its connection address through the container environment.
- Select the top-level stockguard folder in the VS Code Explorer sidebar.
- Click the New File icon beside the repository name.
- Enter compose.yaml as the file name.
- Define the PostgreSQL service by pasting this code:
services:
db:
image: postgres:18.6-alpine3.24
restart: unless-stopped
environment:
POSTGRES_USER: stockguard
POSTGRES_PASSWORD: stockguard-local-password
POSTGRES_DB: stockguard
volumes:
- postgres-data:/var/lib/postgresql
How does the database service persist data?
- The postgres:18.6-alpine3.24 image supplies PostgreSQL 18.6.
- The environment values create the local demonstration database and user.
- The postgres-data volume keeps the database files outside the container lifecycle.
- Save compose.yaml.
The repository now contains a Compose file with the db service.
YAML indentation looks uneven?
Use spaces for every indentation level in compose.yaml. Keep db beneath services.
Ask for a focused indentation check: Help me inspect the db service indentation in compose.yaml.
The API service uses the backend folder already open in VS Code. Its restart policy handles the brief period when PostgreSQL is still initializing.
- Place your cursor below the db service in compose.yaml.
- Add the API service and volume declaration by pasting this code:
api:
image: python:3.13.15-slim-trixie
working_dir: /app
restart: on-failure
depends_on:
- db
environment:
DATABASE_URL: postgresql://stockguard:stockguard-local-password@db:5432/stockguard
volumes:
- ./backend:/app
command: >-
sh -c "pip install --no-cache-dir -r requirements.txt &&
uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
ports:
- "8000:8000"
volumes:
postgres-data:
How does the API service start?
- The python:3.13.15-slim-trixie image provides the Python runtime.
- The backend folder is mounted at /app so saved code changes are available inside the container.
- The startup command installs the pinned packages before Uvicorn serves the API on port 8000.
- The database hostname is db because Compose gives each service its own network name.
- Save compose.yaml.
The complete Compose configuration now links the API to PostgreSQL and publishes the API on local port 8000.
Database address split across lines?
Keep the complete DATABASE_URL value on one line. Confirm that the hostname inside it is db.
Use this prompt for help: Help me compare the api service in compose.yaml with its database connection settings.
✔️ Awesome, I've got everything!
Your Compose configuration defines both local services and the named database volume.
ⓧ I'd like to double check the full code
Compare your file with this complete version.
services:
db:
image: postgres:18.6-alpine3.24
restart: unless-stopped
environment:
POSTGRES_USER: stockguard
POSTGRES_PASSWORD: stockguard-local-password
POSTGRES_DB: stockguard
volumes:
- postgres-data:/var/lib/postgresql
api:
image: python:3.13.15-slim-trixie
working_dir: /app
restart: on-failure
depends_on:
- db
environment:
DATABASE_URL: postgresql://stockguard:stockguard-local-password@db:5432/stockguard
volumes:
- ./backend:/app
command: >-
sh -c "pip install --no-cache-dir -r requirements.txt &&
uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
ports:
- "8000:8000"
volumes:
postgres-data:
Start and test the backend
The code and service configuration are ready to run together. The interactive documentation provides a visible way to call each route without building the React dashboard first.
The first start can take a few minutes while Docker downloads the images and installs the Python packages. The terminal stays attached to the service logs after startup.
- Return to the terminal from the previous step.
- Start PostgreSQL and the API by running this command:
docker compose up db api
What does this command start?
Docker Compose starts the db service and the api service from compose.yaml. The API initializes the inventory table when its application lifespan begins.
When startup completes, the logs show that the API is serving requests. PostgreSQL remains attached to the named postgres-data volume.
API keeps restarting?
A brief restart can happen while PostgreSQL finishes initializing. Wait for the database to become ready before treating the restart as a failure.
Confirm that the connection hostname is db if restarts continue. Help me diagnose my Docker Compose API logs.
Before you test the routes, what do you expect the health response and the initial inventory list to confirm?
- Open a browser tab at http://localhost:8000/docs.
- Expand GET /api/health in the Swagger UI page.
- Click Try it out.
- Click Execute.
You will see a response body with the status set to ok. This proves the API process is responding.
- Expand GET /api/inventory.
- Click Try it out.
- Click Execute.
You will see Mechanical Keyboard with quantity 12. You will also see Wireless Mouse with quantity 18 and USB-C Hub with quantity 8.
- Expand POST /api/reservations.
- Click Try it out.
- Set the generated request body's sku value to KB-101.
- Set the generated request body's quantity value to 1.
- Click Execute.
You will see the Mechanical Keyboard response with a quantity one lower than before. The valid reservation changed the stored inventory immediately.
Before the next request, do you think PostgreSQL will subtract 12 units from a product that only has 8 available?
- Set the request body's sku value to HD-330.
- Set the request body's quantity value to 12.
- Click Execute.
You will see HTTP 409 with a message that there is not enough stock. This rejection is the intended proof that the reservation guard works.
- Return to GET /api/inventory.
- Send the inventory request again with its request controls.
You will see USB-C Hub still at quantity 8. The rejected request did not produce negative inventory.
Interactive documentation does not load?
Confirm that the Compose logs still show the API running. Check that the browser address uses port 8000.
If the inventory request fails, inspect the API logs for a database connection problem. Help me diagnose why my FastAPI documentation or inventory route is unavailable.
That is the stock safeguard working. Your local API can now serve inventory and reject reservations that exceed what PostgreSQL holds.
The backend is running on port 8000. Next up, you will turn these API responses into an interactive React operations dashboard.
Build the React Dashboard
Your transaction-safe FastAPI API now protects every reservation. The next goal is to make that behavior visible to an operator.
Raw JSON is difficult to demonstrate. In this step, React turns your inventory into an interactive dashboard. A temporary localhost address connects the dashboard to FastAPI during local development.
In this step, get ready to:
- Create the Vite frontend foundation.
- Build the interactive inventory view.
- Run the full dashboard through Docker Compose.
Create the Vite frontend foundation
Vite provides the development server that displays your React interface. Its package manifest also pins each frontend dependency so every container installs the same versions.
- Switch back to Visual Studio Code with the stockguard repository from earlier.
- Create frontend/package.json with the New File control in the Explorer sidebar.
- Add the frontend package manifest by pasting this code:
{
"name": "stockguard-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "6.1.1",
"vite": "8.3.0"
}
}
What does this package manifest do?
- The scripts section defines the development server and production build commands.
- The dependencies pin React and React DOM to 19.3.0.
- The development dependencies pin Vite to 8.3.0 and its React plugin to 6.1.1.
- Save frontend/package.json.
- Confirm package.json appears inside the frontend folder in the Explorer sidebar.
Package file in the wrong folder?
Check that the Explorer path reads frontend/package.json. Move the file into frontend if it appears beside that folder.
Ask for help with the file location:
- Create frontend/index.html with the New File control in the Explorer sidebar.
- Add the browser entry page by pasting this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="StockGuard inventory reservation dashboard" />
<title>StockGuard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
How does the page start React?
- The root element gives React a place to render the dashboard.
- The module script loads /src/main.jsx as the JavaScript entry point.
- The description identifies the page as the StockGuard inventory reservation dashboard.
- Save frontend/index.html.
- Confirm index.html appears beside package.json in the Explorer sidebar.
HTML file missing from frontend?
Confirm the file name is exactly index.html. Check that it sits directly inside frontend.
Ask for help checking the folder structure.
- Create frontend/src/main.jsx with the New File control in the Explorer sidebar.
- Connect the HTML entry page to the React component by pasting this code:
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './styles.css'
createRoot(document.getElementById('root')).render(<App />)
What does the entry point do?
- The createRoot import enables React to control the HTML element named root.
- The App import supplies the dashboard component.
- The stylesheet import applies the StockGuard design when the component renders.
- Save frontend/src/main.jsx.
- Confirm the Explorer sidebar now shows main.jsx inside frontend/src.
Entry file in the wrong location?
Check that main.jsx sits inside frontend/src. The import paths depend on that exact location.
Ask for help checking the entry file path.
Build the interactive inventory view
React state holds the inventory response and the quantity typed into each card. An effect loads the stock when the dashboard first renders.
The reservation handler sends each request to the API. A successful response replaces the matching card with the new PostgreSQL quantity.
- Create frontend/src/styles.css with the New File control in the Explorer sidebar.
- Style the dashboard by pasting this code:
:root { color: #eef2ff; background: #080b14; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
* { box-sizing: border-box; }
body { min-width: 320px; min-height: 100vh; margin: 0; background: radial-gradient(circle at top left, #25315f 0, transparent 38%), #080b14; }
button, input { font: inherit; }
.shell { width: min(1100px, calc(100% - 32px)); margin: 0 auto; padding: 64px 0; }
header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 32px; }
h1, h2, p { margin-top: 0; }
h1 { margin-bottom: 8px; font-size: clamp(2.5rem, 7vw, 5rem); letter-spacing: -0.06em; }
.eyebrow, .sku { color: #8ea2ff; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }
.status { max-width: 340px; padding: 10px 14px; border: 1px solid #56607d; border-radius: 999px; color: #fbbf24; background: #111827; }
.status.online { color: #86efac; border-color: #166534; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; }
.card { display: grid; gap: 24px; padding: 24px; border: 1px solid #28324b; border-radius: 20px; background: rgba(17, 24, 39, 0.88); box-shadow: 0 20px 70px rgba(0, 0, 0, 0.28); }
.card strong { color: #c7d2fe; font-size: 1.35rem; }
.controls { display: grid; grid-template-columns: 82px 1fr; gap: 10px; }
input, button { min-height: 44px; border-radius: 10px; }
input { width: 100%; border: 1px solid #44506f; padding: 0 12px; color: #eef2ff; background: #0b1020; }
button { border: 0; padding: 0 18px; color: #07111f; background: #9fb0ff; cursor: pointer; font-weight: 800; }
button:hover { background: #c7d2fe; }
@media (max-width: 640px) { header { flex-direction: column; } }
How does the dashboard design work?
- The shell and grid rules arrange the dashboard within a responsive page.
- The card rules separate each product into a distinct inventory panel.
- The status colors distinguish a connected API from a loading or error state.
- The media query stacks the header on narrow screens.
- Save frontend/src/styles.css.
- Confirm styles.css appears beside main.jsx in the Explorer sidebar.
Stylesheet outside the src folder?
Move styles.css beside main.jsx if it appears elsewhere. The relative import in main.jsx expects both files in the same folder.
Ask for help checking the stylesheet import.
- Create frontend/src/App.jsx with the New File control in the Explorer sidebar.
- Treat the next two code blocks as one component. Paste both before saving the file.
- Start the component with its state and inventory-loading logic by pasting this first chunk:
import { useEffect, useState } from 'react'
const API_BASE = 'http://localhost:8000'
export default function App() {
const [inventory, setInventory] = useState([])
const [quantities, setQuantities] = useState({})
const [status, setStatus] = useState('Loading inventory...')
async function loadInventory() {
try {
const response = await fetch(`${API_BASE}/api/inventory`)
if (!response.ok) throw new Error('Inventory request failed')
setInventory(await response.json())
setStatus('Connected')
} catch (error) {
setStatus(`Cannot reach API: ${error.message}`)
}
}
useEffect(() => {
loadInventory()
}, [])
How does inventory loading work?
- The inventory state holds the product records returned by FastAPI.
- The quantities state holds the current input for each product SKU.
- The status state gives the operator immediate feedback about loading and connectivity.
- The useEffect call runs loadInventory() when the component first renders.
- Continue directly below the first chunk with the reservation logic and rendered dashboard:
async function reserve(sku) {
const quantity = Number(quantities[sku] || 1)
setStatus(`Reserving ${quantity} unit(s)...`)
try {
const response = await fetch(`${API_BASE}/api/reservations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sku, quantity }),
})
const body = await response.json()
if (!response.ok) throw new Error(body.detail || 'Reservation failed')
setInventory((items) => items.map((item) => (item.sku === sku ? body : item)))
setStatus(`Reserved ${quantity} unit(s) of ${body.name}`)
} catch (error) {
setStatus(error.message)
}
}
return <main className="shell"><header><div><p className="eyebrow">Operations dashboard</p><h1>StockGuard</h1><p>Reserve stock without overselling it.</p></div><span className={status === 'Connected' ? 'status online' : 'status'}>{status}</span></header><section className="grid" aria-label="Inventory">{inventory.map((item) => <article className="card" key={item.sku}><div><span className="sku">{item.sku}</span><h2>{item.name}</h2></div><strong>{item.quantity} available</strong><div className="controls"><input aria-label={`Quantity for ${item.name}`} min="1" type="number" value={quantities[item.sku] || 1} onChange={(event) => setQuantities({ ...quantities, [item.sku]: event.target.value })} /><button type="button" onClick={() => reserve(item.sku)}>Reserve</button></div></article>)}</section></main>
}
How do reservations update the page?
- The reserve() function sends the selected SKU and quantity to the reservation endpoint.
- A successful response replaces the matching inventory item with its returned quantity.
- A rejected response places the API detail in the status badge.
- The rendered cards map each product to its own quantity input and reservation button.
- Save frontend/src/App.jsx.
- Confirm the Explorer sidebar shows App.jsx beside main.jsx and styles.css.
Seeing a syntax marker in App.jsx?
- Confirm the first line imports both useEffect and useState.
- Confirm the second chunk sits inside App() before its final closing brace.
- Confirm every opening brace and parenthesis has a matching closing character.
Ask for help comparing the component structure.
Your frontend files are now assembled. Use the comparison view if you want to verify every file before connecting the web container.
✔️ Awesome, I've got everything!
Great. Confirm every frontend file is saved before you continue.
ⓧ I'd like to double check the full code
Compare each saved frontend file with the complete versions below.
{
"name": "stockguard-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "6.1.1",
"vite": "8.3.0"
}
}
Package manifest checkpoint
This file pins the frontend packages and exposes the Vite development command.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="StockGuard inventory reservation dashboard" />
<title>StockGuard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
HTML checkpoint
This page provides the root element and loads the React entry module.
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './styles.css'
createRoot(document.getElementById('root')).render(<App />)
Entry point checkpoint
This entry point imports the component and stylesheet before rendering StockGuard.
import { useEffect, useState } from 'react'
const API_BASE = 'http://localhost:8000'
export default function App() {
const [inventory, setInventory] = useState([])
const [quantities, setQuantities] = useState({})
const [status, setStatus] = useState('Loading inventory...')
async function loadInventory() {
try {
const response = await fetch(`${API_BASE}/api/inventory`)
if (!response.ok) throw new Error('Inventory request failed')
setInventory(await response.json())
setStatus('Connected')
} catch (error) {
setStatus(`Cannot reach API: ${error.message}`)
}
}
useEffect(() => {
loadInventory()
}, [])
async function reserve(sku) {
const quantity = Number(quantities[sku] || 1)
setStatus(`Reserving ${quantity} unit(s)...`)
try {
const response = await fetch(`${API_BASE}/api/reservations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sku, quantity }),
})
const body = await response.json()
if (!response.ok) throw new Error(body.detail || 'Reservation failed')
setInventory((items) => items.map((item) => (item.sku === sku ? body : item)))
setStatus(`Reserved ${quantity} unit(s) of ${body.name}`)
} catch (error) {
setStatus(error.message)
}
}
return <main className="shell"><header><div><p className="eyebrow">Operations dashboard</p><h1>StockGuard</h1><p>Reserve stock without overselling it.</p></div><span className={status === 'Connected' ? 'status online' : 'status'}>{status}</span></header><section className="grid" aria-label="Inventory">{inventory.map((item) => <article className="card" key={item.sku}><div><span className="sku">{item.sku}</span><h2>{item.name}</h2></div><strong>{item.quantity} available</strong><div className="controls"><input aria-label={`Quantity for ${item.name}`} min="1" type="number" value={quantities[item.sku] || 1} onChange={(event) => setQuantities({ ...quantities, [item.sku]: event.target.value })} /><button type="button" onClick={() => reserve(item.sku)}>Reserve</button></div></article>)}</section></main>
}
Component checkpoint
This component contains the complete loading flow and reservation flow. It also renders every inventory card.
:root { color: #eef2ff; background: #080b14; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
* { box-sizing: border-box; }
body { min-width: 320px; min-height: 100vh; margin: 0; background: radial-gradient(circle at top left, #25315f 0, transparent 38%), #080b14; }
button, input { font: inherit; }
.shell { width: min(1100px, calc(100% - 32px)); margin: 0 auto; padding: 64px 0; }
header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 32px; }
h1, h2, p { margin-top: 0; }
h1 { margin-bottom: 8px; font-size: clamp(2.5rem, 7vw, 5rem); letter-spacing: -0.06em; }
.eyebrow, .sku { color: #8ea2ff; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }
.status { max-width: 340px; padding: 10px 14px; border: 1px solid #56607d; border-radius: 999px; color: #fbbf24; background: #111827; }
.status.online { color: #86efac; border-color: #166534; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; }
.card { display: grid; gap: 24px; padding: 24px; border: 1px solid #28324b; border-radius: 20px; background: rgba(17, 24, 39, 0.88); box-shadow: 0 20px 70px rgba(0, 0, 0, 0.28); }
.card strong { color: #c7d2fe; font-size: 1.35rem; }
.controls { display: grid; grid-template-columns: 82px 1fr; gap: 10px; }
input, button { min-height: 44px; border-radius: 10px; }
input { width: 100%; border: 1px solid #44506f; padding: 0 12px; color: #eef2ff; background: #0b1020; }
button { border: 0; padding: 0 18px; color: #07111f; background: #9fb0ff; cursor: pointer; font-weight: 800; }
button:hover { background: #c7d2fe; }
@media (max-width: 640px) { header { flex-direction: column; } }
Stylesheet checkpoint
This stylesheet contains the complete responsive layout and visual states for the dashboard.
Test the full dashboard
Docker Compose already runs the database and API. The new web service adds a pinned Node.js container that installs the frontend packages and exposes Vite on port 5173.
- In compose.yaml locate the ports block at the end of the api service.
- Replace everything from that ports block through the existing volumes block with this code:
ports:
- "8000:8000"
web:
image: node:24.21.0-alpine3.24
working_dir: /app
depends_on:
- api
volumes:
- ./frontend:/app
- frontend-modules:/app/node_modules
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
ports:
- "5173:5173"
volumes:
postgres-data:
frontend-modules:
What does the web service add?
- The Node.js image supplies the runtime that Vite needs inside the container.
- The frontend bind mount makes your saved source files available at /app.
- The named module volume keeps installed packages outside the source folder.
- The port mapping makes the Vite server available to your browser on port 5173.
- Save compose.yaml.
YAML indentation looking uneven?
Align web with api and db. Keep the final volumes block aligned with services.
Ask for help checking the Compose structure.
✔️ Awesome, I've got everything!
Great. Your Compose configuration now includes db, api, and web.
ⓧ I'd like to double check the full code
Compare your saved compose.yaml with this complete version.
services:
db:
image: postgres:18.6-alpine3.24
restart: unless-stopped
environment:
POSTGRES_USER: stockguard
POSTGRES_PASSWORD: stockguard-local-password
POSTGRES_DB: stockguard
volumes:
- postgres-data:/var/lib/postgresql
api:
image: python:3.13.15-slim-trixie
working_dir: /app
restart: on-failure
depends_on:
- db
environment:
DATABASE_URL: postgresql://stockguard:stockguard-local-password@db:5432/stockguard
volumes:
- ./backend:/app
command: >-
sh -c "pip install --no-cache-dir -r requirements.txt &&
uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
ports:
- "8000:8000"
web:
image: node:24.21.0-alpine3.24
working_dir: /app
depends_on:
- api
volumes:
- ./frontend:/app
- frontend-modules:/app/node_modules
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
ports:
- "5173:5173"
volumes:
postgres-data:
frontend-modules:
Compose checkpoint
This complete configuration runs PostgreSQL and FastAPI alongside the new React development server.
The first web start can take a few minutes while the container downloads its image and installs the frontend packages. A quiet pause during that first start is normal.
Before you start the full stack, what status do you expect the dashboard to show after its first inventory request succeeds?
- Start the database and API with the new web service by running:
docker compose up
What does this command start?
Docker Compose reads the updated service definitions. It keeps db and api running while it creates the web container.
- Open a new browser tab at http://localhost:5173.
You should see the StockGuard heading above three inventory cards. The status changes from Loading inventory... to Connected after the API responds.
Dashboard missing or disconnected?
- Confirm the browser address uses port 5173.
- Confirm the Compose output includes activity from the web service.
- Confirm API_BASE in frontend/src/App.jsx equals http://localhost:8000.
Ask for help tracing the browser connection.
Before you reserve one unit, what should happen to that card without a page refresh?
- Enter 1 in the quantity input for the Mechanical Keyboard card.
- Click Reserve on the Mechanical Keyboard card.
You should see the Mechanical Keyboard quantity decrease by one. The status reports the completed reservation without reloading the browser.
That completes the interaction loop. Your React state now reflects the quantity returned by the atomic PostgreSQL update.
- Enter 999 in the quantity input for the USB-C Hub card.
- Click Reserve on the USB-C Hub card.
You should see Not enough stock in the status badge. The USB-C Hub quantity remains unchanged.
Reservation not updating the card?
- Confirm the status badge changes when you click Reserve.
- Confirm reserve() sends its request to /api/reservations.
- Confirm the successful response updates the item whose sku matches the returned record.
Ask for help tracing the reservation response.
Your local dashboard can now display stock and reserve units without a browser refresh. Next, you will package this interface with the API and deploy the combined application to Kubernetes.
Deploy to Amazon EKS
Your React dashboard now reserves stock through the local API. You have proved the complete application flow on your Linux workstation.
A public deployment tests whether every local assumption survives outside your computer. In this step, you will package StockGuard as a Docker image and deploy it with Amazon EKS Auto Mode.
In this step, get ready to:
- Package the React frontend with the FastAPI backend in one production image.
- Create the StockGuard cluster in AWS and publish the initial image to Amazon ECR.
- Deploy PostgreSQL with two application replicas behind a public load balancer.
Package the production image
The production container needs one server for both halves of StockGuard. FastAPI can serve the compiled frontend after its API routes when a static build exists.
- Return to backend/main.py in the Visual Studio Code tab from earlier.
- Find the current import block:
from contextlib import asynccontextmanager
import os
Why This Import Changes
The current imports can read environment variables. The production server also needs to construct the path to the compiled frontend.
- Add the Path import so the block looks like this:
from contextlib import asynccontextmanager
from pathlib import Path
import os
What Does Path Provide?
The Path class builds a filesystem location relative to main.py. This keeps the static directory location stable inside the container.
- Scroll below the final return row line.
- Add the static frontend mount with this code:
static_directory = Path(__file__).parent / "static"
if static_directory.exists():
app.mount("/", StaticFiles(directory=static_directory, html=True), name="frontend")
What Does the Static Mount Do?
- The static_directory value points to the compiled frontend beside main.py.
- The existence check keeps the local API usable when the compiled frontend is absent.
- The mount comes after every /api route. API requests therefore reach FastAPI before the frontend fallback.
- Save backend/main.py.
- Confirm the edited API still starts by running this command in the terminal from earlier:
docker compose up db api
What Does This Check Prove?
Docker Compose starts PostgreSQL before the API. A clean startup proves the new import and static mount contain no Python errors.
You should see the API start without a traceback. The existing API routes remain available on http://localhost:8000.
- Stop the foreground logs by pressing Ctrl+C.
Does the API Stop During Startup?
- Check that Path is imported from pathlib.
- Check that the static mount appears after return row.
- Confirm the indentation beneath if static_directory.exists(): uses four spaces.
Help me diagnose the backend startup error.
✔️ The API still starts
Your backend is ready to serve the compiled frontend when the production image supplies it.
ⓧ I'd like to double check the full code
Compare your saved backend/main.py with this cumulative version:
from contextlib import asynccontextmanager
from pathlib import Path
import os
import psycopg
from psycopg.rows import dict_row
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
DATABASE_URL = os.environ["DATABASE_URL"]
def connect():
return psycopg.connect(DATABASE_URL, row_factory=dict_row)
def initialize_database():
with connect() as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS inventory (
sku TEXT PRIMARY KEY,
name TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity >= 0)
)
"""
)
connection.executemany(
"""
INSERT INTO inventory (sku, name, quantity)
VALUES (%s, %s, %s)
ON CONFLICT (sku) DO NOTHING
""",
[
("KB-101", "Mechanical Keyboard", 12),
("MS-204", "Wireless Mouse", 18),
("HD-330", "USB-C Hub", 8),
],
)
@asynccontextmanager
async def lifespan(app: FastAPI):
initialize_database()
yield
app = FastAPI(title="StockGuard API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["GET", "POST"],
allow_headers=["content-type"],
)
class Reservation(BaseModel):
sku: str
quantity: int
@app.get("/api/health")
def health():
return {"status": "ok"}
@app.get("/api/inventory")
def inventory():
with connect() as connection:
return connection.execute(
"SELECT sku, name, quantity FROM inventory ORDER BY sku"
).fetchall()
@app.post("/api/reservations")
def reserve(request: Reservation):
if request.quantity < 1:
raise HTTPException(status_code=400, detail="Quantity must be positive")
with connect() as connection:
row = connection.execute(
"""
UPDATE inventory
SET quantity = quantity - %s
WHERE sku = %s AND quantity >= %s
RETURNING sku, name, quantity
""",
(request.quantity, request.sku, request.quantity),
).fetchone()
if row is None:
raise HTTPException(status_code=409, detail="Not enough stock")
return row
static_directory = Path(__file__).parent / "static"
if static_directory.exists():
app.mount("/", StaticFiles(directory=static_directory, html=True), name="frontend")
A multi-stage build compiles the Vite frontend in a Node.js image. The final stage copies only the build output into the Python runtime.
- In the Visual Studio Code Explorer sidebar, click the New File icon beside the repository name.
- Enter Dockerfile as the filename.
- Add the frontend build stage with this code:
FROM node:24.21.0-alpine3.24 AS frontend-build
WORKDIR /frontend
COPY frontend/package.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build
How Does the Frontend Stage Work?
- The pinned Node.js image keeps the frontend build environment consistent.
- The package file is copied before the source so Docker can reuse the dependency layer.
- The final command compiles the Vite application into frontend/dist.
- Save Dockerfile.
- Build the frontend stage into a temporary local image by running:
docker build -t stockguard:local .
What Does This Build Check?
Docker reads the current build stage and runs the production frontend build. A successful result proves the React source compiles inside the pinned Node.js image.
The first build can take a few minutes while Docker downloads the image layers. You should see the frontend build complete without a Vite compilation error.
Does the Frontend Build Fail?
- Confirm frontend/package.json exists inside the repository.
- Confirm the filename is exactly Dockerfile.
- Check that the final period is present in the build command.
Help me troubleshoot the StockGuard frontend image build.
- Return to Dockerfile.
- Append the production runtime stage with this code:
FROM python:3.13.15-slim-trixie
WORKDIR /app
COPY backend/requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/main.py ./
COPY --from=frontend-build /frontend/dist ./static
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
How Does the Runtime Stage Work?
- The Python image contains the FastAPI runtime without the Node.js build toolchain.
- The backend dependencies install from requirements.txt.
- The compiled frontend enters the static directory that main.py now mounts.
- Uvicorn listens on port 8000 for both API and frontend requests.
- Save Dockerfile.
- Build the complete production image by running:
docker build -t stockguard:local .
What Does the Final Build Produce?
Docker rebuilds the frontend stage when needed. It then creates a smaller runtime image containing FastAPI and the compiled dashboard.
You should see both stages complete successfully. The local image is now tagged as stockguard:local.
✔️ The production image builds
Your frontend and backend now travel together as one image.
ⓧ I'd like to double check the full code
Compare your saved Dockerfile with this complete version:
FROM node:24.21.0-alpine3.24 AS frontend-build
WORKDIR /frontend
COPY frontend/package.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build
FROM python:3.13.15-slim-trixie
WORKDIR /app
COPY backend/requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/main.py ./
COPY --from=frontend-build /frontend/dist ./static
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
A .dockerignore file keeps repository metadata and local build output outside the image context. This reduces unnecessary uploads to the Docker daemon.
- In the Visual Studio Code Explorer sidebar, click the New File icon beside the repository name.
- Enter .dockerignore as the filename.
- Add the ignored paths with this code:
.git
.github
frontend/node_modules
frontend/dist
__pycache__
*.pyc
Why Ignore These Paths?
Git history does not belong in an application image. Locally generated dependencies and build output are recreated inside the Docker build.
- Save .dockerignore.
- Confirm the hidden file exists by running:
ls -a
Why Use the All-Files View?
Linux hides filenames that begin with a period during a normal directory listing. The -a option includes those files.
You should see .dockerignore beside Dockerfile in the output.
✔️ The build files are ready
The repository now contains the production image recipe and its build exclusions.
ⓧ I'd like to double check the full code
Compare your saved .dockerignore with this complete version:
.git
.github
frontend/node_modules
frontend/dist
__pycache__
*.pyc
Create the cluster and publish the image
The cluster configuration records the deployment region and enables managed infrastructure. The same file also gives cleanup a repeatable way to identify the StockGuard cluster.
- In the Visual Studio Code Explorer sidebar, click the New File icon beside the repository name.
- Enter cluster-config.yaml as the filename.
- Add the EKS Auto Mode configuration with this code:
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: stockguard
region: us-east-1
autoModeConfig:
enabled: true
What Does This Configuration Define?
- The metadata names the cluster stockguard.
- The region places the cluster in us-east-1.
- The Auto Mode setting asks EKS to manage compute and supporting cluster infrastructure.
- Save cluster-config.yaml.
✔️ The cluster file is saved
Your cluster definition is ready for eksctl.
ⓧ I'd like to double check the full code
Compare your saved cluster-config.yaml with this complete version:
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: stockguard
region: us-east-1
autoModeConfig:
enabled: true
AWS Charges Start Here
Creating this cluster starts usage-based AWS charges. The cleanup section removes the cluster and its supporting resources when you finish.
Cluster creation is the slowest command in this step. Expect it to run for several minutes while AWS provisions the control plane and networking.
- Create the StockGuard cluster from the saved configuration by running:
eksctl create cluster -f cluster-config.yaml
What Does eksctl Create?
The command sends the saved cluster definition to AWS. It also configures kubectl access when cluster creation succeeds.
You should see the cluster creation process finish successfully. Your current Kubernetes context should now target the stockguard cluster.
Does Cluster Creation Fail?
- Confirm your AWS identity has permission to create EKS and supporting resources.
- Confirm the region in cluster-config.yaml is us-east-1.
- Read the final failed task in the eksctl output before retrying.
Help me diagnose the EKS cluster creation failure.
The local image needs a registry that the cluster can reach. Amazon ECR stores the initial StockGuard image inside your AWS account.
- Create the stockguard repository in us-east-1 by running:
aws ecr create-repository --repository-name stockguard --region us-east-1
What Does This Command Create?
Amazon ECR creates a private repository named stockguard in the same region as the cluster. The repository becomes the destination for the production image.
You should see repository details returned for stockguard.
The first registry push can take a few minutes because Docker uploads each image layer. Later pushes can reuse unchanged layers.
- Build the initial ECR image and push it by running these commands in the same terminal:
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com"
IMAGE_URI="$ECR_REGISTRY/stockguard:initial"
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin "$ECR_REGISTRY"
docker build -t "$IMAGE_URI" .
docker push "$IMAGE_URI"
How Does the Image Reach ECR?
- The account lookup builds the private registry address for your AWS account.
- The IMAGE_URI value gives this release the initial tag.
- The login command grants Docker temporary access to the private registry.
- The final commands build the production image and upload its layers.
You should see the upload complete for the initial image tag. Keep this terminal open so IMAGE_URI remains available for the deployment.
Does the ECR Push Fail?
- Confirm the repository creation command returned details for stockguard.
- Confirm Docker is running before repeating the login command.
- Confirm your AWS identity can authenticate to ECR and push image layers.
Help me troubleshoot the StockGuard ECR push.
Deploy the Kubernetes workloads
A Kubernetes manifest describes the desired database and application state. The cluster then keeps those workloads aligned with the file.
The database credentials belong in a Kubernetes Secret outside source control. Use a fresh URL-safe demonstration password so the same value works inside the PostgreSQL connection address.
- Replace both occurrences of your-url-safe-password-here with the same fresh demonstration password.
- Create the stockguard-db Secret by running:
kubectl create secret generic stockguard-db --from-literal=POSTGRES_USER=stockguard --from-literal=POSTGRES_PASSWORD=your-url-safe-password-here --from-literal=POSTGRES_DB=stockguard --from-literal=DATABASE_URL=postgresql://stockguard:your-url-safe-password-here@postgres:5432/stockguard
What Does the Secret Contain?
The Secret gives PostgreSQL its user and database settings. It also gives the application a connection address that resolves the in-cluster postgres Service.
You should see confirmation that the stockguard-db Secret was created.
Does the Secret Already Exist?
A duplicate-name response means the cluster already has a Secret named stockguard-db. Remove the stale lab Secret only if you created it during this project.
Help me resolve the Kubernetes Secret creation problem.
The database begins with a headless Service. Its stable name lets the application connect to PostgreSQL inside the cluster.
- In the Visual Studio Code Explorer sidebar, return to the k8s folder from earlier.
- Click the New File icon beside the k8s folder.
- Enter workloads.yaml as the filename.
- Add the PostgreSQL Service with this code:
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
clusterIP: None
selector:
app: postgres
ports:
- name: postgres
port: 5432
targetPort: 5432
Why Use a Headless Service?
The clusterIP: None setting creates stable internal discovery without a public address. The API can reach the database through the name postgres.
- Save k8s/workloads.yaml.
- Create the internal PostgreSQL Service by running:
kubectl apply -f k8s/workloads.yaml
What Does Apply Do Here?
Kubectl sends the desired Service definition to the cluster. The Service becomes the stable network identity for the database workload added next.
You should see confirmation that the postgres Service was created.
- Return to the bottom of k8s/workloads.yaml.
- Append the first StatefulSet section with this code:
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:18.6-alpine3.24
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: stockguard-db
volumeMounts:
- name: data
mountPath: /var/lib/postgresql
How Is PostgreSQL Configured?
- The StatefulSet maintains one PostgreSQL Pod with a stable workload identity.
- The container reads its environment from stockguard-db.
- The data mount uses the PostgreSQL 18.6 storage path.
- Append the persistent volume claim section with this code:
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
Why Request Persistent Storage?
The StatefulSet requests a 5Gi volume with ReadWriteOnce access. Inventory data can therefore survive replacement of the PostgreSQL Pod.
- Save k8s/workloads.yaml.
- Create the PostgreSQL StatefulSet by running:
kubectl apply -f k8s/workloads.yaml
What Changes in the Cluster?
Kubectl keeps the existing Service and adds the StatefulSet. The cluster also provisions the requested persistent volume claim.
Storage provisioning and the first PostgreSQL image pull can take several minutes. The Pod may show a pending or starting state during that work.
- Check the PostgreSQL Pod state by running:
kubectl get pods
What Should the Pod List Show?
The Pod list shows the current readiness and lifecycle state for each workload. Repeat the command until the PostgreSQL Pod is running.
You should eventually see one PostgreSQL Pod in the Running state.
Is PostgreSQL Still Pending?
- Allow time for EKS Auto Mode to provision compute for the first Pod.
- Confirm the stockguard-db Secret exists in the current cluster.
- Inspect the Pod events if the state stops changing.
Help me diagnose the PostgreSQL Pod state.
The application Deployment keeps two StockGuard replicas available. Readiness probes prevent traffic from reaching a new Pod until its health endpoint responds.
- Return to the bottom of k8s/workloads.yaml.
- Append the first Deployment section with this code:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: stockguard
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: stockguard
template:
metadata:
labels:
app: stockguard
spec:
containers:
- name: stockguard
image: stockguard:local
imagePullPolicy: Always
How Does the Deployment Protect Availability?
- The replica count asks Kubernetes to maintain two StockGuard Pods.
- The rolling strategy keeps every current Pod available during an update.
- The surge setting permits one extra Pod while a replacement becomes ready.
- The source manifest keeps stockguard:local as a reusable image placeholder.
- Append the container settings with this code:
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: stockguard-db
key: DATABASE_URL
readinessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
What Keeps Each Replica Healthy?
- The application reads DATABASE_URL from the Kubernetes Secret.
- The readiness probe controls when the Pod can receive traffic.
- The liveness probe lets Kubernetes restart an unhealthy container.
- The resource settings give Auto Mode information for scheduling the Pods.
- Save k8s/workloads.yaml.
- Validate the manifest locally without changing the cluster by running:
kubectl apply -f k8s/workloads.yaml --dry-run=client
Why Use a Client Dry Run?
The client dry run processes the YAML without sending the new Deployment to the cluster. This catches malformed manifest structure before the public Service is added.
You should see the manifest objects listed without a YAML parsing error.
- Return to the bottom of k8s/workloads.yaml.
- Append the public StockGuard Service with this code:
---
apiVersion: v1
kind: Service
metadata:
name: stockguard
spec:
type: LoadBalancer
selector:
app: stockguard
ports:
- protocol: TCP
port: 80
targetPort: 8000
How Does the Public Service Route Traffic?
The LoadBalancer Service requests a public AWS load balancer. Port 80 forwards browser traffic to port 8000 on ready StockGuard Pods.
- Save k8s/workloads.yaml.
- Apply the complete stack and switch the live Deployment to the ECR image by running:
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
IMAGE_URI="$AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/stockguard:initial"
kubectl apply -f k8s/workloads.yaml
kubectl set image deployment/stockguard stockguard="$IMAGE_URI"
How Does the Live Image Replace the Placeholder?
The apply command creates the Deployment and public Service from the manifest. The image update immediately replaces the live Pod template with the image stored in ECR.
The saved manifest retains stockguard:local as its placeholder. The cluster now records the full ECR image URI for the running revision.
Before you check the Pods, how many StockGuard replicas do you expect to become ready beside PostgreSQL?
- Check the complete workload state by running:
kubectl get pods
What Does a Healthy Stack Look Like?
The Pod list should contain one PostgreSQL Pod and two StockGuard Pods. Each running application Pod serves the same combined frontend and API image.
Repeat the command until the PostgreSQL Pod and both StockGuard Pods show Running.
Are the StockGuard Pods Unhealthy?
- Confirm the image push completed before the Deployment update.
- Confirm DATABASE_URL exists inside the stockguard-db Secret.
- Inspect the failing Pod events for image pull or health probe details.
Help me diagnose the StockGuard Pod failure.
The load balancer needs time to receive a public address after the Service is created. Expect the external address to remain pending for several minutes.
- Retrieve the StockGuard Service address by running:
kubectl get service stockguard
What Does the Service Output Show?
The Service output shows its public address after AWS finishes provisioning the load balancer. Repeat the command while the external address is still pending.
- Record the external address here: your StockGuard load balancer address.
Before you open the public address, do you expect every part of the locally successful dashboard to work from a cloud URL?
- Enter http:// followed by your StockGuard load balancer address in your browser address bar.
The StockGuard React shell loads from Amazon EKS. Its status begins with Cannot reach API: because the compiled browser code still targets http://localhost:8000 on your own computer.
This Failure Is the Evidence
The load balancer and production container are working because the browser received the React shell. The failed inventory request exposes the development-only address that survived inside the compiled frontend.
You have now reproduced a real environment-specific configuration failure. The public deployment gives you a concrete signal to resolve in the next release.
✔️ The public shell shows the API error
Your cluster, image, database workload, replicas, and public load balancer are all in place.
ⓧ I'd like to double check the full code
Compare your saved k8s/workloads.yaml with this complete version:
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
clusterIP: None
selector:
app: postgres
ports:
- name: postgres
port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:18.6-alpine3.24
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: stockguard-db
volumeMounts:
- name: data
mountPath: /var/lib/postgresql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: stockguard
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: stockguard
template:
metadata:
labels:
app: stockguard
spec:
containers:
- name: stockguard
image: stockguard:local
imagePullPolicy: Always
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: stockguard-db
key: DATABASE_URL
readinessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: stockguard
spec:
type: LoadBalancer
selector:
app: stockguard
ports:
- protocol: TCP
port: 80
targetPort: 8000
Your production infrastructure is live, and the development address has exposed its limit. Next, you will turn that visible failure into a working automated release.
Automate Production Delivery
Your public StockGuard shell is live on Amazon EKS. Its browser requests still point to localhost.
This step replaces that hardcoded address with same-origin API requests. GitHub Actions will release the fix through OIDC. Each release receives temporary AWS credentials before publishing an immutable image to Amazon ECR.
In this step, get ready to:
- Authorize GitHub Actions to deploy StockGuard with temporary credentials.
- Make the frontend work in local development and behind the public load balancer.
- Publish an immutable image through the pipeline and verify the rolling update.
Create the deployment identity
GitHub OIDC lets a workflow exchange its signed job token for temporary credentials. The deployment role trusts only the subject for your StockGuard repository on the main branch.
This setup changes cloud access, so take each condition one at a time. The role can be assumed only when its audience and repository subject both match.
- Open GitHub's Get a repository reference.
- Use the reference page's request tool to retrieve metadata for the stockguard repository.
The response identifies the repository plus its owner. These values let you construct the immutable OIDC subject used by repositories created after July 15, 2026.
- Record the value of owner.login as your GitHub owner.
- Record the value of owner.id as your GitHub owner ID.
- Record the top-level id value as your stockguard repository ID.
- Construct the subject with repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/BRANCH as the format.
- Use your recorded owner values for OWNER plus OWNER-ID.
- Use stockguard for REPO.
- Use your recorded repository ID for REPO-ID.
- Use main for BRANCH.
Record the completed value as your exact GitHub OIDC subject. It should identify one repository plus one branch.
Why Use the Immutable Subject?
The immutable format includes the numeric owner ID plus the numeric repository ID. A future rename or recycled repository name cannot silently inherit this role's trust.
- Return to the AWS console from earlier.
- In IAM, check whether the GitHub OIDC provider already exists.
An AWS account needs only one provider for GitHub's token issuer. Reuse the provider when its URL and audience already match the values below.
- Keep the existing provider when its URL is https://token.actions.githubusercontent.com.
- Confirm that the existing provider includes the audience sts.amazonaws.com.
- Add an OIDC provider when no matching provider exists.
- Enter https://token.actions.githubusercontent.com as the provider URL.
- Enter sts.amazonaws.com as the audience.
You should now see GitHub's token issuer in the IAM identity provider list. That confirms AWS can validate tokens presented by GitHub Actions.
Keep the Deployment Policy Narrow
- Grant ecr:GetAuthorizationToken against * so the role can authenticate to the registry.
- Grant ecr:CompleteLayerUpload, ecr:UploadLayerPart, ecr:InitiateLayerUpload, ecr:BatchCheckLayerAvailability, ecr:PutImage, and ecr:BatchGetImage against only the StockGuard repository ARN.
- Grant eks:DescribeCluster against only the StockGuard cluster ARN.
- Create an IAM role for web identity federation.
- Select the GitHub OIDC provider.
- Set the audience trust condition to sts.amazonaws.com.
- Set token.actions.githubusercontent.com:aud to sts.amazonaws.com in the trust policy.
- Set token.actions.githubusercontent.com:sub to your exact GitHub OIDC subject in the trust policy.
- Attach the narrow deployment permissions listed above.
- Save the deployment role.
- Record its ARN as your GitHub deployment role ARN.
The saved role should show GitHub as its trusted identity provider. Its permission policy should reference only the StockGuard ECR repository plus the StockGuard EKS cluster.
- Return to the stockguard cluster in Amazon EKS.
- Create an access entry for your GitHub deployment role ARN.
- Associate AmazonEKSEditPolicy with the access entry.
- Scope the access policy to the namespace containing the StockGuard deployment.
The cluster access entries should now list your deployment role. That entry gives the workflow permission to update the existing deployment.
- Return to the settings for your GitHub stockguard repository.
- Create a GitHub Actions repository secret named AWS_ROLE_ARN.
- Set the secret value to your GitHub deployment role ARN.
- Save the repository secret.
GitHub should list AWS_ROLE_ARN with a masked value. You have stored a role reference without storing a long-lived AWS access key.
Unable to Save the Role Trust?
- Confirm the provider URL is exactly https://token.actions.githubusercontent.com.
- Confirm the audience is exactly sts.amazonaws.com.
- Check that the subject ends with :ref:refs/heads/main.
Ask for help with the trust conditions: Why is my GitHub OIDC role trust policy failing?
Fix the API path and add the workflow
A same-origin request follows the address that served the page. This lets the compiled React dashboard call FastAPI through the same public load balancer.
- Switch back to frontend/src/App.jsx in your editor.
- Find the API_BASE declaration near the top of the file.
- Replace the current declaration with this line:
const API_BASE = ''
What Does This Change Do?
An empty API_BASE makes requests such as /api/inventory relative to the page's current origin. The public dashboard now sends API traffic back through its load balancer.
- Save frontend/src/App.jsx.
- Inspect the file in your editor's source-control view.
You should see frontend/src/App.jsx listed as modified. Its only required production-address change is the API_BASE value.
✔️ Awesome, I've got everything!
Great. Your frontend now uses the origin that served the page.
ⓧ I'd like to double check the full code
import { useEffect, useState } from 'react'
const API_BASE = ''
export default function App() {
const [inventory, setInventory] = useState([])
const [quantities, setQuantities] = useState({})
const [status, setStatus] = useState('Loading inventory...')
async function loadInventory() {
try {
const response = await fetch(`${API_BASE}/api/inventory`)
if (!response.ok) throw new Error('Inventory request failed')
setInventory(await response.json())
setStatus('Connected')
} catch (error) {
setStatus(`Cannot reach API: ${error.message}`)
}
}
useEffect(() => { loadInventory() }, [])
async function reserve(sku) {
const quantity = Number(quantities[sku] || 1)
setStatus(`Reserving ${quantity} unit(s)...`)
try {
const response = await fetch(`${API_BASE}/api/reservations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sku, quantity }) })
const body = await response.json()
if (!response.ok) throw new Error(body.detail || 'Reservation failed')
setInventory((items) => items.map((item) => (item.sku === sku ? body : item)))
setStatus(`Reserved ${quantity} unit(s) of ${body.name}`)
} catch (error) { setStatus(error.message) }
}
return <main className="shell"><header><div><p className="eyebrow">Operations dashboard</p><h1>StockGuard</h1><p>Reserve stock without overselling it.</p></div><span className={status === 'Connected' ? 'status online' : 'status'}>{status}</span></header><section className="grid" aria-label="Inventory">{inventory.map((item) => <article className="card" key={item.sku}><div><span className="sku">{item.sku}</span><h2>{item.name}</h2></div><strong>{item.quantity} available</strong><div className="controls"><input aria-label={`Quantity for ${item.name}`} min="1" type="number" value={quantities[item.sku] || 1} onChange={(event) => setQuantities({ ...quantities, [item.sku]: event.target.value })} /><button type="button" onClick={() => reserve(item.sku)}>Reserve</button></div></article>)}</section></main>
}
What Should You Compare?
Confirm that API_BASE is an empty string. Keep the inventory loading plus reservation logic unchanged.
Still Seeing the Localhost Address?
- Search frontend/src/App.jsx for localhost.
- Confirm the API_BASE declaration contains two quotes with no value between them.
Ask for help with the edit: Why is my React app still requesting localhost?
The empty base works in production because FastAPI serves both the frontend and the API. The local Vite server needs a proxy that forwards the same paths to the API container.
- In the editor's Explorer, select the frontend folder.
- Create vite.config.js inside the frontend folder.
- Add the development proxy by pasting this code:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://api:8000',
},
},
})
How Does the Proxy Help?
- The React plugin lets Vite process the existing JSX files.
- The development server sends every /api request to the Compose service at http://api:8000.
- The production build keeps using relative paths because this proxy applies only during local development.
- Save frontend/vite.config.js.
- Confirm vite.config.js appears inside the editor's frontend folder.
✔️ Awesome, I've got everything!
Your frontend now has one API path that works locally plus in production.
ⓧ I'd like to double check the full code
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://api:8000',
},
},
})
What Should You Compare?
Confirm that the proxy key is /api. Confirm that its destination is http://api:8000.
Proxy File in the Wrong Folder?
- Confirm the file path is exactly frontend/vite.config.js.
- Confirm the file is beside frontend/package.json.
Ask for help with the file location: Why is Vite ignoring my API proxy?
The release workflow turns each push to main into a new container image. A timestamp tag gives every release a distinct image reference.
- In the editor's Explorer, select the existing .github/workflows folder.
- Create deploy.yml inside .github/workflows.
- Add the workflow trigger plus shared AWS values by pasting this first section:
name: Deploy StockGuard
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: stockguard
EKS_CLUSTER: stockguard
What Does the Workflow Header Do?
- The push trigger limits automatic releases to the main branch.
- The id-token: write permission allows the job to request an OIDC token.
- The environment values point every step at the existing repository plus cluster in us-east-1.
- Save .github/workflows/deploy.yml.
- Confirm the editor lists deploy.yml under .github/workflows.
Workflow File Not Detected?
- Confirm the complete path is .github/workflows/deploy.yml.
- Check that on, permissions, and env begin at the left edge.
Ask for help with the workflow path: Why is GitHub not detecting deploy.yml?
- Add the deployment job directly below the existing workflow header:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
How Does the Job Authenticate?
- The job runs on a GitHub-hosted Ubuntu runner.
- The checkout action places the repository files on that runner.
- The AWS credentials action exchanges the job's OIDC token for temporary role credentials.
- The workflow reads only the role ARN from AWS_ROLE_ARN.
- Save .github/workflows/deploy.yml.
- Inspect the YAML indentation around the two workflow steps.
You should see both step names aligned beneath steps. Their uses fields should align at the same indentation depth.
Credentials Step Misaligned?
- Place both step names at the same indentation depth.
- Place role-to-assume plus aws-region beneath with.
Ask for help with the YAML: Why is my AWS credentials step invalid?
- Add the immutable image build step directly below the credentials step:
- name: Build and push immutable image
shell: bash
run: |
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
IMAGE_TAG=$(date +%s)
ECR_REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
IMAGE_URI="$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$ECR_REGISTRY"
docker build -t "$IMAGE_URI" .
docker push "$IMAGE_URI"
echo "IMAGE_URI=$IMAGE_URI" >> "$GITHUB_ENV"
How Is the Image Published?
- The timestamp creates a unique IMAGE_TAG for each run.
- The account plus region produce the private ECR registry address.
- The Docker commands build the production image before pushing it to StockGuard's repository.
- The final line makes IMAGE_URI available to the deployment step.
- Save .github/workflows/deploy.yml.
- Confirm each shell command is indented beneath run.
The build step should contain one image tag plus one image URI. The URI is written to the workflow environment after the push succeeds.
Build Commands Outside the Run Block?
- Confirm run ends with a vertical bar.
- Indent every shell command two spaces farther than run.
Ask for help with the shell block: Why is my image build step failing YAML validation?
- Add the Amazon EKS deployment step directly below the image build step:
- name: Deploy to Amazon EKS
shell: bash
run: |
aws eks update-kubeconfig --name "$EKS_CLUSTER" --region "$AWS_REGION"
kubectl set image deployment/stockguard stockguard="$IMAGE_URI"
kubectl rollout status deployment/stockguard
How Does the Rollout Work?
- The kubeconfig command connects the runner to the existing cluster.
- The image update changes the StockGuard Pod template to the immutable ECR image.
- The rollout command keeps the job active until Kubernetes reports the deployment outcome.
- Save .github/workflows/deploy.yml.
- Inspect the complete file for consistent space-based indentation.
Your workflow now has four named steps. The final two steps publish the image plus deploy its exact URI.
✔️ Awesome, I've got everything!
Your production workflow is ready to run when the changes reach main.
ⓧ I'd like to double check the full code
name: Deploy StockGuard
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: stockguard
EKS_CLUSTER: stockguard
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Build and push immutable image
shell: bash
run: |
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
IMAGE_TAG=$(date +%s)
ECR_REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
IMAGE_URI="$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$ECR_REGISTRY"
docker build -t "$IMAGE_URI" .
docker push "$IMAGE_URI"
echo "IMAGE_URI=$IMAGE_URI" >> "$GITHUB_ENV"
- name: Deploy to Amazon EKS
shell: bash
run: |
aws eks update-kubeconfig --name "$EKS_CLUSTER" --region "$AWS_REGION"
kubectl set image deployment/stockguard stockguard="$IMAGE_URI"
kubectl rollout status deployment/stockguard
What Should You Compare?
Confirm the workflow targets main plus us-east-1. Confirm the final step updates deployment/stockguard with IMAGE_URI.
Push the fix and watch it roll out
A Kubernetes rolling update starts new Pods before retiring the old ones. StockGuard's settings allow one extra Pod while keeping every existing replica available.
Before you push, predict what the Pod list will do when the deployment receives a new image. Watch for old plus new Pods to overlap briefly.
- Watch Pod status changes in the current namespace by running this command:
kubectl get pods --watch
What Does Watch Mode Show?
The watch flag keeps the Pod list open as statuses change. This gives you a live view of the rollout instead of a single snapshot.
- Keep the Pod watch running in this terminal.
- Use a second terminal or your existing Git workflow to stage all StockGuard source changes.
- Commit the production fix plus deployment workflow.
- Push the commit to the main branch.
- Open the new run in GitHub Actions.
The image build can take several minutes because the runner downloads dependencies before publishing the image. The Pod watch should show a new StockGuard Pod becoming ready before an old Pod terminates.
- Wait for all four GitHub Actions steps to complete successfully.
- Stop the Pod watch with Ctrl+C.
Workflow Failed During AWS Authentication?
- Confirm the repository secret is named exactly AWS_ROLE_ARN.
- Confirm the role trust uses the exact subject for the StockGuard repository.
- Confirm the trust policy checks the audience sts.amazonaws.com.
Ask for help with the authentication failure: Why can GitHub Actions not assume my AWS role?
Before you run the final deployment check, do you expect Kubernetes to report an active rollout or a completed rollout?
- Verify the StockGuard rollout by running this command:
kubectl rollout status deployment/stockguard
What Does This Check Prove?
The command watches the deployment until Kubernetes reaches its rollout result. A completed rollout confirms the new image became ready without taking the deployment offline.
You should see confirmation that the rollout completed. The Pod watch should finish with two ready StockGuard replicas running the new image.
- Return to the public StockGuard dashboard from earlier.
- Refresh the page.
- Confirm the status reads Connected.
- Enter 1 in an inventory card's quantity field.
- Click Reserve.
You should see the selected item's available quantity decrease without a page refresh. The status should confirm the successful reservation.
Dashboard Still Cannot Reach the API?
- Confirm the successful workflow run deployed the image URI created during that same run.
- Confirm frontend/src/App.jsx defines API_BASE as an empty string.
- Confirm both StockGuard replicas became ready after the rollout.
Ask for help with the production request: Why can my deployed dashboard still not reach FastAPI?
That completes the delivery path. A push to main now publishes an immutable image before rolling it across two healthy replicas.
Secret mission
Prove the Deployment Can Recover
Deliberately point StockGuard at an image tag that does not exist. Diagnose the stalled rollout. Restore the working revision with Kubernetes rollback.
Clean Up Your Resources
Clean Up Your Resources
StockGuard still has paid AWS resources running. Choose between keeping the deployment active or pausing its workloads. You can also delete the cloud footprint.
Cost warning
The standard-support control plane for Amazon EKS Auto Mode starts at $0.10 per cluster-hour. Auto Mode management adds separate charges.
Compute usage adds charges. Amazon EBS storage adds charges.
Load balancing adds charges. Public IPv4 usage adds charges.
Amazon ECR image storage adds charges. Data transfer can add charges. Choose Delete if you do not intend to keep paying for the lab.
Resources you used:
- The stockguard Amazon EKS cluster with Auto Mode compute plus supporting networking resources.
- A public Kubernetes LoadBalancer Service plus the StockGuard and PostgreSQL workloads.
- A persistent 5Gi Amazon EBS volume containing the PostgreSQL data.
- The stockguard Amazon ECR repository with the initial image plus timestamp-tagged images.
- A GitHub Actions deployment role in AWS IAM plus its EKS access entry.
- A GitHub OIDC provider if you created one exclusively for StockGuard.
Keep everything running
No action needed. Choose this if you are still demonstrating the public StockGuard dashboard or testing future releases.
The recovered three-replica deployment stays available. AWS billing continues for the cluster plus its supporting resources.
Pause - I'll come back to this later
Shut down the application workloads to reduce compute use while keeping the cloud infrastructure for later.
- Use kubectl from a terminal configured for the stockguard cluster to scale deployment/stockguard to zero replicas.
- Use kubectl to scale the PostgreSQL StatefulSet named postgres to zero replicas.
- Confirm that no StockGuard Pods remain running.
- Confirm that no PostgreSQL Pod remains running.
Your persistent database volume remains available. Your Amazon ECR images remain available.
The EKS control plane plus retained storage still incur charges. The public load balancer can also continue to incur charges.
Delete - I don't want to use this again
Deleting the cloud infrastructure is permanent. Your committed GitHub source remains available if you decide to rebuild StockGuard later.
- Delete the Kubernetes LoadBalancer Service named stockguard with kubectl.
- Wait until its external address has been removed from AWS.
- Delete the EKS cluster by running this command from the folder containing cluster-config.yaml:
eksctl delete cluster -f cluster-config.yaml --wait
What does this command remove?
The command deletes the cluster described by cluster-config.yaml. The --wait flag keeps the command active until deletion finishes.
The Kubernetes workloads plus the EKS access entry belong to the cluster. Deleting the cluster removes those cluster-scoped resources.
Cluster deletion failed?
Keep the reported failure visible in your terminal. Help me diagnose why my StockGuard EKS cluster deletion failed..
The cluster command handles the Kubernetes objects. The remaining account-level resources need separate cleanup.
- Check Amazon EBS for the 5Gi volume that backed PostgreSQL.
- Delete that volume if it remains after the cluster deletion.
- Select repository stockguard in Amazon ECR.
- Delete repository stockguard with all its stored images.
- Locate the GitHub Actions deployment role used by StockGuard in AWS IAM.
- Delete the StockGuard deployment role.
An OIDC provider can be shared by several repositories. Its cleanup depends on how you configured the AWS account.
- Delete the GitHub OIDC provider only if you created it exclusively for StockGuard.
- Keep the provider if it existed before this project or supports another repository.
The teardown is complete when each StockGuard cloud resource is gone.
- Confirm that cluster stockguard no longer appears in Amazon EKS.
- Confirm that the StockGuard external load balancer no longer appears in AWS.
- Confirm that the PostgreSQL Amazon EBS volume no longer appears in AWS.
- Confirm that repository stockguard no longer appears in Amazon ECR.
- Confirm that the StockGuard deployment role no longer appears in AWS IAM.
That is the costly cloud footprint cleared. Your committed StockGuard source stays in GitHub for future reference.
Nice Work!
Nice Work!
You did it! StockGuard now reserves inventory safely through a public React dashboard running on Amazon EKS.
You've learned how to:
- Build a transaction-safe FastAPI API backed by PostgreSQL. Oversized reservations now fail before stock becomes negative.
- Package the frontend with the backend in one multi-stage Docker image. Docker carries the same application from local development to three healthy cloud replicas.
- Automate releases with GitHub Actions. AWS OIDC supplies short-lived cloud credentials. Each push publishes an immutable image to Amazon ECR before a rolling deployment.
- Complete the Secret Mission by scaling StockGuard to three replicas. You diagnosed a failed rollout caused by a nonexistent image. You recovered the public dashboard through a Kubernetes rollback.
Ready to quiz yourself?