How to Deploy Your First Vibe-Coded App Step by Step
Your application works on localhost. The repository has a lockfile, the production build succeeds, required environment variables are documented, and secrets are no longer stored in source code.
Now you can perform the first deployment.
Deployment is the process of building and running an application in an environment that users can reach over the internet. It involves more than uploading files. A full-stack application usually needs several connected resources:
- A place to serve the frontend
- A process that runs the backend
- A durable production database
- Environment variables and secrets
- Network access between components
- A migration process
- Logs and health checks
- A way to verify and reverse changes
This guide deploys the sample appointment-booking application used throughout this series:
Frontend:
- React
- Vite
- Static production build
Backend:
- Node.js
- Express
- PostgreSQL client or ORM
Database:
- PostgreSQL
Repository structure:
appointment-app/
├── client/
│ ├── package.json
│ ├── package-lock.json
│ ├── src/
│ └── vite.config.js
├── server/
│ ├── package.json
│ ├── package-lock.json
│ ├── src/
│ └── migrations/
└── README.mdThe frontend will become a Render Static Site. The Express backend will become a Render Web Service. The database will use Render Postgres.
Render supports static sites, public web services, background workers, cron jobs, private services, and managed datastores as separate resource types. Static sites are distributed through a CDN, while web services run dynamic applications such as Express and receive a public onrender.com address.
The exact dashboard labels, plan choices, and limits may change. This guide reflects official Render documentation reviewed on July 27, 2026. Check the current documentation before deploying an important application.
What This Deployment Will and Will Not Do
At the end of this guide, you should have:
- A PostgreSQL database
- A publicly reachable Express API
- A publicly reachable React frontend
- Server secrets stored outside Git
- A configured database connection
- A health-check endpoint
- A working production migration
- A verified appointment workflow
- Deployment and runtime logs you can inspect
This is a first controlled deployment, not the final public launch.
Do not invite real users yet. The application still needs broader testing, production database preparation, backups, monitoring, domain configuration, security review, and a documented rollback procedure. Later articles in this series cover those topics individually.
Prerequisites
Before starting, confirm that you have:
- A GitHub, GitLab, or Bitbucket repository containing the project
- A Render account
- A clean Git working tree
- Separate
clientandserverdirectories, or an equivalent documented structure - A successful local frontend build
- A successful backend start command
- A database migration process
- A
.env.examplefile with placeholders - No real secrets committed to Git
- A designated test email address
- No real user data in the local test database
Run the following from the repository root:
git status
git branch --show-currentCommit the version you intend to deploy:
git add <reviewed-files>
git commit -m "Prepare appointment app for first deployment"
git pushReplace <reviewed-files> with specific files you have inspected. Do not blindly stage .env, database exports, private keys, or uploaded files.
Record the resulting commit hash:
git rev-parse --short HEADKeep this value in your deployment notes. It identifies the exact code used for the first deployment.
Step 1: Verify the Backend Production Contract
Open server/package.json and confirm that it contains a production start script.
An illustrative example is:
{
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js",
"test": "vitest run",
"migrate": "node src/database/migrate.js"
}
}Do not use nodemon as the production process. It is a development tool that watches for file changes.
The Express server must use Render’s port instead of assuming port 3000:
const port = Number(process.env.PORT || 3000);
const host = "0.0.0.0";
const server = app.listen(port, host, () => {
console.log(`API listening on port ${port}`);
});Render requires public web services to bind to a port on 0.0.0.0. The platform recommends using the PORT environment variable, which it supplies to the service.
Add a small health endpoint:
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" });
});This endpoint should not expose:
- Environment variables
- Database credentials
- Internal hostnames
- Access tokens
- User information
- Stack traces
Render can send recurring requests to a configured health-check path. It uses those checks to determine whether a newly deployed version is ready and to identify unresponsive instances. Health checks apply to web and private services.
Run the backend locally using its production command:
cd server
npm ci
npm test
npm startIn another terminal, check the endpoint:
curl http://localhost:3000/healthThe expected response is similar to:
{
"status": "ok"
}Return to the repository root after testing:
cd ..Step 2: Verify the Frontend Production Contract
Open client/package.json and confirm the build script:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run"
}
}For a standard Vite project, the production output is commonly written to dist. Confirm the actual output in your configuration rather than assuming it.
Run:
cd client
npm ci
npm test
npm run buildInspect the generated directory:
ls distOn PowerShell:
Get-ChildItem distThe generated files should include the built HTML, JavaScript, CSS, and other static assets.
The frontend also needs a configurable API URL:
const apiBaseUrl = import.meta.env.VITE_API_URL;
if (!apiBaseUrl) {
throw new Error("VITE_API_URL is required");
}
export async function createAppointment(payload) {
const response = await fetch(`${apiBaseUrl}/api/appointments`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error("The appointment could not be created.");
}
return response.json();
}VITE_API_URL is included in browser-delivered code, so it must contain only a public URL. Never put a database password, email-provider key, private token, or session secret in a VITE_ variable.
Return to the root:
cd ..Step 3: Create the PostgreSQL Database First
Create the database before the backend because the backend needs a connection string during deployment.
In the Render dashboard:
- Open the New menu.
- Select Postgres.
- Enter a clear database name such as
appointment-production. - Enter a database name and user when prompted, or accept suitable generated values.
- Select a region.
- Choose a plan appropriate for this controlled deployment.
- Create the database.
Render Postgres provides connection details that include the host, port, user, password, and database name. For an application hosted on Render in the same region, Render recommends using the internal connection URL to reduce latency and keep traffic on its private network.
Choose the region carefully. Render currently does not support changing the region of an existing service or database directly, so moving later may require creating and migrating to a new resource.
Place the backend and database in the same region unless you have a documented reason not to.
After creation, locate the database’s connection information. You may see both:
- An internal database URL
- An external database URL
Use the internal URL for the Render-hosted backend in the same region.
Use the external URL only when an authorized tool outside Render needs to connect, such as a local migration session. Restrict and protect external database access appropriately.
Do not paste either connection URL into:
- Source code
README.md- An issue
- A chat message
- A screenshot
- A frontend environment variable
- A public build log
The URL contains credentials.
Step 4: Create the Express Web Service
In the Render dashboard:
- Open New.
- Select Web Service.
- Connect the Git provider.
- Select the appointment-app repository.
- Select the branch containing the reviewed deployment commit.
- Enter a service name such as
appointment-api.
Render can build and deploy a linked web service after changes are pushed to its selected Git branch. It also supports manual deployments.
Because this repository is a monorepo, set:
Root Directory: serverRender executes the build, start, pre-deploy, and publish settings relative to the configured root directory. Files outside that directory are unavailable to the service during its build and runtime.
Confirm the commands:
Build Command:
npm ci
Start Command:
npm startThe build command installs the exact dependency tree from package-lock.json. The start command runs the production server.
Select the same region as the PostgreSQL database.
For a learning deployment, you might choose an entry-level instance, but study its limitations before treating it as a real production service. Render’s free web services can spin down after periods without inbound traffic, causing a delayed response when they wake. That behavior can be acceptable during learning but may be unsuitable for actual users.
Do not create the service until you have reviewed its environment variables.
Step 5: Add Backend Environment Variables
Open the web service’s environment settings and add the required server variables.
A typical list is:
NODE_ENV=production
DATABASE_URL=<Render internal database URL>
SESSION_SECRET=<new production-only random secret>
APP_BASE_URL=<temporary frontend URL added later>
EMAIL_API_KEY=<test or production provider credential>
LOG_LEVEL=infoDo not reuse a development session secret in production.
Do not use a real email-provider key during the earliest infrastructure test unless email delivery is part of the controlled verification plan. A restricted test credential or provider sandbox is safer where available.
Render stores environment variables outside the repository and supports adding them individually or in bulk. Saving changed variables can trigger a new build or redeployment, depending on the selected save option.
Be careful when using Add from .env. A local .env file may include development URLs, local database credentials, or unrelated keys. Review every imported value before saving it.
Environment-variable values are strings. Convert values such as "false", "30", or "1000" explicitly in application code instead of relying on JavaScript truthiness.
For example:
const appointmentLimit = Number(process.env.APPOINTMENT_LIMIT || "50");
if (!Number.isInteger(appointmentLimit) || appointmentLimit < 1) {
throw new Error("APPOINTMENT_LIMIT must be a positive integer");
}Avoid code such as:
const debugEnabled = Boolean(process.env.DEBUG_ENABLED);Boolean("false") is true because a non-empty string is truthy. Parse the expected values deliberately:
const debugEnabled = process.env.DEBUG_ENABLED === "true";Step 6: Configure the Database Migration
A database server does not automatically contain your application’s tables. You must apply the schema migrations.
The exact command depends on your migration tool. Examples include:
npm run migratenpx prisma migrate deploynpx knex migrate:latestUse only the command that belongs to your application.
For eligible paid Render web services, a pre-deploy command can run after the build and before the new version starts serving traffic. Render recommends this stage for work such as database migrations. If the pre-deploy command fails, the deployment fails and the previous successful service version continues running.
A configuration might be:
Pre-Deploy Command:
npm run migrateDo not use this field casually.
Before enabling automatic migrations:
- Review every migration file.
- Apply the migrations to a new disposable database.
- Confirm they work from the first migration onward.
- Determine whether any migration deletes or rewrites data.
- Confirm the command uses
DATABASE_URL. - Make sure concurrent service starts cannot run the same migration again.
- Record the migration version.
- Prepare a database recovery approach.
The pre-deploy command runs on a separate instance. Changes it makes to that instance’s filesystem do not become part of the deployed service, and it cannot access a persistent disk attached to the service. It is suitable for database operations because those changes occur in the external database, not the temporary deployment filesystem.
For a free or early test configuration that does not provide the required pre-deploy feature, run migrations as a deliberate one-time operation from a secure environment. Do not hide migration logic inside the normal web-server startup command merely to make the first deployment easier. Multiple starts, retries, or scaling events can make that pattern unsafe.
Step 7: Deploy the Backend
Create the web service and watch the deployment logs.
Render’s deployment pipeline generally performs:
- Repository source retrieval
- Dependency installation and build
- The pre-deploy command, when configured
- The start command
- Health verification
- Traffic routing to the new version
If a build or pre-deploy command fails, the deployment does not proceed to later steps.
Look for evidence that:
npm cicompleted- The expected Node.js version was selected
- The correct lockfile was used
- The migration completed
- The server started
- The server listened on the supplied port
- The health check returned a successful response
- No secret values were printed
Do not treat “Deploy live” as proof that the application works.
Open the backend URL:
https://appointment-api.onrender.com/healthUse your actual service URL.
Expected response:
{
"status": "ok"
}Then test a deliberately invalid request to confirm that routing and validation work without creating data:
curl -i \
-X POST \
-H "Content-Type: application/json" \
-d '{}' \
https://appointment-api.onrender.com/api/appointmentsThe response should be a controlled 400-class validation error, not a stack trace or generic hosting error.
Check the Render Logs page after the request. Render provides deployment logs for failed or successful builds and runtime logs for a running service.
Step 8: Configure CORS Before Connecting the Frontend
The frontend and backend will initially use different hostnames. The backend must allow requests from the intended frontend origin.
A restricted Express configuration might look like:
import cors from "cors";
const allowedOrigins = new Set(
(process.env.ALLOWED_ORIGINS || "")
.split(",")
.map((origin) => origin.trim())
.filter(Boolean)
);
app.use(
cors({
origin(origin, callback) {
if (!origin || allowedOrigins.has(origin)) {
callback(null, true);
return;
}
callback(new Error("Origin not allowed"));
},
credentials: true
})
);Then configure:
ALLOWED_ORIGINS=https://appointment-web.onrender.comYou will add the exact static-site URL after creating the frontend.
Avoid:
app.use(cors());That broad setting may be acceptable for a genuinely public API without credentials, but it should not be used automatically for an authenticated application.
When cookies are involved, CORS is only one part of the configuration. You must also review cookie domain, Secure, HttpOnly, and SameSite settings. Authentication and authorization must still be enforced by the server.
Step 9: Create the React Static Site
In the Render dashboard:
- Open New.
- Select Static Site.
- Select the same repository.
- Select the deployment branch.
- Enter a name such as
appointment-web. - Set the root directory to
client. - Enter the build command.
- Enter the publish directory.
Use:
Root Directory:
client
Build Command:
npm ci && npm run build
Publish Directory:
distBecause client is the root directory, the publish path is relative to client, not the repository root. Render’s monorepo settings make build commands and publish paths relative to the selected service root.
Add the public frontend variable:
VITE_API_URL=https://appointment-api.onrender.comUse the actual backend hostname without an unnecessary trailing slash, or normalize the URL in code.
Static sites receive an onrender.com address and are served through Render’s CDN. Render also supports custom domains and automatic TLS for static sites, but custom-domain configuration is covered later in this series.
Create the static site and monitor the build.
The logs should show:
- Clean dependency installation
- Vite production build
- Successful creation of
dist - Publication of the expected files
Open the generated frontend URL and inspect the browser developer console.
Look for:
- Failed JavaScript or CSS requests
- Calls still going to localhost
- CORS failures
404responses- Mixed-content warnings
- Missing environment-variable errors
- API responses containing unexpected details
Step 10: Update the Backend’s Allowed Frontend Origin
Copy the frontend’s exact HTTPS origin:
https://appointment-web.onrender.comOpen the backend’s environment settings and set:
ALLOWED_ORIGINS=https://appointment-web.onrender.com
APP_BASE_URL=https://appointment-web.onrender.comSave and redeploy the backend.
Render offers several save behaviors for environment-variable changes, including saving without deployment, deploying the existing build, or rebuilding and deploying. Variables required only at runtime may not require a full rebuild, but variables used during compilation do. Review how your application consumes each value before selecting an option.
After the backend redeploys, refresh the frontend and repeat the request.
Do not add * as a temporary production origin merely to bypass a CORS error. Use the exact frontend origin and diagnose the underlying configuration.
Step 11: Run a Controlled End-to-End Test
You now have all three primary resources:
Browser
|
v
React Static Site
|
v
Express Web Service
|
v
Render PostgreSQLRun the critical path using designated test data.
Test 1: Frontend availability
Open the frontend in a private browser window.
Confirm:
- The page loads over HTTPS.
- CSS and JavaScript load.
- No localhost URL appears in the browser console.
- The app does not require your local development server.
Test 2: Backend health
Open the /health endpoint directly.
Confirm:
- It returns a successful status.
- It contains no internal information.
- The request appears in the expected service logs where available.
Test 3: Invalid input
Submit an empty or invalid appointment form.
Confirm:
- The user receives a useful validation message.
- The backend returns a
400-class status. - No database record is created.
- No stack trace appears in the browser.
Test 4: Valid appointment
Create one appointment using clearly labeled test information.
Confirm:
- The request returns a successful response.
- The record exists in PostgreSQL.
- The displayed date and time use the expected timezone.
- Refreshing the page does not create another record.
- The UI shows the saved result correctly.
Test 5: Duplicate or conflicting booking
Attempt to reserve the same slot again.
Confirm:
- The database or transaction logic prevents double booking.
- The API returns a controlled conflict response.
- The user sees a useful message.
- The original appointment remains unchanged.
Test 6: Authorization
Attempt to access administrative routes without an administrative account.
Confirm:
- The server rejects the request.
- Hiding the admin interface is not the only control.
- Direct API access does not reveal appointment data.
Test 7: Restart persistence
Trigger a safe backend redeployment without changing the database.
After deployment:
- Open the frontend.
- Retrieve the test appointment.
- Confirm the data still exists.
- Confirm the server reconnected to PostgreSQL.
This verifies that records are not being stored only in process memory or an ephemeral local file.
Step 12: Inspect Logs Without Exposing Data
Review both deployment and runtime logs.
Useful log events include:
Service starting
Database connection established
Migration version applied
Request validation failed
Appointment created
External email request failed
Graceful shutdown startedAvoid logging:
DATABASE_URL
SESSION_SECRET
EMAIL_API_KEY
Authorization headers
Session cookies
Full request bodies
Passwords
Complete customer recordsUse identifiers and event names rather than full personal details:
logger.info("Appointment created", {
appointmentId: appointment.id,
slotId: appointment.slotId
});Do not log the customer’s entire submission unless there is a documented need and an appropriate privacy policy.
Logs must help you investigate failures without becoming a second unsecured database.
Step 13: Record the Deployment
Create a small deployment record:
Deployment date:
July 27, 2026
Repository:
appointment-app
Branch:
prepare-first-deployment
Commit:
<commit hash>
Frontend:
<static site URL>
Backend:
<web service URL>
Health check:
<backend URL>/health
Database:
appointment-production
Region:
<selected region>
Migration version:
<version or migration filename>
Environment variables configured:
- DATABASE_URL
- SESSION_SECRET
- EMAIL_API_KEY
- APP_BASE_URL
- ALLOWED_ORIGINS
- VITE_API_URL
Verification completed:
- Frontend availability
- Health check
- Invalid request
- Valid booking
- Duplicate-booking protection
- Authorization
- Restart persistence
Known limitations:
- No custom domain
- No staging environment
- Backup restoration not tested
- Monitoring not configured
- Worker and cron deployment pendingDo not put secret values in the record.
This document makes the deployment reproducible and reduces dependence on memory or chat history.
Step 14: Understand What Auto-Deploy Will Do
By default, a Git-connected Render service can redeploy after changes reach the linked branch. This is convenient, but it also means a careless push can become a production change.
For the first controlled deployment:
- Use a dedicated deployment branch.
- Require reviewed pull requests where possible.
- Run tests before merging.
- Review the Git diff.
- Keep database migrations backward-compatible.
- Verify each deployment after it completes.
- Do not allow an AI coding agent to push directly without review.
Render keeps the previous successful deployment running when a new build or pre-deploy command fails. A successful build can still contain application defects, so post-deployment testing remains necessary.
Render also allows services to roll back to a previous successful deployment by reusing recent build artifacts. A service rollback does not automatically reverse database migrations or external actions.
The dedicated rollback article later in this series will explain that distinction in detail.
Common First-Deployment Problems
The deployment cannot find package.json
The root directory is probably incorrect.
For this sample:
Frontend root: client
Backend root: serverCommands run relative to those directories.
The build uses the wrong runtime
Declare a supported runtime version in the project and confirm the version shown in the build log.
Do not assume Render chose the same version installed locally.
The backend starts but Render reports no open port
The app may be listening only on localhost, using a fixed port, or exiting after startup.
Use:
app.listen(process.env.PORT || 3000, "0.0.0.0");Review the runtime logs for the actual startup error.
The frontend still calls localhost
The production build may be missing VITE_API_URL, or the URL may still be hardcoded.
Vite variables are incorporated during the build, so update the variable and rebuild the static site.
The browser reports a CORS error
Check:
- The frontend’s exact origin
- The backend’s allowed-origin list
- HTTP versus HTTPS
- An accidental trailing slash
- Credential settings
- Whether the backend returned an error before CORS headers were added
Do not solve the issue by allowing every origin without understanding the security impact.
The backend cannot connect to PostgreSQL
Check:
DATABASE_URL- Whether the internal URL is used
- Whether the service and database share a region
- SSL requirements imposed by the client library
- Database connection limits
- Migration output
- Whether the URL was copied completely
Never print the full URL into logs while debugging.
The database exists but tables do not
Creating PostgreSQL does not apply your application schema.
Run the reviewed production migration through an appropriate controlled process.
Data disappears after a redeployment
The application may be writing to local files or process memory rather than PostgreSQL.
Normal Render service filesystems are ephemeral unless an eligible persistent disk is attached. Application records should use the database, and durable uploads usually require persistent or object storage.
The free service is slow on the first request
A free web service may have spun down after inactivity. Its next request can take longer while the service starts again.
Do not interpret this behavior as a database bug without checking service events and logs.
A migration fails during deployment
Stop and inspect the migration error. Do not repeatedly redeploy the same destructive migration.
Confirm:
- The target database
- The current schema version
- Database permissions
- Missing extensions
- SQL compatibility
- Existing conflicting data
- Whether the migration partially completed
A later article will cover migrations in greater detail.
AI Prompt for Reviewing the Deployment Configuration
Use this prompt before allowing an AI coding assistant to change deployment files:
Goal:
Review this React, Express, and PostgreSQL project for deployment on Render.
Repository structure:
- client/: React and Vite frontend
- server/: Node.js and Express backend
- server/migrations/: PostgreSQL migrations
Target architecture:
- Render Static Site for client
- Render Web Service for server
- Render Postgres database
- Same region for backend and database
Constraints:
- Do not modify files yet.
- Do not expose secrets in frontend variables.
- Do not hardcode Render URLs.
- Do not replace PostgreSQL.
- Do not run destructive database commands.
- Do not run migrations against production.
- Do not add dependencies without explaining why.
- Do not broaden CORS to every origin without justification.
- Treat a successful build as insufficient verification.
Review:
1. Root directories
2. Install, build, start, and migration commands
3. PORT and host binding
4. Health-check endpoint
5. Required environment variables
6. Frontend-exposed variables
7. CORS configuration
8. Database connection behavior
9. Migration safety
10. Local filesystem writes
11. Logging and secret exposure
12. Post-deployment tests
13. Rollback risks
Expected output:
- Findings ranked by risk
- Exact affected files
- Proposed changes without applying them
- Verification command for each change
- Items requiring manual security reviewVerify every recommendation against the repository and current official documentation.
First Deployment Checklist
- The deployment commit is recorded.
- The repository contains no real secrets.
- The frontend installs successfully from its lockfile.
- The backend installs successfully from its lockfile.
- The production frontend build succeeds.
- The backend production start command succeeds.
- The backend binds to
0.0.0.0. - The backend uses the platform-provided
PORT. - The health endpoint returns a safe response.
- The PostgreSQL database is in the intended region.
- The backend is in the same region as the database.
- The backend uses the internal database URL.
DATABASE_URLis stored as a server environment variable.- The production session secret differs from development.
- Frontend variables contain no secret values.
- The migration command has been reviewed.
- Migrations have been tested against a disposable database.
- The backend root directory is correct.
- The frontend root directory is correct.
- Build and start commands run relative to those roots.
- The frontend publish directory matches the actual build output.
- The frontend uses the deployed API URL.
- CORS permits only intended origins.
- Authentication is enforced by the backend.
- Authorization is enforced by the backend.
- Invalid input produces a controlled response.
- A valid test appointment can be created.
- Duplicate-booking protection has been verified.
- Data remains after a backend redeployment.
- Runtime logs contain useful operational information.
- Runtime logs do not expose credentials or unnecessary personal data.
- The deployment URLs and commit are documented.
- Known limitations are documented.
- Auto-deploy behavior is understood.
- No real users have been invited before launch testing.
- The next production-readiness work is identified.
