Full Setup: Next.js Universal Template & Port Manager on HestiaCP

Full Setup: Next.js Universal Template & Port Manager on HestiaCP

Step 1.1: Create the HTTP Template (.tpl)

Location: /usr/local/hestia/data/templates/web/nginx/nextjs-universal.tpl

Command to create:

#=========================================================================#
# Universal Next.js Web Domain Template                                    #
# DO NOT MODIFY THIS FILE! CHANGES WILL BE LOST WHEN REBUILDING DOMAINS   #
# https://hestiacp.com/docs/server-administration/web-templates.html      #
#=========================================================================#

server {
    listen      %ip%:%proxy_port%;
    server_name %domain_idn% %alias_idn%;
    
    error_log /var/log/apache2/domains/%domain%.error.log error;

    # Increase client max body size for file uploads
    client_max_body_size 100M;

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

    # Next.js proxy configuration
    # PORT: Change this port for each domain/user
    location / {
        proxy_pass http://127.0.0.1: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;
        
        # Next.js specific headers
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;
        
        # Timeout settings for long-running requests
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        
        # WebSocket support for Next.js
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Next.js static files (if served from public_html)
    location /static/ {
        alias /home/%user%/web/%domain%/public_html/static/;
        expires max;
        add_header Cache-Control "public, immutable";
    }

    # Next.js media files
    location /media/ {
        alias /home/%user%/web/%domain%/public_html/media/;
        expires max;
        add_header Cache-Control "public, immutable";
    }

    # Next.js _next/static files (if served from public_html)
    location /_next/static/ {
        alias /home/%user%/web/%domain%/public_html/_next/static/;
        expires max;
        add_header Cache-Control "public, immutable";
    }

    # Health check endpoint
    location /health {
        proxy_pass http://127.0.0.1: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;
    }
    
    include %home%/%user%/conf/web/%domain%/nginx.conf_*;
}

Step 1.2: Create the HTTPS/SSL Template (.stpl)

Location: /usr/local/hestia/data/templates/web/nginx/nextjs-universal.stpl

Command to create:

#=========================================================================#
# Universal Next.js SSL Web Domain Template                                #
# DO NOT MODIFY THIS FILE! CHANGES WILL BE LOST WHEN REBUILDING DOMAINS   #
# https://hestiacp.com/docs/server-administration/web-templates.html      #
#=========================================================================#

server {
    listen      %ip%:%proxy_ssl_port% ssl;
    server_name %domain_idn% %alias_idn%;

    ssl_certificate     /home/%user%/conf/web/%domain%/ssl/%domain%.pem;
    ssl_certificate_key /home/%user%/conf/web/%domain%/ssl/%domain%.key;
    
    error_log /var/log/apache2/domains/%domain%.error.log error;

    # Increase client max body size for file uploads
    client_max_body_size 100M;

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

    # Next.js proxy configuration
    # PORT: Change this port for each domain/user
    location / {
        proxy_pass http://127.0.0.1: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;
        
        # Next.js specific headers
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;
        
        # Timeout settings for long-running requests
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        
        # WebSocket support for Next.js
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Next.js static files (if served from public_html)
    location /static/ {
        alias /home/%user%/web/%domain%/public_html/static/;
        expires max;
        add_header Cache-Control "public, immutable";
    }

    # Next.js media files
    location /media/ {
        alias /home/%user%/web/%domain%/public_html/media/;
        expires max;
        add_header Cache-Control "public, immutable";
    }

    # Next.js _next/static files (if served from public_html)
    location /_next/static/ {
        alias /home/%user%/web/%domain%/public_html/_next/static/;
        expires max;
        add_header Cache-Control "public, immutable";
    }

    # Health check endpoint
    location /health {
        proxy_pass http://127.0.0.1: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;
    }
    
    include %home%/%user%/conf/web/%domain%/nginx.ssl.conf_*;
}

Step 1.3: Verify the files were created

ls -la /usr/local/hestia/data/templates/web/nginx/nextjs-universal*
You should see:
  • nextjs-universal.tpl (HTTP template)
  • nextjs-universal.stpl (HTTPS/SSL template)

Key Differences Between the Two Files:

Feature .tpl (HTTP) .stpl (HTTPS/SSL)
Port %proxy_port% %proxy_ssl_port%
SSL No SSL ssl enabled
Certificates None SSL certificates included
Include nginx.conf_* nginx.ssl.conf_*
 
Step 2.1: Create the port manager script
Location: /usr/local/bin/nextjs-port-manager.sh

 

#!/bin/bash

    # Next.js Port Manager for Multiple Users/Domains
    # This script helps manage ports for different Next.js applications

    set -e

    # Configuration
    PORTS_FILE="/etc/nextjs-ports.conf"
    LOG_FILE="/var/log/nextjs-ports.log"

    # Create ports file if it doesn't exist
    if [ ! -f "$PORTS_FILE" ]; then
        echo "# Next.js Port Allocation" > "$PORTS_FILE"
        echo "# Format: USER:DOMAIN:PORT:SERVICE_NAME" >> "$PORTS_FILE"
        echo "# Example: abzal:mclabor.abzal.net:3000:mclabor-nextjs" >> "$PORTS_FILE"
        echo "" >> "$PORTS_FILE"
    fi

    # Function to get next available port
    get_next_port() {
        local start_port=3000
        local max_port=3999
        
        # Read existing ports
        local used_ports=()
        if [ -f "$PORTS_FILE" ]; then
            while IFS=: read -r user domain port service; do
                if [[ "$port" =~ ^[0-9]+$ ]]; then
                    used_ports+=($port)
                fi
            done < <(grep -v '^#' "$PORTS_FILE" | grep -v '^$')
        fi
        
        # Find next available port
        for ((port=start_port; port<=max_port; port++)); do
            if [[ ! " ${used_ports[@]} " =~ " ${port} " ]]; then
                echo $port
                return 0
            fi
        done
        
        echo "No available ports in range $start_port-$max_port" >&2
        return 1
    }

    # Function to register a new Next.js app
    register_app() {
        local user="$1"
        local domain="$2"
        local service_name="$3"
        
        if [ -z "$user" ] || [ -z "$domain" ] || [ -z "$service_name" ]; then
            echo "Usage: $0 register <user> <domain> <service_name>"
            echo "Example: $0 register abzal mclabor.abzal.net mclabor-nextjs"
            exit 1
        fi
        
        # Check if already registered
        if grep -q "^$user:$domain:" "$PORTS_FILE"; then
            echo "App already registered for $user:$domain"
            grep "^$user:$domain:" "$PORTS_FILE"
            return 1
        fi
        
        # Get next available port
        local port=$(get_next_port)
        if [ $? -ne 0 ]; then
            echo "Error: $port"
            exit 1
        fi
        
        # Register the app
        echo "$user:$domain:$port:$service_name" >> "$PORTS_FILE"
        echo "Registered: $user:$domain:$port:$service_name"
        
        # Log the registration
        echo "$(date): Registered $user:$domain on port $port" >> "$LOG_FILE"
        
        echo "Port $port allocated for $domain"
        echo "Update your nginx template to use port $port"
        echo "Update your systemd service to use port $port"
    }

    # Function to list all registered apps
    list_apps() {
        echo "Registered Next.js Applications:"
        echo "================================"
        if [ -f "$PORTS_FILE" ]; then
            while IFS=: read -r user domain port service; do
                if [[ "$port" =~ ^[0-9]+$ ]]; then
                    echo "User: $user"
                    echo "Domain: $domain"
                    echo "Port: $port"
                    echo "Service: $service"
                    echo "Status: $(systemctl is-active $service 2>/dev/null || echo 'Not installed')"
                    echo "---"
                fi
            done < <(grep -v '^#' "$PORTS_FILE" | grep -v '^$')
        else
            echo "No applications registered"
        fi
    }

    # Function to remove an app
    remove_app() {
        local user="$1"
        local domain="$2"
        
        if [ -z "$user" ] || [ -z "$domain" ]; then
            echo "Usage: $0 remove <user> <domain>"
            exit 1
        fi
        
        # Get the port and service
        local line=$(grep "^$user:$domain:" "$PORTS_FILE")
        if [ -z "$line" ]; then
            echo "App not found for $user:$domain"
            return 1
        fi
        
        IFS=: read -r user domain port service <<< "$line"
        
        # Stop the service if running
        if systemctl is-active "$service" >/dev/null 2>&1; then
            echo "Stopping service $service..."
            systemctl stop "$service"
            systemctl disable "$service"
        fi
        
        # Remove from ports file
        sed -i "/^$user:$domain:/d" "$PORTS_FILE"
        
        echo "Removed: $user:$domain:$port:$service"
        echo "$(date): Removed $user:$domain from port $port" >> "$LOG_FILE"
    }

    # Function to get port for a domain
    get_port() {
        local user="$1"
        local domain="$2"
        
        if [ -z "$user" ] || [ -z "$domain" ]; then
            echo "Usage: $0 get-port <user> <domain>"
            exit 1
        fi
        
        local line=$(grep "^$user:$domain:" "$PORTS_FILE")
        if [ -z "$line" ]; then
            echo "App not found for $user:$domain"
            return 1
        fi
        
        IFS=: read -r user domain port service <<< "$line"
        echo $port
    }

    # Function to create service file
    create_service() {
        local user="$1"
        local domain="$2"
        local port="$3"
        local service_name="$4"
        
        local service_file="/etc/systemd/system/$service_name.service"
        local app_dir="/home/$user/web/$domain/public_html"
        
        cat > "$service_file" << EOF
    [Unit]
    Description=Next.js application for $domain
    After=network.target

    [Service]
    User=$user
    Group=$user
    WorkingDirectory=$app_dir
    ExecStart=$app_dir/node_modules/.bin/next start -p $port

    # Environment variables
    Environment="NODE_ENV=production"
    Environment="PORT=$port"
    Environment="PATH=$app_dir/node_modules/.bin:/usr/local/bin:/usr/bin:/bin"

    # Restart configuration
    Restart=always
    RestartSec=3

    # Security settings
    NoNewPrivileges=true
    PrivateTmp=true

    # Resource limits
    LimitNOFILE=65536

    [Install]
    WantedBy=multi-user.target
    EOF

        echo "Service file created: $service_file"
        echo "Run: sudo systemctl daemon-reload"
        echo "Run: sudo systemctl enable $service_name"
        echo "Run: sudo systemctl start $service_name"
    }

    # Main script logic
    case "$1" in
        "register")
            register_app "$2" "$3" "$4"
            ;;
        "list")
            list_apps
            ;;
        "remove")
            remove_app "$2" "$3"
            ;;
        "get-port")
            get_port "$2" "$3"
            ;;
        "create-service")
            if [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ] || [ -z "$5" ]; then
                echo "Usage: $0 create-service <user> <domain> <port> <service_name>"
                exit 1
            fi
            create_service "$2" "$3" "$4" "$5"
            ;;
        *)
            echo "Next.js Port Manager"
            echo "==================="
            echo ""
            echo "Usage:"
            echo "  $0 register <user> <domain> <service_name>  - Register new app"
            echo "  $0 list                                    - List all apps"
            echo "  $0 remove <user> <domain>                  - Remove app"
            echo "  $0 get-port <user> <domain>                - Get port for domain"
            echo "  $0 create-service <user> <domain> <port> <service> - Create service file"
            echo ""
            echo "Examples:"
            echo "  $0 register abzal mclabor.abzal.net mclabor-nextjs"
            echo "  $0 list"
            echo "  $0 get-port abzal mclabor.abzal.net"
            ;;
    esac
Step 2.2: Make the script executable
sudo chmod +x /usr/local/bin/nextjs-port-manager.sh
Step 2.3: Test the script
nextjs-port-manager.sh
Step 2.4: Verify the installation
    # Check if the script is in PATH
    which nextjs-port-manager.sh

    # Check if the ports file was created
    ls -la /etc/nextjs-ports.conf

    # Check if the log file was created
    ls -la /var/log/nextjs-ports.log
What the Port Manager Does:
✅ Manages ports - Automatically assigns ports 3000-3999
✅ Tracks applications - Keeps a database of all Next.js apps
✅ Creates services - Generates systemd service files
✅ Prevents conflicts - Ensures no two apps use the same port
✅ Easy management - Simple commands to register/list/remove apps

Available Commands:
nextjs-port-manager.sh register <user> <domain> <service_name> - Register new app
nextjs-port-manager.sh list - List all registered apps
nextjs-port-manager.sh get-port <user> <domain> - Get port for a domain
nextjs-port-manager.sh remove <user> <domain> - Remove an app
nextjs-port-manager.sh create-service <user> <domain> <port> <service> - Create service file
 
Step 3: Register Your First Next.js Application
1. First, let's check if the port manager script is working:
    1. Check the correct script name:
        
ls -la /usr/local/bin/nextjs-port-manager*

    2. Register your first Next.js app using the correct script name:
        
nextjs-port-manager.sh register abzal mclabor.abzal.net mclabor-nextjs

        This command will:
        Register an app for user abzal
        Domain mclabor.abzal.net
        Service name mclabor-nextjs
   
    3. Check what was created:
        
nextjs-port-manager.sh list
   
    4. View the generated systemd service file:
        
cat /etc/systemd/system/mclabor-nextjs.service

Create the symlink:
ln -s /home/abzal/web/mclabor.abzal.net/public_html/.next /home/abzal/web/mclabor.abzal.net/public_html/_next