• 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 PHP

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

Nginx as a High-Performance Frontend Proxy

For a PHP application, Nginx serves as an exceptionally efficient frontend proxy and static file server. Its asynchronous, event-driven architecture excels at handling a high volume of concurrent connections with minimal resource overhead. We’ll focus on tuning Nginx for optimal performance when serving dynamic PHP requests via Gunicorn (for Python backends) or directly to PHP-FPM.

Core Nginx Configuration Tuning

The primary configuration file, typically located at /etc/nginx/nginx.conf, contains global directives. Key parameters to adjust for performance include:

  • worker_processes: Set this to the number of CPU cores available on your DigitalOcean droplet. This allows Nginx to utilize all available processing power.
  • worker_connections: This defines the maximum number of simultaneous connections that each worker process can handle. A common starting point is 1024 or higher, depending on expected traffic. The actual limit is also constrained by the system’s open file descriptor limit.
  • multi_accept: Setting this to on allows each worker process to accept as many new connections as possible, rather than just one at a time.
  • keepalive_timeout: Controls how long an idle keep-alive connection will remain open. A value between 65 and 75 seconds is often a good balance between resource usage and client responsiveness.
  • send_timeout: Sets the timeout for sending a response to the client.
  • client_header_timeout: Sets the timeout for reading the client header.
  • client_body_timeout: Sets the timeout for reading the client body.

Here’s an example snippet from nginx.conf:

user www-data;
worker_processes auto; # Or set to number of CPU cores
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 4096; # Increased from default
    multi_accept on;
}

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

    server_tokens off; # Hides Nginx version for security

    # Gzip compression for static assets
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

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

    access_log /var/log/nginx/access.log;

    # Include server blocks
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Optimizing PHP-FPM Configuration

When using PHP-FPM (FastCGI Process Manager), tuning its configuration is critical. The primary configuration file is typically /etc/php/[version]/fpm/php-fpm.conf, with pool configurations in /etc/php/[version]/fpm/pool.d/www.conf.

Process Management (pm)

The pm directive controls how PHP-FPM manages worker processes. The most common and recommended settings are:

  • pm = dynamic: This is the default and generally recommended. PHP-FPM will manage the number of child processes based on pm.max_children, pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers.
  • pm = static: All child processes are created during the FPM startup and remain active. This can offer slightly better performance for high-traffic sites as there’s no overhead for process spawning, but it consumes more memory. Use this with caution and ensure sufficient RAM.
  • pm = ondemand: Processes are created only when a request is received and killed after a certain idle period. This is memory-efficient but can introduce latency for the first request after a period of inactivity.

For dynamic, the key parameters are:

  • pm.max_children: The maximum number of child processes that will be created. This is the most important setting. Set it based on your server’s RAM. A common formula is (Total RAM - RAM for OS/Nginx/DB) / Average RAM per PHP-FPM process. Monitor memory usage and adjust.
  • pm.start_servers: The number of child processes to start when PHP-FPM starts.
  • pm.min_spare_servers: The minimum number of idle processes to maintain.
  • pm.max_spare_servers: The maximum number of idle processes to maintain.

For static:

  • pm.max_children: The fixed number of child processes.

Example www.conf snippet for dynamic PM:

; /etc/php/8.1/fpm/pool.d/www.conf (adjust version as needed)

[www]
user = www-data
group = www-data
listen = /run/php/php8.1-fpm.sock ; Or a TCP socket like 127.0.0.1:9000

; Process management settings
pm = dynamic
pm.max_children = 150       ; Adjust based on RAM (e.g., 150 * ~20MB/process = ~3GB)
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.process_idle_timeout = 10s ; For dynamic, kills idle processes beyond this

; Request handling
request_terminate_timeout = 60s ; Timeout for a single script execution
; request_slowlog_timeout = 10s ; Enable slow log for debugging

; Other settings
chdir = /
catch_workers_output = yes
; php_admin_value[memory_limit] = 256M ; Example: override PHP.ini settings
; php_admin_flag[display_errors] = off ; For production

Nginx Configuration for PHP-FPM

Your Nginx server block (virtual host) needs to be configured to pass PHP requests to the PHP-FPM socket or TCP port. Ensure you have appropriate caching headers for static assets.

server {
    listen 80;
    server_name your_domain.com www.your_domain.com;
    root /var/www/your_app/public; # Adjust to your application's public directory
    index index.php index.html index.htm;

    # Enable Gzip compression for text-based assets
    gzip_static on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # Cache static assets for a long time
    location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|webp|woff|woff2|ttf|eot|otf)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
        try_files $uri =404;
    }

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf; # Standard Nginx snippet for FastCGI
        # Use the correct socket path or IP:Port
        fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Or: fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params; # Includes standard FastCGI parameters
    }

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

    # Optional: SSL configuration
    # listen 443 ssl;
    # ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
    # ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
    # include /etc/letsencrypt/options-ssl-nginx.conf;
    # ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

Gunicorn for Python Applications

When deploying Python web applications (e.g., Django, Flask) on DigitalOcean, Gunicorn is a popular WSGI HTTP Server. It’s robust and performs well when proxied by Nginx.

Gunicorn Configuration and Tuning

Gunicorn’s performance is primarily determined by its worker count and type. It’s typically run as a service managed by systemd.

Worker Types

  • sync: The default worker type. It’s simple but can block under heavy load. Each worker handles one request at a time.
  • gevent: Uses greenlets for concurrency. It’s asynchronous and can handle many requests concurrently, making it suitable for I/O-bound applications. Requires installing the gevent library.
  • eventlet: Similar to gevent, using greenlets for asynchronous I/O.
  • tornado: Uses the Tornado IOLoop.

For most modern applications, gevent or eventlet offer superior concurrency and performance, especially if your application spends significant time waiting for database queries or external API calls.

Worker Count

A common recommendation for the number of workers is (2 * Number of CPU Cores) + 1. However, this is a starting point. For I/O-bound applications using asynchronous workers (like gevent), you might need more workers to keep the CPU busy while others are waiting for I/O. Monitor your application’s performance and resource utilization.

Gunicorn Systemd Service Example

Create a systemd service file (e.g., /etc/systemd/system/gunicorn.service) to manage your Gunicorn process.

# /etc/systemd/system/gunicorn.service
[Unit]
Description=gunicorn daemon for my_project
After=network.target

[Service]
User=my_app_user
Group=www-data
WorkingDirectory=/home/my_app_user/my_project
ExecStart=/home/my_app_user/my_project/venv/bin/gunicorn \
    --workers 3 \
    --worker-class gevent \
    --bind unix:/run/gunicorn.sock \
    --log-level info \
    --access-logfile - \
    --error-logfile - \
    my_project.wsgi:application

# Adjust --workers and --worker-class based on your needs and server specs
# Example for 4 CPU cores with gevent: --workers 9 --worker-class gevent

Restart=always
RestartSec=5s

[Install]
WantedBy=multi-user.target

After creating the file, enable and start the service:

sudo systemctl daemon-reload
sudo systemctl start gunicorn
sudo systemctl enable gunicorn
sudo systemctl status gunicorn

Nginx Configuration for Gunicorn

Nginx will proxy requests to the Gunicorn socket specified in the service file.

server {
    listen 80;
    server_name your_python_app.com www.your_python_app.com;
    client_max_body_size 4G; # Adjust if you handle large uploads

    location /static/ {
        alias /home/my_app_user/my_project/static/; # Serve static files directly
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location /media/ {
        alias /home/my_app_user/my_project/media/; # Serve media files directly
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location / {
        proxy_set_header Host $http_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_pass http://unix:/run/gunicorn.sock; # Proxy to Gunicorn socket
    }
}

MongoDB Performance Tuning on DigitalOcean

MongoDB’s performance is heavily influenced by its configuration, hardware, and query patterns. On DigitalOcean, choosing the right droplet type (e.g., CPU-optimized, Memory-optimized) is a good first step.

Key MongoDB Configuration Parameters

The main configuration file is typically /etc/mongod.conf. Key parameters for performance tuning include:

  • storage.wiredTiger.engineConfig.cacheSizeGB: This is arguably the most critical setting. It defines the maximum amount of RAM that the WiredTiger storage engine can use for its cache. A common recommendation is to allocate 50% of the system’s RAM to the WiredTiger cache, leaving the other 50% for the OS and other processes. For example, on a 16GB droplet, set this to 8.
  • storage.journal.enabled: Should generally be true for durability. Disabling it can improve write performance but risks data loss on crashes.
  • operationProfiling.mode: Set to slowOp or all to enable profiling of slow queries. This is essential for identifying performance bottlenecks.
  • net.bindIp: Ensure this is set correctly, usually to 0.0.0.0 to listen on all interfaces or a specific private IP if using a firewall.
  • sharding.clusterRole: If running a sharded cluster, this defines the role of the mongod instance.
# /etc/mongod.conf
systemLog:
  destination: file
  path: /var/log/mongodb/mongod.log
  logAppend: true
  verbosity: 0 # 0 is default, higher values for more verbose logging

storage:
  dbPath: /var/lib/mongodb
  journal:
    enabled: true
  engine: wiredTiger
  wiredTiger:
    engineConfig:
      cacheSizeGB: 8 # Example: For a 16GB RAM droplet, allocate 8GB to cache

# For production, consider enabling profiling
# operationProfiling:
#   mode: slowOp # or 'all'
#   slowOpThresholdMs: 100 # Log operations slower than 100ms

net:
  port: 27017
  bindIp: 0.0.0.0 # Listen on all interfaces, or a specific IP

# Security settings (essential for production)
# security:
#   authorization: enabled

# Replication settings (for replica sets)
# replication:
#   replSetName: "rs0"

# Sharding settings (if applicable)
# sharding:
#   clusterRole: shardsvr
#   configsvr: false

Monitoring and Query Optimization

Effective MongoDB performance tuning relies heavily on monitoring and optimizing queries. Use the following tools and techniques:

  • mongotop: Provides a near real-time view of the read and write activity of collections.
  • mongostat: Provides a summary of MongoDB server statistics, including operations, network traffic, and memory usage.
  • explain(): The most crucial tool for query optimization. Run db.collection.find(...).explain("executionStats") on your slow queries to understand how MongoDB is executing them. Look for COLLSCAN (collection scan) and ensure indexes are being used effectively.
  • Indexes: Ensure appropriate indexes are created for your common query patterns. Use db.collection.getIndexes() to view existing indexes.
  • Slow Query Log: If profiling is enabled, analyze the slow query log (often found in /var/log/mongodb/mongod.log if operationProfiling.mode is set) to identify problematic queries.

Example of using explain():

// Connect to your database
// use mydatabase;

// Example query that might be slow without an index
var slowQuery = { status: "active", createdAt: { $lt: new Date("2023-01-01") } };

// Analyze the query execution
db.mycollection.find(slowQuery).explain("executionStats");

/*
Look for:
- indexName: Should show an index being used.
- stage: Should ideally be IXSCAN (index scan) not COLLSCAN (collection scan).
- nReturned: Number of documents returned.
- executionTimeMillis: Time taken for execution.
- totalKeysExamined: Number of index keys scanned.
- totalDocsExamined: Number of documents scanned.

If indexName is null or stage is COLLSCAN, you likely need to create an index.
*/

// Example of creating an index
db.mycollection.createIndex({ status: 1, createdAt: -1 });

Putting It All Together: A Holistic Approach

Achieving peak performance on DigitalOcean involves a layered approach. Start with the foundational configurations for Nginx, your application server (Gunicorn/PHP-FPM), and your database (MongoDB). Then, iteratively tune based on real-world monitoring and profiling. Remember that optimal settings are highly dependent on your specific application’s workload, traffic patterns, and the chosen DigitalOcean droplet size.

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

  • Step-by-Step: Diagnosing thread pools deadlock during concurrent ActiveRecord transaction processing on Linode Servers
  • Securing Your E-commerce APIs: Preventing SQL Injection (SQLi) in customized checkout queries in WooCommerce Implementations
  • Disaster Recovery 101: Architecting Auto-Failovers for MySQL and Ruby Deployments on Linode
  • High-Throughput Caching Strategies: Scaling MySQL for Perl Application APIs
  • Disaster Recovery 101: Architecting Auto-Failovers for DynamoDB and Laravel Deployments on DigitalOcean

Copyright © 2026 · Vinay Vengala