Deploy OrbitX to Production
Dockerize, add PWA support, and deploy OrbitX live with CI/CD.
Introduction
30 Second Summary
You have built an impressive 3D satellite tracker on your own machine. Every real application needs to make the leap from "works on my laptop" to "anyone can visit this link."
In this project, you will take OrbitX from a local development project to a live, installable app anyone can access with a URL. You will add Progressive Web App support, package the full stack with Docker, and deploy the frontend to Vercel and the backend to Render with automatic redeployment on every code push.
What You'll Build
Anyone you share the URL with can open OrbitX in their browser, install it on their phone, and watch satellites orbit a 3D globe in real time.
By the end of this project, you'll have:
- An installable app that launches fullscreen on any phone or desktop with its own icon. Even offline, the last satellite positions are cached and visible with a "Data may be stale" badge.
- A Docker Compose setup that starts the entire stack in containers behind an nginx reverse proxy with a single command. It runs identically on any machine.
- A live URL you can share with anyone. Every push to GitHub auto-deploys the new version to Vercel and Render within minutes.
- Secret Mission: Add a GitHub Actions CI workflow that catches lint errors before broken code reaches production.
Are there any prerequisites?
This project picks up where OrbitX Projects 1-5 left off. You need the working frontend/ and backend/ codebase from those projects, along with Docker Desktop, Node.js, Python, and Git installed on your machine.
Before We Start
Before we dive in, let's lock in what you're about to deploy and why it matters.
After five projects building OrbitX on your local machine, this is where it becomes a real app that anyone can access with a URL.
Set Up PWA Support
OrbitX works on your machine, but it only exists as a development project. Before deploying it to the world, you are going to make it installable as a standalone app that feels native on any device.
A Progressive Web App lets users install OrbitX on their phone or desktop with its own icon, no browser chrome. It also caches data so the app keeps working offline. In this step, you will verify your tools, add PWA support, and build an offline status indicator.
In this step, get ready to:
- Confirm Docker Desktop, Git, Node.js, and Python are ready.
- Install and configure PWA support for the frontend.
- Add an offline status badge to the app.
Verify your tools and install vite-plugin-pwa
OrbitX depends on several tools you set up in previous projects. A quick version check confirms everything is still in place before you start making changes.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Confirm Docker Desktop is running by running this command:
docker --version
- You should see a version number printed, confirming Docker is active.
- Confirm Git, Node.js, and Python are available by running these commands:
git --version
node --version
python3 --version
Each command prints a version number. If any command fails, revisit the setup from your earlier OrbitX projects before continuing.
Seeing command not found?
If docker --version fails, make sure Docker Desktop is open and running. Look for the Docker whale icon in your system tray (Windows) or menu bar (macOS).
If python3 is not found on Windows, try python --version instead. Windows often uses python without the 3 suffix.
Still stuck? Help me troubleshoot a missing tool for my OrbitX project.
Tools confirmed. Now you need one new package to turn OrbitX into a PWA.
- Navigate to your OrbitX frontend directory and install vite-plugin-pwa by running these commands:
cd frontend
npm install vite-plugin-pwa@1.3.0 -D
What does this command do?
The -D flag installs the package as a dev dependency, meaning it is only needed during the build process. The @1.3.0 pins the version so your config matches this project exactly.
This plugin generates a web app manifest and a service worker for your Vite project. The manifest tells the browser how to install your app. The service worker caches assets for offline use.
- You should see vite-plugin-pwa appear in the install output with no errors.
Install failed?
Make sure you are inside the frontend directory, not the project root. Run pwd (macOS) or cd (Windows) to check your current location.
If you see permission errors, try running the command with sudo on macOS or running your terminal as Administrator on Windows.
Need more help? Help me fix my npm install error for vite-plugin-pwa.
Configure the PWA plugin in vite.config.ts
The vite-plugin-pwa package is installed, but it is not wired into your build yet. You need to add the plugin to your Vite config so it generates the manifest and service worker when the app builds.
- Open vite.config.ts in the frontend folder.
- Find your existing import lines at the top of the file. Add this import alongside them:
import { VitePWA } from 'vite-plugin-pwa'
- Find the plugins array inside defineConfig. Add the following VitePWA call after react() by copying this code:
VitePWA({
// Auto-update the service worker when new content is available
registerType: 'autoUpdate',
// Enable the service worker and manifest during development
devOptions: {
enabled: true
},
// App manifest controls how the installed PWA appears
manifest: {
name: 'OrbitX',
short_name: 'OrbitX',
theme_color: '#0a0a1a',
display: 'standalone',
icons: [
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' }
]
},
workbox: {
// Precache all static assets including the Earth texture
globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,webp}'],
// Cache API responses so satellite data works offline
runtimeCaching: [
{
urlPattern: /\/api\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache'
}
}
]
}
})
What does this configuration do?
The manifest object describes OrbitX to the browser. display: 'standalone' means the installed app opens in its own window without browser chrome. The two icon sizes (192px and 512px) are required by Chrome for the install prompt.
The workbox section controls caching. globPatterns precaches all static files (JavaScript, CSS, images, the Earth texture) so the app shell loads offline. runtimeCaching with NetworkFirst tries the network for API calls first, then falls back to the cache if the backend is unreachable.
The devOptions: { enabled: true } line activates the service worker and manifest during development so you can test PWA features with npm run dev instead of needing a production build.
- Save vite.config.ts.
- Start the dev server to confirm the config works by running:
npm run dev
- Open the URL Vite prints (usually http://localhost:5173) in Chrome.
- Open Chrome DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
- Click the Application tab in the top panel.
- Select Manifest in the left sidebar.
You should see the manifest loaded with the name OrbitX, the theme color #0a0a1a, and standalone display mode listed.
Manifest not showing in DevTools?
Make sure you added VitePWA() inside the plugins array, not outside it. It should sit alongside react() with a comma separating them.
Check that devOptions: { enabled: true } is present in your VitePWA config. Without it, the manifest is not served during development.
Still stuck? Help me figure out why my PWA manifest is not loading in Chrome DevTools.
- Stop the dev server by pressing Ctrl+C in the terminal.
Add an offline status indicator
The service worker caches API responses, but the user has no way to know when they are looking at stale data. You need a visible signal that tells the user the backend is unreachable.
- Open App.tsx in frontend/src/.
- Add the following state and effect at the top of your App component, alongside the existing state hooks:
// Track whether the backend is reachable
const [isOffline, setIsOffline] = useState(false)
// Periodically check backend connectivity
useEffect(() => {
const checkConnection = async () => {
try {
await fetch('/api/satellites')
setIsOffline(false)
} catch {
setIsOffline(true)
}
}
checkConnection()
const interval = setInterval(checkConnection, 30000)
return () => clearInterval(interval)
}, [])
What does this code do?
The isOffline state tracks whether the backend is reachable. The useEffect fires a fetch to /api/satellites every 30 seconds.
If the fetch throws (network error, server down), isOffline flips to true and the badge appears. When the backend comes back, the next check succeeds and the badge disappears automatically.
- Find your component's return JSX. Add the offline badge inside the return, above the globe element:
{/* Offline status badge */}
{isOffline && (
<div className="offline-badge">
Data may be stale - offline mode
</div>
)}
How does the badge connect to the service worker?
The service worker's NetworkFirst cache from the Vite config serves cached API responses when the backend is down. The globe still shows satellite positions even offline.
This badge tells the user that the data they see might be stale. It is the user-facing half of the offline story. The service worker is the behind-the-scenes half.
- Save App.tsx.
- Start the dev server again by running:
npm run dev
Open the app in Chrome. The globe should load normally with satellite data. The offline badge should not be visible because the backend is reachable.
Seeing errors in the console?
If you see a compile error, check that useState and useEffect are imported from React at the top of App.tsx. Your existing imports should already include them, but verify they are present.
If the badge shows even though your backend is running, make sure the fetch URL /api/satellites matches the route your backend serves.
Need help? Help me debug the offline badge in my OrbitX App component.
Time to verify the full PWA setup. Before you check, think about this: given that your manifest includes the app name, icons, and display mode, what do you expect to see in Chrome DevTools?
- In Chrome, open DevTools with Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
- Click the Application tab.
- Select Manifest in the left sidebar.
The manifest loads with the name OrbitX. You can see the theme color, display mode, and both icon sizes listed. After interacting with the page for about 30 seconds, Chrome shows an install icon in the address bar.
✔️ Awesome, I've got everything!
Great. Double check you have saved both vite.config.ts and App.tsx.
ⓧ I'd like to double check the full code
Confirm these pieces are present in your files, in order:
In vite.config.ts:
- The import { VitePWA } from 'vite-plugin-pwa' line alongside your existing imports.
- The VitePWA({...}) call inside the plugins array with registerType, devOptions, manifest, and workbox sections.
In App.tsx:
- The isOffline state hook at the top of your component.
- The useEffect that calls /api/satellites every 30 seconds.
- The {isOffline && (...)} JSX block with the offline badge text.
OrbitX is now a Progressive Web App with offline caching and a connection status indicator. Next up, you will package the full stack into Docker containers so it runs the same way on any machine.
Dockerize the Application
OrbitX is installable as a PWA and caches satellite data offline. But it still only runs on your machine because you have Node.js, Python, and every dependency installed.
Docker packages your entire app so it runs the same way on any machine. In this step, you will containerize both the frontend and backend. Along the way, you will run into a real networking problem that every developer hits when containers need to talk to each other.
In this step, get ready to:
- Create Dockerfiles for both the frontend and backend.
- Run the full app in Docker containers and hit a networking obstacle.
- Fix the obstacle with an nginx reverse proxy.
Create Dockerfiles for frontend and backend
A Dockerfile is a recipe that tells Docker how to build an image of your app. You need one for each service: the Python backend and the React frontend.
- Create a new file called Dockerfile inside your backend/ folder.
- Paste the following code into backend/Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
What does this Dockerfile do?
- FROM python:3.11-slim starts from a lightweight Python image so you do not need Python installed on the host.
- COPY requirements.txt and RUN pip install install dependencies before copying the rest of the code. This lets Docker cache the install layer when only your code changes.
- CMD starts the FastAPI server with uvicorn, binding to all interfaces so Docker can route traffic into the container.
- Save backend/Dockerfile.
- Confirm the file exists by running this from the project root:
ls backend/Dockerfile
You should see backend/Dockerfile printed in the output.
File not found?
Make sure the file is named exactly Dockerfile with a capital D and no file extension. It should sit directly inside the backend/ folder, not nested deeper.
help me find why my backend Dockerfile is not showing up
✔️ Awesome, I've got everything!
Great. Make sure you have saved backend/Dockerfile.
ⓧ I'd like to double check the full code
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Now the frontend needs its own Dockerfile. The frontend is a React app built with Vite. Unlike the backend, the frontend has two phases: building the static files, then serving them.
- Create a new file called Dockerfile inside your frontend/ folder.
- Paste the following code into frontend/Dockerfile:
# Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: serve
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
What is a multi-stage build?
A multi-stage build uses multiple FROM statements. Each stage starts fresh from a different base image.
- Stage 1 (builder) uses a Node image to run npm run build, producing static HTML, CSS, and JS files in the dist/ folder.
- Stage 2 copies only those built files into a tiny nginx:alpine image that serves them. Node.js, node_modules, and all build tools are left behind.
The final image is only a few megabytes instead of hundreds.
- Save frontend/Dockerfile.
- Confirm both Dockerfiles exist by running:
ls backend/Dockerfile frontend/Dockerfile
You should see both file paths listed in the output.
Only one file listed?
Check that the missing Dockerfile is named exactly Dockerfile (capital D, no extension) and sits directly inside the correct folder. A common mistake is creating it as Dockerfile.txt.
help me find my missing Dockerfile
Create docker-compose.yml and hit the networking obstacle
Docker Compose lets you define and run multiple containers together in one file. You will wire up both services and a shared network so they can communicate.
- Create a new file called docker-compose.yml at the project root (the same level as frontend/ and backend/).
- Paste the following configuration into docker-compose.yml:
version: '3.9'
services:
backend:
build: ./backend
expose:
- '8000'
networks:
- orbitx-net
frontend:
build: ./frontend
ports:
- '80:80'
depends_on:
- backend
networks:
- orbitx-net
networks:
orbitx-net:
What does this configuration do?
- backend builds from ./backend and uses expose to make port 8000 visible to other containers on the same network. It does NOT publish port 8000 to your host machine.
- frontend builds from ./frontend and publishes port 80 to your host using ports. That means you can open http://localhost in your browser.
- orbitx-net is a shared Docker network. Both services join it so they can find each other by service name (e.g. backend resolves as a hostname inside the network).
- Save docker-compose.yml.
Time to build and run everything. Before you do, think about this: your frontend JavaScript currently makes API calls to http://localhost:8000. The backend container's port 8000 is only exposed to other containers on the Docker network, not to your host machine. What do you think will happen when the browser tries to call localhost:8000?
- Build and start both containers by running:
docker compose up --build
This takes a minute or two the first time. Docker downloads base images, installs dependencies, and builds both services.
- Once you see log output from both frontend and backend services, open http://localhost in your browser.
The OrbitX frontend loads. The 3D globe renders. But there are no satellites. No debris. The data is missing.
- Open Chrome DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
- Click the Network tab.
- Refresh the page.
You will see failed requests to localhost:8000 marked in red. This is the obstacle. The JavaScript running in your browser tries to reach the backend at localhost:8000, but nothing is listening on port 8000 on your host machine.
Why can the browser not reach the backend?
The docker-compose.yml uses expose for the backend, not ports. That makes port 8000 available to other containers on the Docker network, but NOT to your host machine.
Your browser runs on the host, not inside a container. When the JavaScript calls http://localhost:8000, it hits your host's port 8000, which has nothing listening. The containers are isolated.
You could publish port 8000 with ports and that would work locally. But in production, the frontend (on Vercel) and backend (on Render) will be on completely different domains. The real fix is a reverse proxy that routes API traffic from the frontend container to the backend container.
- Stop the running containers by pressing Ctrl+C in the terminal where docker compose is running.
Fix with nginx reverse proxy
The fix has two parts. First, you set up nginx as a reverse proxy inside the frontend container so it forwards any /api/ request to the backend container. Second, you update the frontend code to use relative paths (/api/) instead of http://localhost:8000.
- Create a new file called nginx.conf inside your frontend/ folder.
- Paste the following configuration into frontend/nginx.conf:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
try_files $uri $uri/ /index.html;
}
}
What does this nginx config do?
- location /api/ catches any request starting with /api/ and forwards it to http://backend:8000/api/. Docker Compose resolves the name backend to the backend container's IP on the shared network.
- location / with try_files serves the React app's static files. If a URL does not match a file, it falls back to index.html so client-side routing works.
- Save frontend/nginx.conf.
- Confirm the file exists by running:
ls frontend/nginx.conf
You should see frontend/nginx.conf in the output.
✔️ Awesome, I've got everything!
Great. Make sure you have saved frontend/nginx.conf.
ⓧ I'd like to double check the full code
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
try_files $uri $uri/ /index.html;
}
}
Now you need to tell the frontend Dockerfile to use this nginx config instead of the default.
- In frontend/Dockerfile, find this line in Stage 2:
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
- Add a new line between them to copy your nginx config into the container, so it looks like this:
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
This replaces nginx's default site config with your custom proxy config at /etc/nginx/conf.d/default.conf.
- Save frontend/Dockerfile.
The last piece is the frontend code. It still makes API calls to http://localhost:8000. You need to change those to a relative /api/ path so the browser sends requests to nginx, which proxies them to the backend.
- In your frontend code, find where the backend API base URL is configured (look in vite.config.ts or wherever you defined the API URL in previous projects).
- Change the API base URL from http://localhost:8000 to /api.
- Update any fetch calls in App.tsx that use an absolute http://localhost:8000 URL to use the relative /api/ path instead.
- Save both files.
Why relative paths?
When the browser requests /api/satellites, it hits port 80 on the same host the page came from. Nginx catches the /api/ prefix and proxies the request to the backend container internally.
This pattern works in local Docker and will also work when you deploy to production, since you can set the API URL with an environment variable there.
Everything is wired up. Before you rebuild, what do you expect to see when the frontend's API calls go through nginx instead of directly to localhost:8000?
- Rebuild and start the containers by running:
docker compose up --build
- Once both services are running, open http://localhost in your browser.
The full OrbitX app loads with live satellite data, served through nginx. The globe, satellites, and debris field all appear. Open DevTools Network tab and confirm the API calls to /api/ return 200 responses.
Still seeing failed API requests?
- Check that frontend/nginx.conf has proxy_pass http://backend:8000/api/; with both trailing slashes. Missing slashes change how nginx rewrites the path.
- Make sure the COPY line in frontend/Dockerfile copies to /etc/nginx/conf.d/default.conf exactly. A different path means nginx ignores your config.
- Verify that your frontend code no longer references http://localhost:8000 anywhere. Search for localhost:8000 across your frontend source files.
help me debug my nginx reverse proxy setup in Docker
✔️ Awesome, I've got everything!
Great. Make sure you have saved all your files.
ⓧ I'd like to double check the full code
Here is the final frontend/Dockerfile:
# Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: serve
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Here is the docker-compose.yml:
version: '3.9'
services:
backend:
build: ./backend
expose:
- '8000'
networks:
- orbitx-net
frontend:
build: ./frontend
ports:
- '80:80'
depends_on:
- backend
networks:
- orbitx-net
networks:
orbitx-net:
Your entire stack runs in Docker containers behind an nginx reverse proxy. Next up, you will push your code to GitHub and deploy the backend to Render so anyone on the internet can access OrbitX.
Push to GitHub and Deploy the Backend
OrbitX now runs in Docker containers with the full app loading at http://localhost. There is one problem: "localhost" means only you can see it.
In this step, you will push your project to GitHub, then deploy the backend to Render. Render connects directly to your GitHub repo and auto-deploys whenever you push new code. Its free tier gives you 750 hours per month with no credit card required.
In this step, get ready to:
- Push all project files to a new GitHub repository.
- Deploy the backend to Render as a free Web Service.
- Verify the API is live at a public URL.
Initialize Git and push to GitHub
Your project folder contains files that should never be tracked in version control. Node packages, Python cache files, environment secrets, and build output would bloat the repository and leak sensitive data.
- In your code editor, create a new file called .gitignore in the project root (the folder that contains frontend/, backend/, and docker-compose.yml).
- Paste the following content into the file:
node_modules/
__pycache__/
.env
dist/
What does this file do?
Each line is a pattern Git will skip when tracking files. node_modules/ and dist/ cover the frontend's installed packages and build output. __pycache__/ covers Python's compiled bytecode. .env keeps environment secrets out of your repo.
- Save .gitignore.
✔️ Awesome, I've got everything!
Great. Double check you have saved .gitignore in the project root.
ⓧ I'd like to double check the full code
Your .gitignore should contain exactly this:
node_modules/
__pycache__/
.env
dist/
Your terminal from the previous step is likely showing Docker logs. Open a new terminal window or tab so you can run Git commands.
- Navigate to the project root by running this command:
cd ~/Desktop/orbitx
- Initialize a Git repository by running this command:
git init
You should see a message confirming the repository was initialized.
- Stage all files and make your first commit by running these commands:
git add .
git commit -m "Initial commit: OrbitX full stack with Docker"
You should see a summary listing all the files that were committed.
Seeing a Git identity error?
If Git asks you to set your name and email, run the two commands it suggests (git config --global user.email and git config --global user.name), then re-run the commit command above.
Still stuck? Help me fix my Git identity error.
Now you need a GitHub repository to push your code to.
- Open github.com in your browser.
- Click the + icon in the top-right corner, then click New repository.
- Enter a name for your repository (e.g., orbitx).
- Do NOT check any of the initialization options (no README, no .gitignore, no license). Your local repo already has these.
- Click Create repository.
GitHub shows a quick setup page with your repository URL. Use it to connect your local repo.
- Link your local repo to GitHub and push your code by running these commands:
git remote add origin [[REPO_URL="https://github.com/your-username/orbitx.git"]]
git branch -M main
git push -u origin main
What do these commands do?
- git remote add origin tells Git where to push your code (your GitHub repo URL).
- git branch -M main renames your default branch to main if it is not already.
- git push -u origin main uploads all your committed files to GitHub.
- Refresh your GitHub repository page in the browser.
You should see all your project files listed, including frontend/, backend/, docker-compose.yml, and .gitignore.
Don't see your files on GitHub?
Make sure you copied the correct repository URL from GitHub's quick setup page. It should end in .git.
If the push failed with an authentication error, GitHub requires a personal access token or SSH key instead of a password. Follow GitHub's authentication guide to set this up.
Need help? Help me push my OrbitX project to GitHub.
Deploy the backend to Render
Render is a cloud platform that builds and deploys your app directly from a GitHub repository. You will create a Web Service for the OrbitX backend so it gets a public URL anyone can reach.
- Open the Render Dashboard in your browser. Sign in or create a free account if you have not already.
- Click + New in the top-right corner.
- Select Web Service.
- Connect your GitHub account if prompted, then select your OrbitX repository from the list and click Connect.
The service creation form appears. Fill in the following fields:
- Enter orbitx-backend in the Name field.
- Set Root Directory to backend.
- Set Language to Python 3.
- Set Build Command to pip install -r requirements.txt.
- Set Start Command to uvicorn main:app --host 0.0.0.0 --port $PORT.
- Under Instance Type, select Free.
What do these settings mean?
Root Directory tells Render to run build and start commands from the backend/ folder, not the project root. Without this, Render would look for requirements.txt in the wrong place.
The Start Command mirrors what your Dockerfile uses, but Render manages the port dynamically through the $PORT environment variable instead of hardcoding 8000.
Now set up the environment variable your backend needs for CORS.
- Scroll down to the Environment Variables section.
- Add a new variable with the key ALLOWED_ORIGINS and the value http://localhost. This is a temporary placeholder. You will update it with your Vercel URL in the next step.
- Click Create Web Service.
Render kicks off your first deploy. You can watch the build progress on the service's Events page. The build takes a few minutes as Render installs your Python dependencies and starts uvicorn.
Why set ALLOWED_ORIGINS now?
Your FastAPI backend uses CORSMiddleware to control which domains can make API requests. Setting ALLOWED_ORIGINS to http://localhost is a placeholder for now. Once you deploy the frontend to Vercel in the next step, you will update this value to match the Vercel domain.
Deploy failing on Render?
Check that Root Directory is set to backend (not backend/ with a trailing slash, and not blank). If Render cannot find requirements.txt, the root directory is wrong.
Also confirm your Start Command references main:app (matching your FastAPI file name main.py).
Still stuck? Help me debug my Render deploy.
Verify the live API
Once the deploy finishes, Render assigns a public URL to your service. You can find it near the top of the service page (e.g., https://orbitx-backend.onrender.com).
- Copy this URL and save it somewhere. You will need it in the next step to connect the frontend.
Before you test the endpoint, what do you think the API will return when you visit /api/satellites?
- Open a new browser tab and visit your Render URL with /api/satellites appended (e.g., https://orbitx-backend.onrender.com/api/satellites).
You should see JSON satellite data in the browser. This confirms your backend is live and serving real data from a public URL.
Why does the first request take so long?
Render's free tier spins down your service after 15 minutes of inactivity. The first request after sleep takes 30-60 seconds while the server boots back up. This cold start is normal for a free-tier learning project.
Not seeing JSON data?
If the page shows a timeout or error, wait 60 seconds and try again. Free tier cold starts can take up to a minute.
If you see a 404 or application error, go back to the Render Dashboard and check the Events page for your service. The build logs will show what went wrong.
Need help? Help me get my Render backend responding.
Your backend is live on the internet. Anyone with the URL can hit the API and get satellite data. Next up, you will deploy the frontend to Vercel and connect it to this backend.
Deploy the Frontend to Vercel and Fix CORS
Your backend is live on Render and returning satellite data to anyone who hits the API URL directly. But no one can see the 3D globe, the debris field, or the search filters yet.
In this step, you will deploy the frontend to Vercel so the full OrbitX app is accessible from a public URL. You will immediately run into a CORS error because the browser blocks cross-origin API calls between your Vercel frontend and Render backend. Fixing this is a universal deployment skill you will use in every full-stack project.
In this step, get ready to:
- Deploy the OrbitX frontend to Vercel with auto-deploy from GitHub.
- Diagnose and fix the CORS error between the Vercel frontend and Render backend.
- Verify that pushing code to GitHub triggers auto-deploys on both platforms.
Deploy the frontend to Vercel
Vercel is a frontend hosting platform that connects to your GitHub repo and auto-deploys every time you push code. Before you create the Vercel project, you need to prepare two things in your codebase.
First, the frontend needs to know where the backend lives when it is running on Vercel instead of behind the local nginx proxy. Vite exposes environment variables prefixed with VITE_ to your client code at build time.
- In your code editor, open the file where you define the API base URL (this may be in vite.config.ts or in your API service module).
- Find the line where the API base is set to a relative /api/ path.
- Update it so the app reads from the VITE_API_URL environment variable in production, and falls back to /api/ for local Docker. The pattern looks like this:
// Use VITE_API_URL for production (Vercel), fall back to /api/ for local Docker
const API_BASE = import.meta.env.VITE_API_URL || '/api';
Why two different API paths?
Locally with Docker, nginx proxies /api/ requests to the backend container. On Vercel, there is no nginx proxy. The frontend needs the full Render URL to reach the backend directly.
The VITE_API_URL env var will be set in the Vercel dashboard. Locally it is undefined, so the fallback /api/ kicks in.
- Save the file.
Second, Vite React apps need a rewrite rule for client-side routing. Without it, deep links (like /satellites/25544) return a 404 because Vercel looks for a file at that path instead of serving index.html.
- Create a new file called vercel.json inside the frontend/ directory by pasting the following:
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
What does this rewrite do?
This tells Vercel to route every URL path back to index.html. Your React app's client-side router then handles the URL and renders the correct page.
- Save vercel.json.
- Commit and push both changes to GitHub by running these commands from the project root:
git add .
git commit -m "Add Vercel config and env-based API URL"
git push
- Confirm the push succeeded by checking for a message like main -> main in the terminal output.
Push failed?
Make sure you are in the project root directory (the folder containing docker-compose.yml) and that you have already set up a remote with git remote add origin in the previous step.
If you need help, ask here: why is my git push failing?
Now create the Vercel project.
- Open the Vercel Dashboard at vercel.com/dashboard.
- Click the Add New… dropdown in the top-right corner.
- Select Project.
- Under Import Git Repository, find your OrbitX repo and click Import.
You are now on the project configuration page.
- Under Root Directory, click Edit and type frontend.
- Confirm that Framework Preset shows Vite. Vercel auto-detects this from your project.
- Expand Environment Variables.
- In the Key field, enter VITE_API_URL.
- In the Value field, paste your Render backend URL:
[[RENDER_BACKEND_URL="https://orbitx-backend.onrender.com"]]
- Click Deploy.
Vercel starts building your frontend. The build takes about 1-2 minutes. Wait for the build to finish and the congratulations screen to appear.
What just happened?
Vercel cloned your repo, ran npm run build inside the frontend/ directory, and deployed the output to its CDN. The VITE_API_URL variable was baked into the build so the frontend knows where to reach your Render backend.
- Copy the public URL that Vercel assigned (it will look something like https://orbitx-xxxx.vercel.app). You will need this in the next substep.
Hit the CORS wall and fix it
Your frontend is deployed. Time to see if it works.
Before you open the URL, make a prediction. What do you think happens when your frontend at orbitx.vercel.app tries to call orbitx-backend.onrender.com?
- Open your Vercel URL in the browser.
The app loads, the globe renders, but there are no satellites. The globe is empty.
- Open DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
- Click the Console tab.
You will see an error like this:
Access to fetch at 'https://orbitx-backend.onrender.com/api/...' from origin 'https://orbitx.vercel.app' has been blocked by CORS policy
This is expected. This is the exact problem you are here to fix.
What is CORS and why is it blocking you?
CORS stands for Cross-Origin Resource Sharing. Browsers block requests from one domain to a different domain by default. Your frontend lives at orbitx.vercel.app and your backend lives at orbitx-backend.onrender.com. Those are different origins.
The backend must explicitly tell the browser which origins are allowed to make requests. FastAPI's CORSMiddleware handles this. Right now, the ALLOWED_ORIGINS env var on Render does not include your Vercel URL.
The fix is straightforward. Update the ALLOWED_ORIGINS environment variable on Render to include your Vercel URL.
- Open the Render Dashboard at dashboard.render.com.
- Click on your backend service (orbitx-backend).
- Click Environment in the left pane.
- Find the ALLOWED_ORIGINS variable and update its value to your Vercel URL:
[[VERCEL_FRONTEND_URL="https://orbitx.vercel.app"]]
- Click the save dropdown and select Save and deploy. This redeploys your existing backend build with the new environment variable.
Why Save and deploy instead of Save, rebuild, and deploy?
You did not change any backend code. You only changed an environment variable. Save and deploy redeploys the existing build with the updated variable, which is faster than a full rebuild.
- Wait for the Render deploy to finish. You can watch the progress in the Events tab.
- Confirm the backend is healthy by opening your Render backend URL followed by /api/satellites in the browser. You should see JSON satellite data.
Now verify that the CORS fix worked.
- Go back to your Vercel URL in the browser.
- Do a hard refresh by pressing Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).
Satellites appear on the globe. The debris field renders. Search and filter work. No CORS errors in the console.
Still seeing CORS errors?
Make sure the ALLOWED_ORIGINS value on Render exactly matches your Vercel URL, including https:// and without a trailing slash.
Also confirm that your FastAPI backend reads ALLOWED_ORIGINS from the environment and passes it to CORSMiddleware in the allow_origins parameter. If the value is hardcoded instead of read from the env var, the update will not take effect.
If you need help, ask here: why am I still getting CORS errors after updating ALLOWED_ORIGINS on Render?
Verify auto-deploy
Both Vercel and Render auto-deploy when you push to GitHub. Time to prove it works end to end.
- In your code editor, open the frontend file where the app title or heading is defined.
- Make a small visible change. For example, update the page title from OrbitX to OrbitX Live.
- Save the file.
- Commit and push the change by running:
git add .
git commit -m "Update app title to verify auto-deploy"
git push
- Open the Vercel Dashboard and click on your OrbitX project.
- Under Deployments, watch for a new deployment triggered by your push.
Before you refresh the live URL, predict: will your title change appear immediately, or does the build need to finish first?
- Wait for the Vercel build to complete (about 1-2 minutes).
- Refresh your Vercel URL.
Your title change is live. The build had to finish first because Vercel rebuilds the frontend from source on every push.
Does Render also auto-deploy?
Yes. Render's default auto-deploy behavior is On Commit. Every push to your linked branch triggers a new deploy on Render too. Since you only changed a frontend file, the backend redeploy will produce the same result. But the pipeline is proven.
No new deployment in Vercel?
Make sure your GitHub repo is connected to the Vercel project. In the Vercel Dashboard, go to Settings and confirm the Git section shows your repo and branch.
If you need help, ask here: why is Vercel not auto-deploying when I push to GitHub?
OrbitX is live on the internet. Anyone with your Vercel URL can see the 3D globe, live satellite data, and use search and filter. Next up, you will add a CI workflow with GitHub Actions to catch lint errors before they reach production.
Secret mission
Add a CI Workflow with GitHub Actions
Auto-deploy is convenient, but right now broken code goes straight to production. Can you add a GitHub Actions CI workflow that lints both frontend and backend code on every push?
Clean Up Your Resources
Clean Up Your Resources
All services in this project run on free tiers. There are no ongoing charges for Render, Vercel, or GitHub.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Render backend service (free tier, 750 hrs/month, no credit card). Spins down after 15 minutes of inactivity.
- Vercel frontend deployment (free Hobby plan, no billing).
- GitHub repository (free).
- Local Docker images and containers.
- Local project files on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building on OrbitX.
- All services run on free tiers. No charges will appear.
- Render's free tier spins down your backend after 15 minutes of inactivity. The first request after sleep takes 30 to 60 seconds (cold start). This is expected.
- Vercel stays live with no downtime.
- Local Docker images remain on disk. They take up storage but cost nothing.
Pause - I'll come back to this later
Shut down running processes and pause cloud deployments to free up local resources while keeping your files intact.
- In the Render Dashboard, click the checkbox next to your backend service, then click Suspend in the action menu that appears. This stops the service from receiving traffic.
- In the Vercel Dashboard, select your OrbitX project. Open Settings in the sidebar. Select Build and Deployment. In the Ignored Build Step section, select Don't build anything. This stops new deployments on push. Your existing deployment stays live.
- Stop any running Docker containers locally by running:
docker compose down
- Your project files, GitHub repo, and cloud configurations remain intact for when you return.
Delete - I don't want to use this again
Remove all project resources and start fresh. This cannot be undone for cloud resources.
Delete the Render backend service
- Open the Render Dashboard and click your backend service to view its details.
- Click Settings in the left sidebar.
- Scroll down and click Delete Web Service.
- Type the service name to confirm, then click Delete.
Delete the Vercel frontend project
- Open the Vercel Dashboard and select your OrbitX project.
- Click Settings in the sidebar.
- Scroll to the bottom of the General page. In the Delete Project section, click Delete.
- In the confirmation dialog, enter the project name and type delete my project when prompted. Click Continue.
Delete the GitHub repository
- Open your repository on GitHub.
- Click Settings in the repository header.
- Scroll down to the Danger Zone section and click Delete this repository.
- Click I want to delete this repository.
- Click I have read and understand these effects.
- Type the repository name to confirm, then click Delete this repository.
Remove local Docker resources and project files
- Stop any running containers and remove local Docker images by running:
docker compose down
docker system prune -a
- Delete the project folder from your Desktop:
macOS
rm -rf ~/Desktop/orbitx
Windows
Remove-Item -Recurse -Force ~\Desktop\orbitx
That's a Wrap!
That's a Wrap!
Nice work! You just took OrbitX from a local dev project to a live, installable app that anyone can access with a URL.
You've learned how to:
- Made OrbitX installable as a Progressive Web App with offline caching so satellite data loads even when the backend is unreachable.
- Dockerized the full stack with a multi-stage build, docker-compose, and an nginx reverse proxy to solve the container networking problem where localhost does not cross container boundaries.
- Deployed the backend to Render and the frontend to Vercel with auto-deploy from GitHub, and fixed the CORS error that blocks cross-origin API calls between different domains.
- Secret Mission: Added a GitHub Actions CI workflow that lints both frontend and backend code on every push, creating a quality gate before deploys complete.
Ready to quiz yourself?