Skip to Content
Production Deployment

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 build

This creates optimized JavaScript files in dist/.

2. Install Production Dependencies Only

npm ci --only=production

This installs only runtime dependencies, excluding devDependencies.

3. Test Production Build Locally

NODE_ENV=production npm start

Environment 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=info

Setting Environment Variables

Linux/Mac (.env file):

export NODE_ENV=production export PORT=8080 export BASE_URL=https://short.yourdomain.com

Docker:

ENV NODE_ENV=production ENV PORT=8080

Cloud 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 nodejs

3. Clone and Build:

git clone <repository-url> cd url-shortener/backend npm install npm run build

4. Install PM2:

sudo npm install -g pm2

5. Start Application:

pm2 start dist/index.js --name url-shortener pm2 save pm2 startup

6. 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.com

Option 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 down

Option 3: Heroku

Prepare Application

1. Create Procfile in root:

web: cd backend && npm start

2. 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 --tail

Option 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.app

Option 5: AWS

Using Elastic Beanstalk

1. Install EB CLI:

pip install awsebcli

2. Initialize:

eb init -p node.js-18 url-shortener

3. Create Environment:

eb create production-env

4. Deploy:

eb deploy

5. Configure Environment Variables:

eb setenv NODE_ENV=production BASE_URL=https://your-app.elasticbeanstalk.com

Database Setup

Migrating from In-Memory to PostgreSQL

1. Install PostgreSQL Client

npm install pg npm install --save-dev @types/pg

2. 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/redis
import { 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 compression
import 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-limit
import 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 winston
import 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 helmet
import 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:

  • .env files (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-3

Database 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.gz

Code 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 production

Rollback:

pm2 deploy production revert 1

Docker 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.0

Troubleshooting

Check Application Logs

# PM2 pm2 logs url-shortener # Docker docker-compose logs -f backend # Heroku heroku logs --tail

Common Issues

Port Already in Use:

lsof -ti:3001 | xargs kill -9

Out 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