Article
Production Deployment Chaos: From Dev to Production in One Painful Day
Wrong build commands, missing environment separation, crashed deployments, and configuration nightmares—why only 15% of vibe-coded apps make it to production.
Production Deployment Chaos: From Dev to Production in One Painful Day
Your app works perfectly in the AI preview. You're ready to show the world. You hit "Deploy to Production" and... nothing works. The build fails. Environment variables are missing. The database won't connect. Your custom domain shows a 404. After 8 hours of troubleshooting, you're ready to give up.
Sound familiar? Research shows that only 15% of hobbyist projects on Replit make it to deployment. The gap between "works in preview" and "works in production" is enormous—and AI tools don't prepare you for it.
The Build Command Disaster
AI tools often configure the wrong build command, causing silent failures:
Common Mistakes
// ❌ Wrong build commands in package.json
{
"scripts": {
"build": "vite dev", // Running dev server, not building
"deploy": "npm start" // Starts local server instead of building
}
}
Correct Build Configuration
// ✅ Proper build and start commands
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"start": "node server.js"
}
}
// Deployment platform config
// vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"framework": "vite"
}
Platform-Specific Requirements
Vercel/Netlify:
- Build command:
npm run build - Output directory:
dist,build, or.next - Install command:
npm install
Cloudflare Pages:
- Build command:
npm run build - Build output directory:
dist - Root directory:
/
Railway/Render:
- Build command:
npm install && npm run build - Start command:
npm start - Port: Must use
process.env.PORT
No Separation Between Dev and Production
AI generates a single database, a single API key set, and no environment separation. This leads to:
- Development changes affecting production users
- Test data mixing with real user data
- Debugging in production with exposed error details
The Replit Disaster: A Cautionary Tale
In July 2025, a Replit AI agent deleted a production database during a live conference demo because there was no separation between development and production environments. The AI was told not to modify production, but it couldn't distinguish between environments.
Proper Environment Separation
// ✅ Environment-aware configuration
const config = {
development: {
database: process.env.DEV_DATABASE_URL,
apiUrl: 'http://localhost:3000',
debug: true,
},
staging: {
database: process.env.STAGING_DATABASE_URL,
apiUrl: 'https://staging.yourapp.com',
debug: true,
},
production: {
database: process.env.PROD_DATABASE_URL,
apiUrl: 'https://yourapp.com',
debug: false,
},
};
const env = process.env.NODE_ENV || 'development';
export default config[env];
Database Separation
Never share databases between environments:
# ❌ DON'T DO THIS
DEV_DATABASE_URL=postgres://...production-db...
PROD_DATABASE_URL=postgres://...production-db... # Same database!
# ✅ Separate databases
DEV_DATABASE_URL=postgres://localhost/myapp_dev
STAGING_DATABASE_URL=postgres://host/myapp_staging
PROD_DATABASE_URL=postgres://host/myapp_production
Missing Health Checks and Monitoring
You deploy, everything seems fine, but users start reporting errors hours later. You have no visibility into what's happening.
Add Health Check Endpoints
// ✅ Health check endpoint
app.get('/health', async (req, res) => {
try {
// Check database
await db.query('SELECT 1');
// Check external APIs
await fetch('https://api.stripe.com/v1/charges', {
method: 'GET',
headers: { 'Authorization': `Bearer ${process.env.STRIPE_KEY}` },
});
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: error.message,
});
}
});
Add Error Tracking
// ✅ Integrate error tracking (Sentry example)
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
});
app.use(Sentry.Handlers.errorHandler());
// Now all errors are tracked
app.get('/api/data', async (req, res) => {
try {
const data = await fetchData();
res.json(data);
} catch (error) {
Sentry.captureException(error);
res.status(500).json({ error: 'Internal server error' });
}
});
Port Configuration Failures
Your app listens on port 3000, but the hosting platform expects a dynamic port:
// ❌ Hardcoded port (fails on most platforms)
const PORT = 3000;
app.listen(PORT);
Dynamic Port Configuration
// ✅ Use environment variable for port
const PORT = process.env.PORT || 3000;
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});
Static File Serving Issues
Your app builds successfully, but all images, CSS, and JavaScript fail to load in production.
Common Mistakes
// ❌ Wrong static file path
app.use(express.static('public')); // Works locally, fails in production
// ❌ Absolute paths that break
<img src="/public/logo.png" /> // Should be "/logo.png"
Correct Static File Configuration
// ✅ Use absolute path to public directory
import path from 'path';
app.use(express.static(path.join(__dirname, '../public')));
// ✅ Or configure framework properly
// Vite
export default {
publicDir: 'public',
build: {
outDir: 'dist',
},
};
// Next.js
// Files in /public are automatically served at /
<img src="/logo.png" /> // Correct
SSL/HTTPS Configuration Problems
Your API calls fail with mixed content errors because you're calling HTTP APIs from an HTTPS site.
The Problem
// ❌ HTTP call from HTTPS site (blocked by browser)
fetch('http://api.yourapp.com/data')
Modern browsers block "mixed content" (HTTP resources on HTTPS pages).
The Solution
// ✅ Always use HTTPS in production
const API_URL = process.env.NODE_ENV === 'production'
? 'https://api.yourapp.com'
: 'http://localhost:3000';
fetch(`${API_URL}/data`);
// ✅ Or use relative URLs
fetch('/api/data'); // Uses same protocol as current page
Force HTTPS Redirects
// ✅ Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
Custom Domain Configuration Failures
You bought a domain, but can't figure out how to connect it to your deployed app.
DNS Configuration Checklist
For Vercel:
Type: A Record
Name: @
Value: 76.76.21.21
Type: CNAME
Name: www
Value: cname.vercel-dns.com
For Cloudflare Pages:
Type: CNAME
Name: @
Value: yourproject.pages.dev
Type: CNAME
Name: www
Value: yourproject.pages.dev
For Netlify:
Type: A Record
Name: @
Value: 75.2.60.5
Type: CNAME
Name: www
Value: yoursite.netlify.app
Common DNS Mistakes
- Forgetting www subdomain - Configure both
@andwww - Wrong TTL - Use 3600 seconds initially, then increase
- Not waiting for propagation - DNS changes take 24-48 hours
- Mixed DNS providers - Don't use Cloudflare + GoDaddy nameservers together
Log Access and Debugging
Your app crashes in production, but you can't see any error logs.
Enable Logging
# Vercel
vercel logs
# Railway
railway logs
# Netlify
netlify logs
# Cloudflare
wrangler tail --format pretty
Add Structured Logging
// ✅ Use proper logging library
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
],
});
// Usage
logger.info('User logged in', { userId: user.id });
logger.error('Database query failed', { error: err.message });
// NOT THIS
console.log('something happened'); // Lost in production
Deployment Rollback Strategy
You deploy a breaking change. Users are locked out. How do you roll back?
AI Tools Rarely Configure This
Most AI-generated projects have no rollback strategy. If a deployment breaks, you're stuck.
Rollback Solutions
Vercel:
# List deployments
vercel ls
# Promote previous deployment
vercel promote [deployment-url]
Railway:
# Rollback to previous deployment
railway rollback
Git-Based (Netlify/Cloudflare Pages):
# Revert to previous commit
git revert HEAD
git push
# Deploy automatically triggers
Blue-Green Deployment
// Advanced: Run two environments simultaneously
// Deploy to "green" while "blue" is live
// Test green, then switch traffic over
// Keep blue as instant rollback option
// routing-config.json
{
"routes": [
{
"pattern": "/*",
"destination": process.env.ACTIVE_ENV === 'blue'
? 'blue.yourapp.com'
: 'green.yourapp.com'
}
]
}
CI/CD Pipeline Missing
Every deployment requires manual steps: build locally, run tests, push to Git, configure hosting, deploy. This is error-prone and slow.
Basic GitHub Actions CI/CD
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
env:
NODE_ENV: production
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
Database Migrations in Production
You change the database schema locally, deploy, and... production still has the old schema. Your app crashes.
Migration Strategy
# ❌ Manual migrations (forgotten constantly)
# "Oh no, I forgot to run the migration!"
# ✅ Automated migrations in deployment
# package.json
{
"scripts": {
"deploy": "npm run migrate && npm run build",
"migrate": "prisma migrate deploy"
}
}
# Or in deployment config
# railway.toml
[deploy]
startCommand = "npm run migrate && npm start"
Safe Migration Practices
- Test migrations in staging first
- Backup database before migrating
- Make migrations backwards-compatible
- Run migrations before deploying new code
-- ✅ Safe: Add nullable column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Later deploy sets defaults, then make non-null
ALTER TABLE users ALTER COLUMN phone SET DEFAULT '';
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
-- ❌ Unsafe: Breaking change in one step
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NOT NULL;
-- This fails if any existing rows exist!
When Deployment Gets Enterprise-Grade
Some deployment requirements need expertise:
- Multi-region deployments
- Load balancing and auto-scaling
- Container orchestration (Kubernetes)
- Infrastructure as Code (Terraform)
- Compliance and security certifications
- Disaster recovery planning
Stuck trying to deploy your AI-generated app? Book an operations audit and we'll get you deployed in 24 hours.