Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Ubuntu Server Setup Guide

By Kokil Thapa | Last reviewed: August 2026

A reliable Ubuntu Server Setup Guide is the difference between a production environment that sleeps through the night and one that wakes you up at 3 AM with 502 errors. Most tutorials stop at installing packages, leaving you with an insecure, unoptimized box that fails under real traffic. This guide covers the complete stack I use daily for client projects in Nepal and abroad: Ubuntu 24.04 LTS, Nginx, PHP 8.4-FPM, MySQL 8.4, and automated deployment pipelines. Whether you are hosting a high-traffic WooCommerce store or a custom Laravel legal-tech portal, these steps establish a secure, maintainable foundation that scales without unnecessary complexity.

How do you secure a fresh Ubuntu Server Setup Guide installation?

Security is not a feature you add later; it is the first step of any competent server hardening strategy. On a fresh Ubuntu 24.04 LTS install, your immediate priority is reducing the attack surface before installing a single web package. I have seen too many developers in Kathmandu expose port 3306 or leave password authentication enabled, only to face brute-force attacks within hours of provisioning.

Create a non-root sudo user

Never run web services as root. Create a dedicated administrative user immediately after first boot:

adduser deploy
usermod -aG sudo deploy
su - deploy

All subsequent commands in this guide assume you are operating as this non-root user. If you are migrating from an older setup where everything runs as root, plan a maintenance window to refactor permissions. Running PHP-FPM or Nginx workers as root is a critical vulnerability that no amount of application-level security can mitigate.

Harden SSH access

Password authentication is obsolete for production servers. Generate an ED25519 key pair on your local machine and copy it to the server:

ssh-keygen -t ed25519 -C "deploy@production"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@your-server-ip

Edit /etc/ssh/sshd_config to enforce key-based authentication and disable root access:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2

Restart the SSH service with sudo systemctl restart sshd. Keep your current session open and test a new connection before closing the existing one. Getting locked out during initial setup is a rite of passage, but it is entirely avoidable with this verification step.

Configure UFW firewall rules

Ubuntu’s Uncomplicated Firewall (UFW) should be configured before exposing any services. The default policy must deny all incoming traffic except explicitly allowed ports:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose

Note that we use the Nginx Full profile, which opens both port 80 and 443. Never open port 3306 (MySQL) or 6379 (Redis) to the public internet. Database connections should only come from localhost or a private VPC network. For teams working remotely across Nepal’s variable ISP infrastructure, consider adding rate limiting to SSH to prevent lockouts during legitimate access from dynamic IPs.

Security Hardening PipelineFresh InstallUbuntu 24.04 LTSSSH HardeningKeys Only + No RootUFW FirewallDeny All / Allow 22,80,443Secure BaseReady for StackCritical Rules EnforcedNo password authentication permittedRoot login disabled system-wideDatabase ports bound to 127.0.0.1 only
Ubuntu Server Setup Guide security hardening pipeline: SSH key enforcement, UFW defaults, and service isolation must precede application installation

How do you configure Nginx and PHP 8.4-FPM for Laravel?

Nginx with PHP-FPM remains the gold standard for serving Laravel and Symfony applications in 2026. While Apache with mod_php still works, Nginx’s event-driven architecture handles concurrent connections more efficiently on resource-constrained VPS instances common in Nepal’s hosting market. PHP 8.4 is the current stable release and the minimum recommended version for Laravel 12.x, offering JIT improvements and property hooks that directly benefit framework performance.

Install the Ondřej Surý PPA

Ubuntu 24.04’s default repositories may not yet include PHP 8.4. Add the trusted PPA that has been the community standard for over a decade:

sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php8.4-fpm php8.4-mysql php8.4-xml php8.4-curl \
  php8.4-mbstring php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl \
  nginx mysql-server redis-server

This installs PHP-FPM (not the CLI-only or Apache module variant), Nginx, MySQL 8.4, and Redis in a single operation. Pinning versions explicitly prevents accidental upgrades during routine apt upgrade cycles that could break production.

Tune PHP-FPM pool configuration

The default FPM pool is configured for minimal memory usage, not production throughput. Edit /etc/php/8.4/fpm/pool.d/www.conf to match your server’s RAM:

[www]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data

pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000

php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 60
php_admin_value[upload_max_filesize] = 64M
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.memory_consumption] = 256
php_admin_value[opcache.interned_strings_buffer] = 16
php_admin_value[opcache.max_accelerated_files] = 20000
php_admin_value[opcache.validate_timestamps] = 0

The pm.max_children value depends entirely on available RAM. Each PHP-FPM worker consumes approximately 30–50 MB under load. On a 4 GB VPS, 50 children is aggressive; start at 30 and monitor. Setting opcache.validate_timestamps = 0 is mandatory for production — it tells OPcache to never check for file changes, eliminating thousands of filesystem stat calls per request. You must reload PHP-FPM after every deployment to clear the cache.

Configure Nginx virtual host

Create /etc/nginx/sites-available/laravel-app with a configuration optimized for Laravel 12:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    root /var/www/laravel-app/current/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 16 32k;
        fastcgi_busy_buffers_size 64k;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    access_log /var/log/nginx/laravel-app.access.log;
    error_log /var/log/nginx/laravel-app.error.log;
}

The /var/www/laravel-app/current/public path assumes a symlinked release structure used by Deployer. This pattern enables zero-downtime deployments by atomically swapping the current symlink. Enable the site with sudo ln -s /etc/nginx/sites-available/laravel-app /etc/nginx/sites-enabled/, test with sudo nginx -t, and reload with sudo systemctl reload nginx.

Nginx + PHP 8.4-FPM Request FlowClientHTTPS RequestNginxStatic Files DirectPHP → FastCGI SocketPHP 8.4-FPMWorker Pool (50 max)Laravel BootstrapOPcachePrecompiled BytecodeMySQL 8.4 / RedisData Layer (Localhost)Socket: /run/php/php8.4-fpm.sock | validate_timestamps=0 in production
Nginx forwards PHP requests via Unix socket to PHP 8.4-FPM workers with OPcache precompilation, while static assets bypass PHP entirely

What database and caching stack supports production Laravel apps?

MySQL 8.4 LTS is the default choice for most Laravel applications I deploy. It offers window functions, CTEs, and improved JSON support that simplify complex queries in legal-tech portals and e-commerce reporting. PostgreSQL 17 is equally viable and sometimes preferable for GIS-heavy applications or when you need advanced indexing strategies, but MySQL’s ecosystem maturity in Nepal’s hosting landscape makes it the pragmatic default.

Secure MySQL 8.4 installation

Run the security script immediately after installation and make these specific choices:

sudo mysql_secure_installation

# VALIDATE PASSWORD COMPONENT: Yes (STRONG)
# REMOVE ANONYMOUS USERS: Yes
# DISALLOW ROOT LOGIN REMOTELY: Yes
# REMOVE TEST DATABASE: Yes
# RELOAD PRIVILEGE TABLES: Yes

Create application-specific users with minimal privileges. Never connect as root from your Laravel application:

CREATE USER 'laravel_app'@'localhost' IDENTIFIED BY 'strong_random_password';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP ON laravel_db.* TO 'laravel_app'@'localhost';
FLUSH PRIVILEGES;

Bind MySQL to localhost only by editing /etc/mysql/mysql.conf.d/mysqld.cnf and setting bind-address = 127.0.0.1. This single line prevents remote exploitation attempts entirely.

Configure Redis for queues and caching

Redis 7.x handles Laravel queues, session storage, and application caching far more reliably than file-based drivers. Edit /etc/redis/redis.conf:

bind 127.0.0.1 ::1
maxmemory 256mb
maxmemory-policy allkeys-lru
save ""
appendonly yes

Disabling RDB snapshots (save "") and enabling AOF persistence provides better durability for queue jobs without the I/O overhead of periodic forks. Set maxmemory based on available RAM — 256 MB is sufficient for most mid-traffic Laravel apps. Restart Redis with sudo systemctl restart redis-server and verify connectivity with redis-cli ping.

ComponentRecommended Version (2026)PurposeCritical Config
PHP-FPM8.4.xApplication runtimeOPcache validate_timestamps=0
Nginx1.26+Reverse proxy + static filesFastCGI buffer tuning
MySQL8.4 LTSPrimary data storebind-address=127.0.0.1
Redis7.4.xQueues, cache, sessionsmaxmemory-policy allkeys-lru
CertbotLatestTLS certificate automationAuto-renewal cron enabled

How do you automate deployments and SSL certificate management?

Manual deployments via FTP or SSH+git pull are unacceptable for production systems in 2026. I use Deployer 7 for every Laravel project because it provides atomic releases, shared persistent directories, and instant rollback — all without requiring Node.js or build tools on the production server. Frontend assets are compiled locally or in CI and committed as artifacts, keeping the production environment lean and secure.

Set up Deployer 7 for zero-downtime releases

Install Deployer globally via Composer and initialize your project:

composer global require deployer/deployer:^7.0
dep init

Your deploy.php should define shared files and writable directories explicitly:

set('shared_files', ['.env']);
set('shared_dirs', ['storage', 'bootstrap/cache']);
set('writable_dirs', ['storage', 'bootstrap/cache']);
set('keep_releases', 5);

task('deploy:build_assets', function () {
    runLocally('npm ci && npm run build');
    upload('public/build', '{{release_path}}/public/build');
});

after('deploy:update_code', 'deploy:build_assets');
after('deploy:symlink_release', 'php-fpm:reload');

The php-fpm:reload task is non-negotiable when OPcache timestamp validation is disabled. Without it, users will see stale code until the next natural cache expiration. Deployer handles the symlink swap atomically, so there is zero downtime during the transition.

Automate TLS certificates with Certbot

Let’s Encrypt certificates expire every 90 days. Manual renewal is a ticking time bomb. Install Certbot and obtain certificates non-interactively:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com --non-interactive --agree-tos -m admin@example.com

Certbot automatically configures Nginx SSL directives and sets up a systemd timer for renewal. Verify the timer is active with sudo systemctl list-timers | grep certbot. Test renewal without hitting rate limits using sudo certbot renew --dry-run. For multi-domain setups common in Nepal’s legal-tech sector (where firms often operate separate portals for marriage, divorce, and notary services), Certbot handles SAN certificates seamlessly.

Deployer 7 Zero-Downtime Release Cycle1. Upload CodeNew Release Dir2. Link Shared.env + storage/3. Swap SymlinkAtomic Operation4. Reload FPMClear OPcacheFilesystem State During Deploymentreleases/20260825100000/current → releases/new/shared/.envshared/storage/← Symlink swap is atomic← No partial state visible
Deployer 7 atomic release cycle: code upload, shared directory linking, symlink swap, and PHP-FPM reload ensure zero downtime and instant rollback capability

How do you monitor and maintain production Ubuntu servers long-term?

Setup is a one-time event; maintenance is forever. The most common failure mode I encounter on client servers is not misconfiguration but neglect — outdated packages, full disks, expired certificates, and silently failing backup jobs. Build monitoring and maintenance into your infrastructure from day one, not as a reactive measure after an outage.

Implement automated updates safely

Unattended security patches prevent known exploits from lingering. Install and configure unattended-upgrades:

sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades

Edit /etc/apt/apt.conf.d/50unattended-upgrades to allow only security updates, not feature upgrades that could break PHP extensions or Nginx modules. Application-level dependencies (Composer, npm) should be updated deliberately during scheduled maintenance windows, never automatically.

Set up log rotation and disk monitoring

Laravel logs, Nginx access logs, and MySQL slow query logs grow indefinitely without rotation. Ubuntu’s logrotate handles this, but verify configurations exist for your custom paths. More critically, set up disk space alerts before partitions fill. A simple cron job checking df -h and emailing warnings at 80% utilization has saved countless production incidents on projects I maintain. For teams managing multiple client sites across Nepal, centralized monitoring via Netdata or Prometheus provides visibility without per-server SSH sessions.

Schedule and verify backups

Backups that are never tested are not backups. Automate nightly MySQL dumps and file archives, then monthly restore tests to a staging environment. Store backups off-server — S3-compatible object storage costs pennies per gigabyte and survives total server failure. For legal-tech portals handling sensitive documents, encrypt backups at rest and maintain retention policies compliant with Nepal’s data handling expectations. Document the restore procedure; during an actual crisis, nobody remembers the exact tar flags or mysql import syntax they used six months ago.

Next Steps for Your Ubuntu Server Setup Guide Implementation

This Ubuntu Server Setup Guide provides the foundation, but production readiness requires validation against your specific workload. Test PHP-FPM pool sizing under realistic load before going live. Benchmark database queries with your actual dataset, not synthetic fixtures. Verify SSL renewal works end-to-end before trusting automation. If you are building a Laravel application for Nepal’s legal or e-commerce sector and need hands-on implementation support, reach out to discuss your infrastructure requirements. For developers evaluating whether to self-host or use managed platforms, my comparison of AWS cloud hosting versus shared hosting in Nepal breaks down the real cost and operational trade-offs. And if your application layer needs optimization alongside server tuning, the modern Laravel architecture best practices guide complements this infrastructure foundation with application-level patterns that reduce server resource demands.

Frequently Asked Questions

Canonical recommends 1 GHz CPU, 1 GB RAM, and 2.5 GB disk space for minimal installs. In my production experience hosting Laravel applications, allocate at least 2 vCPUs, 4 GB RAM, and 40 GB SSD to comfortably run PHP-FPM, MySQL 8.0, Redis, and Nginx without swapping under moderate traffic loads.

Local Nepali VPS providers typically charge Rs 1,500–3,000 monthly (~USD 11–22) for basic specs. International cloud providers like DigitalOcean or Hetzner offer comparable performance for USD 6–12 monthly. For production Laravel or WooCommerce sites serving Nepali customers, I recommend budgeting Rs 3,000–5,000 monthly (~USD 22–37) for reliable uptime and adequate resources.

Ubuntu dominates PHP ecosystem support with official PPAs for PHP 8.2 through 8.4, making version management trivial via ondrej/php. AlmaLinux requires more manual repository configuration. Most Laravel deployment tools like Deployer 7 assume Debian-based paths. Unless your organization mandates RHEL derivatives, Ubuntu 24.04 LTS offers faster security patches and broader community documentation for web stacks.

Add the ondrej/php PPA with sudo add-apt-repository ppa:ondrej/php then install php8.4-fpm php8.4-mysql php8.4-redis packages. Configure separate PHP-FPM pools listening on different sockets like /run/php/php8.4-fpm.sock. Update your Nginx virtual host fastcgi_pass directive to point to the new socket. This allows running Laravel 12 on PHP 8.4 while legacy sites remain on PHP 8.2 without conflicts.

UFW provides human-readable syntax reducing firewall misconfiguration risk. Commands like ufw allow 22/tcp and ufw allow 'Nginx Full' are auditable and reversible. Raw iptables rules become unmaintainable across team members. On every Ubuntu server I configure, UFW wraps iptables reliably while persisting rules across reboots automatically. Enable it only after allowing SSH to avoid lockout.

Disable root login by setting PermitRootLogin no in /etc/ssh/sshd_config. Switch to key-based authentication only with PasswordAuthentication no. Change the default port from 22 to reduce automated brute-force noise. Install fail2ban with the sshd jail enabled to ban IPs after failed attempts. Restrict SSH access via UFW to specific IP ranges when possible. These steps prevent the majority of compromise vectors I encounter during security audits on client servers.

The most common cause is opcache holding stale bytecode after file updates. Configure opcache.revalidate_freq=0 in production or send SIGUSR2 to PHP-FPM master process post-deploy to gracefully reload workers. Also verify the socket path in your Nginx config matches the actual PHP-FPM pool socket. Check journalctl -u php8.4-fpm for permission errors when switching PHP versions. I have resolved this exact issue repeatedly on Deployer-managed Laravel deployments where symlink swaps triggered cached file references.

Install unattended-upgrades package and enable it via dpkg-reconfigure unattended-upgrades. Configure /etc/apt/apt.conf.d/50unattended-upgrades to apply only security origins, excluding kernel upgrades that require reboot verification. Set Automatic-Reboot-Time to off-peak hours if enabling auto-reboot. Monitor /var/log/unattended-upgrades.log weekly. This prevents critical CVEs from lingering while avoiding unexpected downtime during business hours for Nepal-based clients operating on IST timezone.

Choose Nginx for high-concurrency Laravel or static-heavy sites due to lower memory footprint per connection. Apache with mod_php remains viable for shared hosting compatibility or complex .htaccess requirements in legacy WordPress setups. For new Laravel 12 or Symfony 7 projects, I default to Nginx with PHP-FPM because it handles thousands of concurrent connections efficiently on limited RAM. Apache excels when you need per-directory configuration overrides without restarting services.

Enable slow_query_log with long_query_time set to 1 second in /etc/mysql/mysql.conf.d/mysqld.cnf. Analyze logs using mysqldumpslow or pt-query-digest to identify problematic patterns. Check EXPLAIN output for missing indexes or full table scans. Verify innodb_buffer_pool_size consumes 60–70% of available RAM. On production Laravel systems, I frequently find N+1 query issues masked by local caching that surface only under real data volumes. Use Laravel Debugbar or Telescope to correlate application code with database bottlenecks.

Use Certbot with Let's Encrypt for free automated certificates. For multiple domains on one server, obtain individual certs rather than wildcard unless managing subdomains dynamically. Configure certbot renew --deploy-hook to reload Nginx automatically. Store certificates in /etc/letsencrypt/live/domain.com/ with proper permissions. Set up HTTP-01 challenge validation through Nginx. I manage dozens of legal-tech portals on shared EC2 infrastructure using this approach, achieving zero certificate expiration incidents over three years through automated renewal hooks integrated into Deployer workflows.

Set system timezone with sudo timedatectl set-timezone Asia/Kathmandu. Configure PHP timezone in /etc/php/8.4/fpm/php.ini as date.timezone = Asia/Kathmandu. Match MySQL timezone by adding default-time-zone='+05:45' under [mysqld] section. Restart both services after changes. Mismatched timezones between system, PHP, and database cause subtle bugs in booking systems and legal document timestamps. On Court Marriage In Nepal and similar portals, consistent IST/NPT configuration prevented appointment scheduling errors across Bikram Sambat and Gregorian calendar conversions.

Implement layered backups combining daily database dumps via mysqldump or pg_dump compressed with gzip, plus weekly filesystem snapshots using restic or borgbackup to external storage. Store at least one copy off-server. Test restoration quarterly. For Laravel applications, include storage/app and .env files in backups. Automate via cron with failure notifications. On production eCommerce sites like Petals Nepal, this approach recovered order data after accidental deletion within minutes. Never rely solely on hosting provider snapshots as your only recovery mechanism.

Install netdata for real-time metrics visualization with minimal configuration overhead. Configure logwatch for daily email summaries of system events. Use monit to auto-restart crashed services like PHP-FPM or MySQL. Set up basic alerting via shell scripts checking disk usage, memory pressure, and load averages sent to Slack or email. For Nepal-based clients with limited budgets, this stack provides 90% of Datadog functionality at zero licensing cost. I have used this combination across sister sites on shared EC2 infrastructure to catch resource exhaustion before customer impact.

Default PHP CLI memory_limit often caps at 128M insufficient for large dependency trees. Increase temporarily with COMPOSER_MEMORY_LIMIT=-1 environment variable or permanently in /etc/php/8.4/cli/php.ini. Ensure swap space exists as fallback with sudo fallocate -l 2G /swapfile followed by mkswap and swapon. On constrained VPS instances common in Nepal hosting environments, Composer regularly exhausts memory during laravel/framework updates. Adding 2GB swap prevents OOM kills during deployment while keeping production RAM allocation optimized for PHP-FPM workers.

Share this article

Quick Contact Options
Choose how you want to connect me: