• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on DigitalOcean for Laravel

The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and MongoDB on DigitalOcean for Laravel

Nginx as a High-Performance Frontend Proxy

Nginx is the de facto standard for serving web applications due to its event-driven, asynchronous architecture, making it exceptionally efficient at handling concurrent connections. When deploying Laravel applications, Nginx acts as a robust reverse proxy, efficiently serving static assets and forwarding dynamic requests to your application server (Gunicorn for Python, or PHP-FPM for PHP).

A critical aspect of Nginx tuning for Laravel involves optimizing worker processes, connection handling, and caching strategies. For a DigitalOcean Droplet, we’ll start by adjusting the worker_processes directive. A common best practice is to set this to the number of CPU cores available on your server. You can determine this using the nproc command.

Determining Optimal Worker Processes

Execute the following command on your Droplet:

nproc

Let’s assume nproc returns 4. We’ll then configure Nginx accordingly.

Nginx Configuration Tuning

Edit your main Nginx configuration file, typically located at /etc/nginx/nginx.conf. We’ll focus on the events and http blocks.

user www-data;
worker_processes 4; # Set to the output of 'nproc'
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024; # Adjust based on expected load and server memory
    multi_accept on;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    server_tokens off; # Hide Nginx version for security

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

    # Gzip compression for dynamic content
    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;

    # SSL configuration (if applicable)
    # ssl_protocols TLSv1.2 TLSv1.3;
    # ssl_prefer_server_ciphers on;
    # ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    # Logging configuration
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # Include virtual host configurations
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Within your Laravel application’s server block (e.g., in /etc/nginx/sites-available/your_laravel_app), ensure efficient handling of static assets and proper proxying to your application server. For static files, leverage Nginx’s ability to serve them directly, bypassing the application server entirely.

server {
    listen 80;
    server_name your_domain.com www.your_domain.com;
    root /var/www/your_laravel_app/public; # Adjust to your Laravel public directory

    index index.php index.html index.htm;

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

    # Serve static assets directly
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
        expires 30d; # Cache static assets for 30 days
        add_header Cache-Control "public";
        access_log off; # Don't log access for static files
    }

    # Pass PHP requests to PHP-FPM or Gunicorn
    # Example for PHP-FPM:
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        # With php-fpm (or other unix sockets):
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version and socket path
        # fastcgi_pass 127.0.0.1:9000; # Or TCP socket
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Example for Gunicorn (if using Python/Flask/Django with Nginx proxy)
    # location / {
    #     proxy_pass http://unix:/run/gunicorn.sock; # Or http://127.0.0.1:8000;
    #     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;
    # }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
    }
}

After making changes, always test your Nginx configuration and reload the service:

sudo nginx -t
sudo systemctl reload nginx

Optimizing PHP-FPM or Gunicorn for Laravel

The application server is where your Laravel code executes. For PHP applications, PHP-FPM (FastCGI Process Manager) is the standard. For Python-based frameworks often deployed with Laravel-like structures, Gunicorn is a popular choice.

PHP-FPM Tuning

PHP-FPM’s performance is heavily influenced by its process management. The configuration file is typically /etc/php/X.Y/fpm/pool.d/www.conf (replace X.Y with your PHP version, e.g., 8.1).

; The number of child processes that will be spawned.
; This can be set to 'auto' to set it dynamically based on the number of CPUs.
; Default Value: -1 (means 1 to each CPU core)
pm.max_children = 50 ; Adjust based on server memory and expected load

; The maximum number of children that can be spawned at the same time.
; Default Value: 5
pm.max_spawns = 10

; The minimum number of children that should always be available.
; Default Value: 5
pm.min_spare_servers = 5

; The maximum number of children that can be idle before they are killed.
; Default Value: 10
pm.max_spare_servers = 15

; The script is executed with uid/gid of the pool.
; Default Value: run_as_user
; user = www-data
; group = www-data

; The address on which to accept FastCGI requests.
; Valid syntaxes are:
;   'ip.add.re.ss:port', 'port', 'unix:/path/to/socket'
; Note: This value is mandatory.
listen = /var/run/php/php8.1-fpm.sock ; Ensure this matches Nginx config

; Set to 'on' if you want to use CPU affinity.
; Default Value: off
; pm.affinity = off

; The number of requests each child process should execute before respawning.
; This can be useful to prevent memory leaks but can also slow down
; processing if set too low.
; Default Value: 0 (unlimited)
pm.max_requests = 500 ; A good balance to prevent memory leaks

; Define the slowlog directory
; slowlog = /var/log/php/php8.1-fpm.slow.log
; request_slowlog_timeout = 10s

After modifying www.conf, restart PHP-FPM:

sudo systemctl restart php8.1-fpm # Adjust PHP version

Gunicorn Tuning (for Python/Django/Flask)

If you’re using Gunicorn to serve a Python application that integrates with or complements Laravel (e.g., a separate microservice), tuning is crucial. Gunicorn’s worker processes are key. The number of workers is typically calculated as (2 * Number of CPU Cores) + 1.

You can start Gunicorn with specific worker settings:

# Example command to start Gunicorn
gunicorn --workers 3 --bind unix:/run/gunicorn.sock --chdir /path/to/your/python_app your_module:app

For more persistent deployments, consider using a process manager like systemd. A typical systemd service file for Gunicorn might look like this:

[Unit]
Description=Gunicorn instance to serve your_python_app
After=network.target

[Service]
User=your_user
Group=your_group
WorkingDirectory=/path/to/your/python_app
ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/run/gunicorn.sock your_module:app
# Or for TCP binding:
# ExecStart=/usr/local/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 your_module:app

Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl enable your_python_app.service
sudo systemctl start your_python_app.service

MongoDB Performance Tuning on DigitalOcean

MongoDB, a NoSQL document database, is often used with Laravel for its flexibility. Performance tuning involves optimizing memory usage, indexing, and query patterns.

Memory Allocation and WiredTiger

MongoDB’s default storage engine, WiredTiger, performs best when it can cache a significant portion of the working set in RAM. The storage.wiredTiger.engineConfig.cacheSizeGB parameter in /etc/mongod.conf is crucial. A common recommendation is to allocate 50% of system RAM to the WiredTiger cache, ensuring enough is left for the OS and other processes.

storage:
  dbPath: /var/lib/mongodb
  journal:
    enabled: true
  engine: wiredTiger
  wiredTiger:
    engineConfig:
      cacheSizeGB: 2 # Example: If Droplet has 4GB RAM, leave 2GB for OS/other services
    collectionConfig:
      cacheResource:
        string: "UTF-8"
    indexConfig:
      prefixCompression: "disabled"

# ... other configurations ...

# Example: If Droplet has 8GB RAM, set to 4GB
# storage:
#   wiredTiger:
#     engineConfig:
#       cacheSizeGB: 4

After modifying mongod.conf, restart MongoDB:

sudo systemctl restart mongod

Indexing Strategies

Proper indexing is paramount for MongoDB query performance. Analyze your application’s query patterns using the MongoDB profiler or tools like mongotop and mongostat. Identify slow queries and create appropriate indexes.

To enable the profiler:

use admin
db.setProfilingLevel(1, 100) // Level 1, log queries slower than 100ms

To view slow queries:

db.system.profile.find().pretty()

Example of creating an index on a `users` collection for queries filtering by `email` and sorting by `created_at`:

use your_database_name
db.users.createIndex( { email: 1, created_at: -1 } )

For compound indexes, consider the order of fields based on query selectivity and sort order. Use tools like explain() to verify index usage.

db.collection.find({ field1: "value1", field2: "value2" }).explain()

Query Optimization and Schema Design

Avoid querying fields that are not indexed. Use projection to retrieve only the necessary fields, reducing network I/O and memory usage.

db.users.find( { status: "active" }, { name: 1, email: 1, _id: 0 } )

Denormalization can be beneficial for read-heavy workloads, embedding related data to reduce the need for joins (though MongoDB doesn’t have traditional joins, it refers to embedding documents). However, excessive embedding can lead to large documents and performance issues. Balance this with the application’s access patterns.

Putting It All Together: A Holistic Approach

This playbook provides a foundational set of optimizations for Nginx, PHP-FPM/Gunicorn, and MongoDB on DigitalOcean for Laravel applications. Remember that performance tuning is an iterative process. Continuously monitor your system’s metrics (CPU, RAM, I/O, network, application response times) using tools like Prometheus, Grafana, or DigitalOcean’s built-in monitoring. Adjust configurations based on observed behavior and load patterns. Regularly review and update your indexes and query strategies as your application evolves.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Disaster Recovery 101: Architecting Auto-Failovers for Redis and PHP Deployments on OVH
  • How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Race conditions during high-concurrency payment processing
  • Disaster Recovery 101: Architecting Auto-Failovers for Elasticsearch and Magento 2 Deployments on DigitalOcean
  • An Auditor’s Checklist for Securing WordPress Backends on OVH
  • Step-by-Step: Diagnosing Perl script high CPU throttling due to unoptimized regular expressions on AWS Servers

Copyright © 2026 · Vinay Vengala