Explore plans starting at ₹699/mo →
Applications & Infrastructure

Multi-Tier Application Deployment on Cloud VPS: The Complete Walkthrough

S
ServerRaja
11 min read
#Infrastructure#Nginx#Database#Redis#Security#Tutorial#Cloud VPS#MySQL#Firewall
Multi-Tier Application Deployment on Cloud VPS: The Complete Walkthrough

Why Multi-Tier Architecture Matters

Running everything on a single server is fine for development, but production applications need separation of concerns. Multi-tier architecture splits your application into distinct layers, each running on its own server or group of servers.

This separation provides: - **Security**: Database servers are never directly exposed to the internet - **Scalability**: Scale each tier independently based on its specific load - **Reliability**: Failure in one tier doesn't necessarily cascade to others - **Maintainability**: Update web server configs without touching application code

Architecture Overview

A standard three-tier architecture separates presentation, logic, and data:

Internet
    |
    v
+---------------------------------------------+
|  Tier 1: Web / Presentation Layer           |
|  +---------+  +---------+  +---------+     |
|  | Nginx   |  | Nginx   |  | Nginx   |     |
|  | + SSL   |  | + SSL   |  | + SSL   |     |
|  +----+----+  +----+----+  +----+----+     |
+-------+-----------+-----------+-------------+
        |           |           |
        v           v           v
+---------------------------------------------+
|  Tier 2: Application Layer                  |
|  +---------+  +---------+  +---------+     |
|  | Node.js |  | Node.js |  | Node.js |     |
|  | or      |  | or      |  | or      |     |
|  | Django  |  | Django  |  | Django  |     |
|  +----+----+  +----+----+  +----+----+     |
+-------+-----------+-----------+-------------+
        |           |           |
        v           v           v
+---------------------------------------------+
|  Tier 3: Data Layer                         |
|  +----------+  +---------+  +---------+    |
|  | MySQL    |  | Redis   |  | File    |    |
|  | Primary  |  | Cache   |  | Storage |    |
|  | + Replica|  | Cluster |  | (NFS)   |    |
|  +----------+  +---------+  +---------+    |
+---------------------------------------------+

Network Design and Security

Private Networking

Each tier communicates over private networks. Only the web tier is publicly accessible.

Public Network:  203.0.113.0/24  (Web tier only)
Private Network: 10.0.0.0/16     (All tiers)
+-- Web Tier:    10.0.1.0/24
+-- App Tier:    10.0.2.0/24
+-- Data Tier:   10.0.3.0/24

Firewall Rules

#!/bin/bash
# Firewall rules for multi-tier deployment

# Web Tier (10.0.1.0/24) - Public facing iptables -A INPUT -p tcp --dport 80 -j ACCEPT # HTTP iptables -A INPUT -p tcp --dport 443 -j ACCEPT # HTTPS iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/16 -j ACCEPT # SSH from internal

# App Tier (10.0.2.0/24) - Internal only iptables -A INPUT -p tcp --dport 3000 -s 10.0.1.0/24 -j ACCEPT # From web tier iptables -A INPUT -p tcp --dport 3000 -s 10.0.2.0/24 -j ACCEPT # Inter-app iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/16 -j ACCEPT # SSH from internal iptables -A INPUT -s 10.0.0.0/16 -j DROP # Drop other internal

# Data Tier (10.0.3.0/24) - Most restricted iptables -A INPUT -p tcp --dport 3306 -s 10.0.2.0/24 -j ACCEPT # MySQL from app tier iptables -A INPUT -p tcp --dport 6379 -s 10.0.2.0/24 -j ACCEPT # Redis from app tier iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/16 -j ACCEPT # SSH from internal iptables -A INPUT -s 10.0.0.0/16 -j DROP # Drop other internal

# Default deny iptables -P INPUT DROP iptables -P FORWARD DROP ```

Tier 1: Web Layer Setup

Nginx Reverse Proxy Configuration

# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events { worker_connections 4096; use epoll; multi_accept on; }

http { include /etc/nginx/mime.types; default_type application/octet-stream;

# Logging log_format main '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' '$request_time $upstream_response_time';

access_log /var/log/nginx/access.log main buffer=16k flush=5s;

# Performance sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048;

# Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Content-Security-Policy "default-src 'self'" always;

# Rate limiting limit_req_zone $binary_remote_addr zone=general:10m rate=50r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;

# Gzip compression gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

# Upstream app servers upstream app_backend { least_conn; server 10.0.2.10:3000 max_fails=3 fail_timeout=15s; server 10.0.2.11:3000 max_fails=3 fail_timeout=15s; server 10.0.2.12:3000 max_fails=3 fail_timeout=15s; keepalive 32; }

# SSL configuration server { listen 443 ssl http2; server_name app.example.com;

ssl_certificate /etc/ssl/certs/app.example.com.crt; ssl_certificate_key /etc/ssl/private/app.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;

# Static files (served directly from web tier) location /static/ { alias /var/www/static/; expires 1y; add_header Cache-Control "public, immutable"; access_log off; }

location /media/ { alias /var/www/media/; expires 30d; }

# Login endpoint with stricter rate limiting location /api/auth/login { limit_req zone=login burst=10 nodelay; proxy_pass http://app_backend; 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; }

# Application proxy location / { limit_req zone=general burst=100 nodelay; proxy_pass http://app_backend; 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; proxy_http_version 1.1; proxy_set_header Connection "";

proxy_connect_timeout 5s; proxy_read_timeout 60s; proxy_send_timeout 30s; } }

# Redirect HTTP to HTTPS server { listen 80; server_name app.example.com; return 301 https://$server_name$request_uri; } } ```

Tier 2: Application Layer Setup

Systemd Service Configuration

# /etc/systemd/system/webapp.service
[Unit]
Description=Web Application
After=network.target

[Service] Type=simple User=webapp Group=webapp WorkingDirectory=/opt/webapp Environment=NODE_ENV=production Environment=PORT=3000 Environment=DB_HOST=10.0.3.10 Environment=DB_NAME=webapp_production Environment=REDIS_HOST=10.0.3.11 ExecStart=/usr/bin/node /opt/webapp/server.js Restart=always RestartSec=5 StandardOutput=journal StandardError=journal SyslogIdentifier=webapp

# Security hardening NoNewPrivileges=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/opt/webapp/uploads

# Resource limits LimitNOFILE=65535 MemoryMax=2G

[Install] WantedBy=multi-user.target ```

Environment Configuration

# /opt/webapp/.env.production
NODE_ENV=production
PORT=3000

# Database DB_HOST=10.0.3.10 DB_PORT=3306 DB_NAME=webapp_production DB_USER=webapp_user DB_PASSWORD=<strong-password> DB_POOL_MIN=5 DB_POOL_MAX=20

# Redis REDIS_HOST=10.0.3.11 REDIS_PORT=6379 REDIS_PASSWORD=<strong-password> REDIS_DB=0

# Session SESSION_SECRET=<random-64-char-string> SESSION_TTL=3600

# Logging LOG_LEVEL=info LOG_FILE=/var/log/webapp/app.log ```

Tier 3: Database Layer Setup

MySQL Production Configuration

# /etc/mysql/mysql.conf.d/production.cnf
[mysqld]
# Basic settings
server-id = 1
bind-address = 10.0.3.10
port = 3306

# InnoDB tuning innodb_buffer_pool_size = 4G innodb_buffer_pool_instances = 4 innodb_log_file_size = 256M innodb_flush_log_at_trx_commit = 1 innodb_flush_method = O_DIRECT innodb_io_capacity = 2000 innodb_io_capacity_max = 4000

# Connection settings max_connections = 500 max_connect_errors = 100000 wait_timeout = 600 interactive_timeout = 600

# Binary logging for replication log_bin = mysql-bin binlog_expire_logs_seconds = 604800 max_binlog_size = 100M binlog_format = ROW

# Slow query log slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1

# Security local_infile = 0 symbolic-links = 0

# Character set character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci ```

Redis Cache Configuration

# /etc/redis/redis.conf
bind 10.0.3.11
port 6379
protected-mode yes
requirepass <strong-password>

# Memory management maxmemory 2gb maxmemory-policy allkeys-lru

# Persistence appendonly yes appendfsync everysec auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

# Performance tcp-backlog 511 timeout 300 tcp-keepalive 300

# Security rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command CONFIG "CONFIG_SERVERRAJA" ```

Deployment Automation

Deployment Script

#!/bin/bash
# deploy.sh - Multi-tier deployment script

set -euo pipefail

WEB_SERVERS="10.0.1.10 10.0.1.11 10.0.1.12" APP_SERVERS="10.0.2.10 10.0.2.11 10.0.2.12" DB_SERVER="10.0.3.10" APP_VERSION="${1:?Usage: deploy.sh <version>}"

echo "Deploying version $APP_VERSION..."

# Step 1: Database migrations (if any) echo "Running database migrations..." ssh deploy@$DB_SERVER "cd /opt/webapp && ./migrate.sh $APP_VERSION"

# Step 2: Deploy to app servers (rolling update) for server in $APP_SERVERS; do echo "Deploying to app server: $server" scp app-$APP_VERSION.tar.gz deploy@$server:/opt/webapp/ ssh deploy@$server " cd /opt/webapp tar xzf app-$APP_VERSION.tar.gz sudo systemctl restart webapp sleep 5 curl -sf http://localhost:3000/health || exit 1 " echo "App server $server updated and healthy" done

# Step 3: Update web server configs (if changed) for server in $WEB_SERVERS; do echo "Reloading web server: $server" scp nginx.conf deploy@$server:/tmp/nginx.conf.new ssh deploy@$server " sudo cp /tmp/nginx.conf.new /etc/nginx/nginx.conf sudo nginx -t && sudo systemctl reload nginx " done

echo "Deployment of $APP_VERSION complete!" ```

Monitoring All Tiers

# Prometheus targets for multi-tier monitoring
scrape_configs:
  - job_name: 'web-tier'
    static_configs:
      - targets: ['10.0.1.10:9113', '10.0.1.11:9113', '10.0.1.12:9113']
        labels:
          tier: 'web'

- job_name: 'app-tier' static_configs: - targets: ['10.0.2.10:3000', '10.0.2.11:3000', '10.0.2.12:3000'] labels: tier: 'app'

- job_name: 'db-tier' static_configs: - targets: ['10.0.3.10:9104'] labels: tier: 'database'

- job_name: 'cache-tier' static_configs: - targets: ['10.0.3.11:9121'] labels: tier: 'cache' ```

Real-World Example: Healthcare Portal

A Hyderabad-based healthcare startup deployed their patient portal on a three-tier architecture:

**Web Tier**: 3 Nginx servers behind a load balancer, handling SSL termination and static content **App Tier**: 4 Node.js servers running the API and business logic **Data Tier**: MySQL primary-replica setup with Redis for session and query caching

**Key decisions**: - HIPAA-compliant: Data tier has no public internet access - Encryption: All data encrypted at rest and in transit - Audit logging: Every database query logged with user context

**Results**: Sub-200ms response times for 95th percentile, zero security incidents in 18 months, and the ability to handle 10,000 concurrent users during telehealth appointment booking surges.

Security Hardening Summary

Web Tier:
+-- TLS 1.2+ only
+-- Security headers (HSTS, CSP, X-Frame-Options)
+-- Rate limiting (50 req/s general, 5 req/s login)
+-- WAF rules (optional)
+-- Fail2ban for brute force protection

App Tier: +-- No public internet access +-- Input validation on all endpoints +-- Parameterized database queries +-- Environment variable secrets +-- Non-root process execution

Data Tier: +-- Most restricted network access +-- Encrypted connections required +-- Strong passwords (32+ characters) +-- Regular automated backups +-- Query logging for audit ```

Multi-tier deployment requires more initial setup than a single server, but the security, scalability, and reliability benefits make it the standard for production applications. Start with three tiers, automate deployments from day one, and scale each tier independently as your traffic grows.

Key Takeaways

  • **Three tiers (web, app, data)** with private networking between them isolates each layer — a compromised web server cannot directly access the database without traversing the application layer.
  • **Automate deployments from day one** — a deployment script with rolling updates, health checks, and rollback capability prevents human error during every release.
  • **Non-root process execution and environment variable secrets** are essential security baselines — never hardcode credentials in config files or run services as root.
  • **Monitor all tiers independently** with Prometheus — database slow queries, application response times, and Nginx error rates tell different parts of the same story.
  • **Each tier scales independently** — add more app servers for CPU-bound workloads, scale the database vertically for storage, and add web nodes for connection handling without affecting the other layers.
Multi-Tier App Deployment on Cloud VPS | ServerRaja