Everything you need to know to get your web application off your laptop and in front of real users — covering free hosting, cloud platforms, DNS, HTTPS, and production best practices.
Deployment is the process of making your application available to users on a server, not just on your machine.
For sites built with HTML/CSS/JS, React, Vue, Next.js (static export), etc. — no server needed.
| Platform | Free Tier | Custom Domain | HTTPS | Build from Git | Best For |
|---|---|---|---|---|---|
| GitHub Pages | Unlimited (public repos) | Yes | Yes (auto) | Yes (Actions) | Docs, portfolios, blogs |
| Netlify | 100 GB/month bandwidth | Yes | Yes (auto) | Yes (built-in) | JAMstack, forms, functions |
| Vercel | 100 GB/month bandwidth | Yes | Yes (auto) | Yes (built-in) | Next.js, React, SSR |
| Cloudflare Pages | Unlimited bandwidth | Yes | Yes (auto) | Yes (built-in) | Speed, global CDN |
All four platforms offer free HTTPS, custom domains, and automatic deploys from Git. For static sites, you should almost never pay for hosting.
Push HTML to main or a gh-pages branch. Enable in repo Settings → Pages.
# Create and push to gh-pages branch
git checkout -b gh-pages
git push origin gh-pages
# Your site is live at:
# https://username.github.io/repo-name/
Build step runs in CI, deploys the output. Works for any framework.
GitHub Actions workflow:
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- uses: actions/upload-pages-artifact@v3
with:
path: ./dist
- uses: actions/deploy-pages@v4
# CLI deploy (one-time setup)
npm i -g vercel
vercel # deploys to preview
vercel --prod # deploys to production
// vercel.json (optional config)
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"rewrites": [
{ "source": "/(.*)", "destination": "/" }
]
}
# CLI deploy
npm i -g netlify-cli
netlify deploy # preview
netlify deploy --prod # production
# netlify.toml
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
For apps that need a backend server (Node, Python, Ruby, Go, etc.). PaaS handles infrastructure so you focus on code.
| Platform | Free Tier | Docker Support | Databases | Starting Price | Notes |
|---|---|---|---|---|---|
| Heroku | None (removed 2022) | Yes | Postgres add-on | $5/mo (Eco dynos) | Pioneer of PaaS, mature ecosystem |
| Railway | $5 credit/month | Yes | Postgres, MySQL, Redis | Usage-based | Modern Heroku alternative |
| Render | Static sites free; services sleep | Yes | Postgres (free 90 days) | $7/mo | Auto-deploy from Git |
| Fly.io | 3 shared VMs free | Yes (primary) | Postgres, SQLite (LiteFS) | Usage-based | Edge deployment, low latency |
# Railway example
npm i -g @railway/cli
railway login
railway init
railway up # deploys your app
The "big three" offer hundreds of services. Here are the ones relevant to web deployment.
Free tier: 12 months of t2.micro EC2, 5 GB S3, 1M Lambda requests/mo
Free tier: e2-micro VM always free, 2M Cloud Functions/mo
Free tier: B1S VM (12 months), 1M Functions/mo
Cloud providers charge per use. Always set billing alerts and review costs weekly. A misconfigured service can run up a large bill quickly.
# Upload static site to S3
aws s3 sync ./dist s3://my-bucket --delete
# Create CloudFront distribution pointing to bucket
# Gives you HTTPS + global CDN
aws cloudfront create-invalidation \
--distribution-id EDIST123 \
--paths "/*"
# Initialize and deploy
eb init my-app --platform node.js
eb create production
eb deploy
# Manages EC2, load balancer, auto-scaling
# Like Heroku but on AWS
| Aspect | Virtual Machines | Containers | Serverless |
|---|---|---|---|
| Abstraction | Full OS, you manage everything | App + dependencies packaged | Just your function code |
| Startup time | Minutes | Seconds | Milliseconds (cold: seconds) |
| Scaling | Manual or auto-scaling groups | Orchestrator (K8s, ECS) | Automatic, instant |
| Cost model | Pay for uptime (always on) | Pay for uptime (can scale to 0) | Pay per invocation |
| Control | Full (SSH, install anything) | High (Dockerfile defines env) | Limited (runtime constraints) |
| Examples | EC2, Compute Engine, Droplets | ECS, Cloud Run, Fly.io | Lambda, Cloud Functions, Vercel |
| Best for | Legacy apps, custom runtimes | Microservices, complex apps | APIs, event handling, low traffic |
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Starting out? Use serverless (Vercel, Netlify) or PaaS (Railway, Render). Move to containers when you need more control. VMs are for when you need full OS access or run specialized software.
DNS (Domain Name System) translates human-readable domain names into IP addresses that computers use.
| Record | Points To | Example |
|---|---|---|
| A | IPv4 address | 93.184.216.34 |
| AAAA | IPv6 address | 2606:2800:220:1::248 |
| CNAME | Another domain | myapp.netlify.app |
| TXT | Text data | Verification, SPF |
| MX | Mail server | mail.example.com |
Typical cost: $10–15/year for .com domains
If deploying to Vercel/Netlify, use a CNAME record pointing to their domain (e.g., cname.vercel-dns.com). They handle the rest.
HTTPS encrypts data between the browser and your server. It is required for modern web apps (browsers warn on HTTP).
Free, automated, open certificate authority. Certificates renew every 90 days.
# Using Certbot (most common)
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d myapp.com -d www.myapp.com
# Auto-renewal (runs twice daily)
sudo certbot renew --dry-run
Configuration that changes between environments (API keys, database URLs, feature flags) should never be hardcoded in your source code.
// WRONG: secrets in code
const dbUrl = "postgres://user:p4ssw0rd@db.com/prod";
const apiKey = "sk-abc123secret456";
// RIGHT: read from environment
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;
# .env (add to .gitignore!)
DATABASE_URL=postgres://localhost/myapp
API_KEY=dev-test-key-12345
NODE_ENV=development
# Vercel
vercel env add DATABASE_URL
# Netlify
netlify env:set DATABASE_URL "postgres://..."
# Railway
railway variables set DATABASE_URL="..."
# Render: Dashboard → Environment tab
# AWS (Elastic Beanstalk)
eb setenv DATABASE_URL="postgres://..."
# Docker
docker run -e DATABASE_URL="..." myapp
A reverse proxy sits between users and your app server(s), handling HTTPS termination, load balancing, caching, and more.
server {
listen 443 ssl;
server_name myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Caddyfile - that's it! Auto HTTPS.
myapp.com {
reverse_proxy localhost:3000
}
Caddy automatically provisions and renews Let's Encrypt certificates.
How you roll out new code to production without causing downtime or breaking things for users.
Platforms like Vercel, Netlify, and Railway handle deployment strategy automatically. You get atomic deploys — the new version replaces the old instantly with zero downtime. You only need to think about these strategies when running your own infrastructure.
Deploying is not "done" when the code is live. You need to know when things break and why.
| Metric | Why It Matters |
|---|---|
| Response time | Users leave if pages are slow |
| Error rate | 5xx errors mean your app is broken |
| Uptime | Is your site actually accessible? |
| CPU / Memory | Are you about to hit limits? |
| Request count | Traffic spikes, DDoS detection |
// Use structured JSON logs, not console.log
const logger = require('pino')();
logger.info({ userId: 123, action: 'login' },
'User logged in');
logger.error({ err, requestId: req.id },
'Payment failed');
JSON logs are searchable. Plain text logs are not.
What it actually costs to host a web application at different scales.
| Scenario | Platform | Monthly Cost | Includes |
|---|---|---|---|
| Static portfolio site | GitHub Pages / Cloudflare Pages | $0 | Hosting, HTTPS, CDN |
| React SPA + API routes | Vercel free tier | $0 | 100 GB BW, serverless functions |
| Node.js app + Postgres | Railway | $5–15 | App server + managed database |
| Full-stack app, moderate traffic | Render / Fly.io | $15–50 | Always-on server, database, SSL |
| Production app, team | AWS (ECS + RDS + CloudFront) | $50–200 | Containers, managed DB, CDN |
| High-traffic SaaS | AWS / GCP (multi-service) | $500+ | Auto-scaling, redundancy, monitoring |
API keys, database passwords, .env files pushed to public repos. Use .gitignore and rotate any leaked credentials immediately.
# .gitignore
.env
.env.local
*.pem
node_modules/
Running HTTP in production. All platforms offer free HTTPS. There is no excuse not to use it.
Using http://localhost:3000 in frontend code that runs in production. Use environment variables for API URLs.
Unhandled exceptions crash your server. Add global error handlers and process managers (pm2, container restart policies).
Deploying development source instead of production build. Always run npm run build and serve the optimized output.
Production uses a different runtime version than development. Pin versions in package.json engines, .nvmrc, or Dockerfile.
// package.json
{
"engines": {
"node": ">=20.0.0"
}
}
Load balancers and orchestrators need a /health route to know your app is alive.
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
Run through this before every production deploy.
.gitignore covers .env, node_modules, etc.A complete GitHub Actions workflow that builds, tests, containerizes, and deploys.
# .github/workflows/deploy.yml
name: Build & Deploy
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm test
- run: npm run build
deploy:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# Deploy to your platform of choice:
# - fly deploy
# - railway up
# - aws ecs update-service ...
The best deployment is the one you don't have to think about. Automate everything.