Source: Jolli-sample-repos/url-shortener Last Updated: 4/8/2026
Production Deployment
Guide for deploying the URL Shortener to production environments.
Pre-Deployment Checklist
Before deploying to production, ensure:
- Code is tested and working locally
- TypeScript compiles without errors (
npm run build) - Environment variables are configured
- Database/storage solution is set up (if not using in-memory)
- CORS is configured for your domain
- HTTPS is enabled
- Monitoring and logging are configured
- Backup strategy is in place
Build for Production
1. Build the Backend
cd backend
npm run buildThis creates optimized JavaScript files in dist/.
2. Install Production Dependencies Only
npm ci --only=productionThis installs only runtime dependencies, excluding devDependencies.
3. Test Production Build Locally
NODE_ENV=production npm startEnvironment Configuration
Required Environment Variables
# Server Configuration
NODE_ENV=production
PORT=8080
BASE_URL=https://short.yourdomain.com
# Database (example for PostgreSQL)
DATABASE_URL=postgresql://user:password@host:5432/dbname
# Security
CORS_ORIGIN=https://yourdomain.com
# Monitoring
LOG_LEVEL=infoSetting Environment Variables
Linux/Mac (.env file):
export NODE_ENV=production
export PORT=8080
export BASE_URL=https://short.yourdomain.comDocker:
ENV NODE_ENV=production
ENV PORT=8080Cloud Platforms:
- Heroku: Use dashboard or
heroku config:set - AWS: Environment variables in Elastic Beanstalk/Lambda
- Google Cloud: Cloud Run environment variables
- Azure: App Service configuration
Deployment Options
Option 1: Traditional VPS (DigitalOcean, Linode, AWS EC2)
Setup Steps
1. Provision Server:
- Ubuntu 20.04 LTS or similar
- At least 1GB RAM
- Node.js 18.x installed
2. Install Node.js:
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs3. Clone and Build:
git clone <repository-url>
cd url-shortener/backend
npm install
npm run build4. Install PM2:
sudo npm install -g pm25. Start Application:
pm2 start dist/index.js --name url-shortener
pm2 save
pm2 startup6. Configure Nginx:
server {
listen 80;
server_name short.yourdomain.com;
location / {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
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;
}
}7. Enable HTTPS (Let’s Encrypt):
sudo apt-get install certbot python3-certbot-nginx
sudo certbot --nginx -d short.yourdomain.comOption 2: Docker
Dockerfile
Create Dockerfile in the backend directory:
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install all dependencies (including dev for build)
RUN npm ci
# Copy source code
COPY . .
# Build TypeScript
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install only production dependencies
RUN npm ci --only=production
# Copy built files from builder
COPY --from=builder /app/dist ./dist
# Expose port
EXPOSE 3001
# Set environment to production
ENV NODE_ENV=production
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start server
CMD ["node", "dist/index.js"]docker-compose.yml
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- PORT=3001
- BASE_URL=https://short.yourdomain.com
- DATABASE_URL=${DATABASE_URL}
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/health')"]
interval: 30s
timeout: 10s
retries: 3
frontend:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./frontend:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- backend
restart: unless-stopped
# Optional: PostgreSQL database
database:
image: postgres:15-alpine
environment:
- POSTGRES_DB=urlshortener
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:Build and Run
# Build images
docker-compose build
# Start services
docker-compose up -d
# View logs
docker-compose logs -f backend
# Stop services
docker-compose downOption 3: Heroku
Prepare Application
1. Create Procfile in root:
web: cd backend && npm start2. Ensure package.json has start script:
{
"scripts": {
"start": "node dist/index.js",
"build": "tsc",
"heroku-postbuild": "cd backend && npm install && npm run build"
}
}Deploy
# Login to Heroku
heroku login
# Create app
heroku create your-url-shortener
# Set environment variables
heroku config:set NODE_ENV=production
heroku config:set BASE_URL=https://your-url-shortener.herokuapp.com
# Deploy
git push heroku main
# Open app
heroku open
# View logs
heroku logs --tailOption 4: Vercel (Frontend + Serverless Functions)
Note: Requires adapting backend to serverless functions.
vercel.json
{
"version": 2,
"builds": [
{
"src": "backend/src/index.ts",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/api/(.*)",
"dest": "backend/src/index.ts"
}
]
}Deploy
# Install Vercel CLI
npm install -g vercel
# Deploy
vercel
# Set environment variables
vercel env add NODE_ENV production
vercel env add BASE_URL https://your-project.vercel.appOption 5: AWS
Using Elastic Beanstalk
1. Install EB CLI:
pip install awsebcli2. Initialize:
eb init -p node.js-18 url-shortener3. Create Environment:
eb create production-env4. Deploy:
eb deploy5. Configure Environment Variables:
eb setenv NODE_ENV=production BASE_URL=https://your-app.elasticbeanstalk.comDatabase Setup
Migrating from In-Memory to PostgreSQL
1. Install PostgreSQL Client
npm install pg
npm install --save-dev @types/pg2. Create Database Schema
CREATE TABLE short_urls (
short_code VARCHAR(20) PRIMARY KEY,
long_url TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP,
clicks INTEGER NOT NULL DEFAULT 0,
last_accessed_at TIMESTAMP
);
CREATE INDEX idx_long_url ON short_urls(long_url);
CREATE INDEX idx_expires_at ON short_urls(expires_at) WHERE expires_at IS NOT NULL;
CREATE INDEX idx_clicks ON short_urls(clicks DESC);3. Update Storage Layer
import { Pool } from 'pg';
class PostgresUrlStorage {
private pool: Pool;
constructor() {
this.pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
}
async create(url: ShortUrl): Promise<ShortUrl> {
await this.pool.query(
'INSERT INTO short_urls (short_code, long_url, created_at, expires_at, clicks) VALUES ($1, $2, $3, $4, $5)',
[url.shortCode, url.longUrl, url.createdAt, url.expiresAt, url.clicks]
);
return url;
}
async findByShortCode(shortCode: string): Promise<ShortUrl | undefined> {
const result = await this.pool.query(
'SELECT * FROM short_urls WHERE short_code = $1',
[shortCode]
);
return result.rows[0];
}
// Implement other methods...
}
export const urlStorage = new PostgresUrlStorage();Redis for Caching
Add Redis for high-performance caching:
npm install redis
npm install --save-dev @types/redisimport { createClient } from 'redis';
const redisClient = createClient({
url: process.env.REDIS_URL
});
await redisClient.connect();
// Cache frequently accessed URLs
async function findByShortCode(shortCode: string): Promise<ShortUrl | undefined> {
// Check cache first
const cached = await redisClient.get(`url:${shortCode}`);
if (cached) {
return JSON.parse(cached);
}
// Fetch from database
const url = await postgresStorage.findByShortCode(shortCode);
if (url) {
// Cache for 1 hour
await redisClient.setEx(`url:${shortCode}`, 3600, JSON.stringify(url));
}
return url;
}Performance Optimization
1. Enable Compression
npm install compressionimport compression from 'compression';
app.use(compression());2. Add Caching Headers
app.use('/api/v1/urls/:shortCode', (req, res, next) => {
res.setHeader('Cache-Control', 'public, max-age=300'); // 5 minutes
next();
});3. Rate Limiting
npm install express-rate-limitimport rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { error: 'Too many requests' }
});
app.use('/api/', limiter);4. Connection Pooling
For databases, use connection pools:
const pool = new Pool({
max: 20, // Maximum number of clients
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});Monitoring and Logging
Winston for Logging
npm install winstonimport winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}
// Use logger
logger.info('Server started', { port: PORT });
logger.error('Error creating URL', { error: err.message });Health Checks
The application includes a health endpoint at /health:
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
});
});Use this for:
- Load balancer health checks
- Monitoring systems (Datadog, New Relic)
- Kubernetes liveness/readiness probes
Monitoring Services
Recommended Services:
- Datadog - Full-stack monitoring
- New Relic - APM and monitoring
- Sentry - Error tracking
- LogRocket - Session replay
- Prometheus + Grafana - Self-hosted metrics
Security Best Practices
1. HTTPS Only
Force HTTPS in production:
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
}2. Helmet for Security Headers
npm install helmetimport helmet from 'helmet';
app.use(helmet());3. CORS Configuration
Restrict to your domains:
app.use(cors({
origin: ['https://yourdomain.com', 'https://www.yourdomain.com'],
credentials: true,
optionsSuccessStatus: 200
}));4. Environment Variables
Never commit sensitive data. Use:
.envfiles (gitignored)- Secret management services (AWS Secrets Manager, HashiCorp Vault)
- Platform environment variables
5. Input Validation
Already implemented via Zod schemas. Keep all validation on the server side.
Scaling
Horizontal Scaling
Run multiple instances behind a load balancer:
nginx Load Balancer:
upstream backend {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}Start Multiple Instances:
PORT=3001 pm2 start dist/index.js --name url-shortener-1
PORT=3002 pm2 start dist/index.js --name url-shortener-2
PORT=3003 pm2 start dist/index.js --name url-shortener-3Database Scaling
- Read Replicas: Route read queries to replicas
- Connection Pooling: Reuse database connections
- Caching: Use Redis for frequently accessed data
- Sharding: Partition data across multiple databases
Backup and Recovery
Database Backups
PostgreSQL:
# Backup
pg_dump -U username dbname > backup.sql
# Restore
psql -U username dbname < backup.sql
# Automated daily backups
0 2 * * * pg_dump -U username dbname | gzip > /backups/backup_$(date +\%Y\%m\%d).sql.gzCode Backups
- Use Git and remote repositories (GitHub, GitLab)
- Tag releases:
git tag v1.0.0 - Keep deployment artifacts
Rollback Strategy
PM2 Ecosystem File
// ecosystem.config.js
module.exports = {
apps: [{
name: 'url-shortener',
script: './dist/index.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3001
}
}]
};Deploy:
pm2 deploy productionRollback:
pm2 deploy production revert 1Docker Rollback
# Tag images
docker tag url-shortener:latest url-shortener:v1.0.0
# Rollback
docker-compose down
docker-compose up -d url-shortener:v1.0.0Troubleshooting
Check Application Logs
# PM2
pm2 logs url-shortener
# Docker
docker-compose logs -f backend
# Heroku
heroku logs --tailCommon Issues
Port Already in Use:
lsof -ti:3001 | xargs kill -9Out of Memory:
- Increase server RAM
- Optimize database queries
- Add connection pooling
- Enable caching
Database Connection Errors:
- Check DATABASE_URL
- Verify firewall rules
- Confirm database is running
- Check connection limits
Next Steps
- Configuration Guide - Environment setup
- Development Setup - Local development
- Architecture Overview - System design