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.

Ansible Playbooks for PHP Server Provisioning

By Kokil Thapa | Last reviewed: August 2026

Manually configuring production servers is a liability that compounds with every deployment. Ansible Playbooks for PHP Server Provisioning eliminate configuration drift by codifying your infrastructure into repeatable, version-controlled YAML files. Whether you are deploying a legal-tech portal or a high-traffic WooCommerce store, automation ensures your Ubuntu 24.04 environment is identical every time. This guide covers the exact playbook structure I use to provision secure, optimized LEMP stacks for Laravel and WordPress applications.

Before writing complex automation, it helps to understand the baseline architecture required for modern PHP applications. If you are new to backend infrastructure or looking to understand the stack these playbooks build, my overview of Laravel development services covers the application-side requirements that drive these server configurations. The goal of provisioning is to create a foundation where the application code can run securely and performantly.

How do you structure Ansible Playbooks for PHP Server Provisioning?

A common mistake in early automation attempts is dumping every task into a single monolithic file. In practice, maintainable Ansible Playbooks for PHP Server Provisioning separate concerns into roles or distinct task files. This modularity allows you to reuse the "nginx" configuration across multiple projects while swapping out database or application-specific variables.

Ansible Project Structureinventory/hosts.ymlgroup_vars/all.ymlroles/php-fpm/tasks/main.ymlsite.yml (Entry Point)Modular roles enable reuse across staging and production
Recommended directory layout for scalable Ansible Playbooks for PHP Server Provisioning

Your entry point, typically site.yml, should orchestrate these roles rather than define logic directly. This separation makes debugging significantly easier when a specific component like Redis or Nginx fails during a provisioning run. For teams managing multiple client sites, this structure also allows you to override variables per environment without duplicating code.

Defining the inventory and variables

Never hardcode IP addresses or credentials in your playbooks. Use an inventory file to define targets and group_vars to manage environment-specific settings. This is critical when managing both staging and production servers for the same project.

# inventory/production.yml
all:
  children:
    webservers:
      hosts:
        prod-web-01:
          ansible_host: 203.0.113.10
          php_version: "8.4"
          site_domain: "example.com"
      vars:
        env: production
        enable_opcache: true

What are the essential packages for a modern PHP server?

The package list for a production PHP server has evolved significantly. As of 2026, Ubuntu 24.04 LTS is the standard base, and PHP 8.4 is the current stable release offering the best performance for Laravel 12 and Symfony 7 applications. Your playbook must explicitly target the correct PPA or repository to avoid installing outdated system defaults.

ComponentRecommended Version (2026)Ansible Package NamePurpose
PHP Runtime8.4php8.4-fpmProcess manager for PHP execution
Web ServerNginx 1.26+nginxReverse proxy and static asset serving
DatabaseMySQL 8.4 / MariaDB 11mysql-serverPrimary relational data store
CachingRedis 7.4redis-serverSession storage and queue backend
SSL ManagementCertbot 2.xcertbot python3-certbot-nginxAutomated Let's Encrypt certificates

When installing PHP extensions, be precise. A generic php-mysql package might pull in an incompatible version if multiple PHP versions coexist on the server. Always prefix extensions with the specific version number, such as php8.4-mysql or php8.4-gd. This prevents subtle runtime errors where the extension loads for CLI but not for FPM.

Handling multi-version PHP installations

In my experience maintaining legacy systems alongside new builds, servers often need to run PHP 8.1 and 8.4 simultaneously. Ansible handles this well by parameterizing the version string. Ensure your Nginx configuration template references the correct socket path dynamically based on the host variable.

- name: Install PHP packages
  apt:
    name:
      - "php{{ php_version }}-fpm"
      - "php{{ php_version }}-cli"
      - "php{{ php_version }}-mysql"
      - "php{{ php_version }}-xml"
      - "php{{ php_version }}-mbstring"
      - "php{{ php_version }}-curl"
      - "php{{ php_version }}-zip"
      - "php{{ php_version }}-bcmath"
      - "php{{ php_version }}-intl"
    state: present
    update_cache: yes

How do you configure Nginx and PHP-FPM securely?

Security in Ansible Playbooks for PHP Server Provisioning isn't just about firewalls; it's about minimizing the attack surface of the application stack. Default configurations for Nginx and PHP-FPM are rarely suitable for production. You must disable dangerous functions, restrict file access, and tune worker processes for your specific hardware.

Secure Request FlowClient RequestNginxRate LimitingSSL TerminationHeader SecurityPHP-FPMUnix SocketApp Response
Security boundaries in Ansible Playbooks for PHP Server Provisioning prevent direct backend access

I always configure PHP-FPM to listen on a Unix socket rather than a TCP port. Sockets offer better performance on localhost and eliminate the risk of accidentally exposing the FPM port to the public internet. Additionally, disabling functions like exec, shell_exec, and passthru in php.ini mitigates damage if an attacker achieves code injection.

Tuning worker processes for stability

Default FPM pools often spawn too many workers for small VPS instances common in Nepal and Southeast Asia. Use the pm = dynamic setting and calculate pm.max_children based on available RAM divided by average process size (typically 30-50MB for Laravel). Over-provisioning leads to swap thrashing, which kills performance faster than any code inefficiency.

# templates/www.conf.j2
[www]
user = www-data
group = www-data
listen = /run/php/php{{ php_version }}-fpm.sock
listen.owner = www-data
listen.group = www-data

pm = dynamic
pm.max_children = {{ fpm_max_children | default(10) }}
pm.start_servers = {{ fpm_start_servers | default(2) }}
pm.min_spare_servers = {{ fpm_min_spare_servers | default(1) }}
pm.max_spare_servers = {{ fpm_max_spare_servers | default(3) }}

php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[expose_php] = off

How do you handle secrets and SSL in automated provisioning?

Managing secrets is where many automation efforts fail security audits. Never commit database passwords or API keys to your Git repository. For Ansible Playbooks for PHP Server Provisioning, Ansible Vault provides encrypted storage for sensitive variables that decrypts only at runtime using a master key or CI/CD secret.

SSL certificate management should be fully automated. Manual renewal is a ticking time bomb. Your playbook should install Certbot and trigger certificate generation as part of the initial provision, but crucially, it must also set up the systemd timer or cron job for automatic renewal. On a recent legal-tech portal project, we standardized on Certbot's Nginx plugin because it automatically modifies the vhost config and reloads the service, reducing human error.

Integrating Vault with CI/CD pipelines

When running these playbooks from GitLab CI or GitHub Actions, pass the vault password via a protected environment variable. Never store the vault password file itself in the repo. This pattern aligns with the deployment strategies discussed in my guide on CI/CD pipeline setup, where secret hygiene is paramount for maintaining trust with clients handling sensitive legal or financial data.

# Encrypted variable example
# ansible-vault encrypt_string 'SuperSecretDbPass' --name 'mysql_root_password'

- name: Configure MySQL root password
  mysql_user:
    name: root
    password: "{{ mysql_root_password }}"
    login_unix_socket: /var/run/mysqld/mysqld.sock
    state: present

Why is idempotency critical for PHP server automation?

Idempotency means running the same playbook ten times produces the same result as running it once. Without this property, your automation is destructive. In PHP provisioning, non-idempotent tasks often manifest as duplicate Nginx config blocks, reset file permissions breaking uploads, or unnecessary service restarts causing downtime.

Non-Idempotent (Dangerous)Run 1: Installs PHP ✓Run 2: Reinstalls PHP + Resets Config ✗Run 3: Duplicate VHost Entries ✗Idempotent (Safe)Run 1: Installs PHP ✓Run 2: Verifies State (No Change) ✓Run 3: Verifies State (No Change) ✓
Idempotency ensures Ansible Playbooks for PHP Server Provisioning remain safe to re-run

Always use Ansible modules (apt, template, copy) instead of raw shell commands. Modules have built-in state checking. If you must use shell or command, include a creates or removes argument so Ansible knows when to skip execution. This discipline prevents the "it works on my machine but breaks staging" syndrome that plagues teams transitioning from manual setup scripts.

Validating configuration before applying

For Nginx and PHP-FPM, always validate syntax before reloading. A malformed config pushed via automation can take down every site on the server. Use the validate parameter in the template module to run nginx -t or php-fpm8.4 -t before the file is actually replaced. This safety net has saved me countless times during late-night maintenance windows.

- name: Deploy Nginx virtual host
  template:
    src: vhost.conf.j2
    dest: "/etc/nginx/sites-available/{{ site_domain }}.conf"
    owner: root
    group: root
    mode: '0644'
    validate: '/usr/sbin/nginx -t -c %s'
  notify: Reload Nginx

Conclusion

Reliable Ansible Playbooks for PHP Server Provisioning transform server management from a source of anxiety into a competitive advantage. By structuring your automation around idempotent roles, securing secrets properly, and targeting current software versions like PHP 8.4 and Ubuntu 24.04, you build infrastructure that supports business growth rather than hindering it. The initial investment in writing clean playbooks pays dividends every time you deploy a new feature or recover from a failure.

If you need help designing a provisioning strategy for your Laravel or WordPress infrastructure, or want to audit your existing automation for security gaps, contact me to discuss your specific requirements. I regularly help teams in Nepal and globally establish robust DevOps foundations that scale with their business.

Frequently Asked Questions

Ansible Core 2.16 or higher is recommended for full PHP 8.4 and Ubuntu 24.04 support. While older versions may function, 2.16+ includes updated apt modules and Python 3.12 compatibility needed for modern server provisioning without deprecation warnings or module failures during playbook execution.

Freelance rates typically range from Rs 3,000 to Rs 6,000 per hour (~USD 22–45). A complete PHP server provisioning playbook suite usually takes 15–25 hours depending on complexity, totaling roughly Rs 75,000–150,000 (~USD 550–1,100) for a production-ready setup including testing and documentation.

Use both. Ansible provisions and configures the base server, PHP-FPM, Nginx, databases, and security hardening. Deployer handles application-specific deployment tasks like symlinked releases, asset building, cache clearing, and zero-downtime swaps. In my experience maintaining multiple client sites, separating infrastructure provisioning from application deployment prevents configuration drift and simplifies rollbacks when things break.

Use the geerlingguy.php role with the php_version variable set per host or group in your inventory. For side-by-side installations on Ubuntu 24.04, enable the Ondřej Surý PPA and specify packages like php8.3-fpm and php8.4-fpm explicitly. Configure separate PHP-FPM pool files with distinct socket paths so Nginx can route different virtual hosts to specific PHP versions without conflicts.

The most frequent issues are missing universe/multiverse repositories causing package-not-found errors, incorrect PHP-FPM pool ownership preventing socket creation, and opcache configuration syntax changes between PHP versions. Always validate your playbook against a fresh Vagrant or Docker container before running against production. I have debugged many deployments where the playbook worked on 22.04 but failed on 24.04 due to systemd service name changes or default config path relocations.

Never store secrets in plain-text YAML. Use ansible-vault to encrypt sensitive variables or integrate with HashiCorp Vault for dynamic secret generation. For smaller projects, encrypted vault files committed to Git work well. On client projects I maintain, we use environment-specific vault passwords stored outside the repository and passed via CI/CD pipeline secrets, ensuring developers can run playbooks locally without accessing production credentials.

Yes, using conditional tasks and role variables. Define a web_server variable in your inventory (apache or nginx) and include the appropriate role conditionally. However, in practice I recommend standardizing on one web server per project unless you have a specific reason to support both. Maintaining dual web server configurations doubles your testing surface and increases the chance of subtle behavioral differences causing production bugs that only appear under one server type.

Use the geerlingguy.php role which checks extension state before installing, or write custom tasks with the php_extension module that verifies installation via php -m before attempting changes. Avoid raw shell commands like pecl install without creates or removes parameters. Idempotent playbooks should produce zero changed tasks on consecutive runs. When troubleshooting non-idempotent behavior, check if the detection logic matches the actual installed state, especially for PECL extensions compiled from source.

Use Molecule with Docker or Vagrant drivers to spin up disposable test containers matching your target OS. Write Testinfra or Ansible assert tasks verifying PHP version, loaded extensions, FPM socket existence, and Nginx configuration validity. Run molecule test in CI before merging playbook changes. This catches issues like missing dependencies or incompatible config directives before they reach staging. On real projects, this testing step has prevented countless production outages caused by untested playbook modifications.

Ansible provisions the server but should not handle application deployment cache invalidation; that belongs to Deployer or your CI pipeline. However, your Ansible playbook must configure opcache.validate_timestamps=0 in production and ensure PHP-FPM has permission to reload. After deployment, trigger systemctl reload php8.4-fpm rather than restart to avoid dropping active connections. The reload signal gracefully spawns new workers with fresh opcache while existing requests complete on old workers.

At minimum: geerlingguy.php for PHP installation and configuration, geerlingguy.nginx or geerlingguy.apache for the web server, geerlingguy.mysql or geerlingguy.postgresql for databases, geerlingguy.redis for caching, and geerlingguy.security for SSH hardening and fail2ban. Add geerlingguy.certbot for SSL certificates. These roles are actively maintained, support current OS and PHP versions, and follow security best practices. Avoid abandoned roles; check last commit date and issue activity before adopting any community role.

Use the geerlingguy.certbot role with certbot_create_standalone_stop_services set to false if Nginx is already running. Define domains and email in certbot_certs variable. For existing Nginx setups, use the webroot authenticator instead of standalone to avoid stopping the web server. Schedule automatic renewal via certbot_renewal_command with a systemd timer or cron job configured by the role. Always test certificate issuance against Let's Encrypt staging environment first to avoid rate limits during playbook development.

Configure pm = dynamic with pm.max_children calculated as available RAM divided by average PHP process size (typically 30–50MB for Laravel). Set pm.start_servers, pm.min_spare_servers, and pm.max_spare_servers proportionally. Enable opcache with opcache.memory_consumption=256, opcache.interned_strings_buffer=16, and opcache.max_accelerated_files=20000. Disable opcache.validate_timestamps in production. These values depend on your workload; benchmark with wrk or ab after provisioning to validate settings match actual memory usage and request patterns.

Audit the current server configuration first by documenting installed packages, PHP ini overrides, FPM pool settings, and Nginx vhost configs. Create an Ansible playbook matching the existing state exactly, then run it in check mode to identify discrepancies. Fix mismatches incrementally rather than forcing Ansible's desired state onto a working server. Test thoroughly in a cloned environment before applying to production. This brownfield approach prevents downtime; I have seen teams cause outages by assuming their manual config matched what they thought was running.

For a single static server you manage manually, yes. But if you plan to add staging environments, replicate setups for clients, or rebuild after failures, Ansible pays for itself quickly. Even for one server, having infrastructure as code means recovery from disk failure takes minutes instead of days of manual reconfiguration. The tipping point is usually the second server or first disaster recovery scenario. Start simple with a single playbook file; adopt roles and advanced patterns only when complexity justifies them.

Share this article

Quick Contact Options
Choose how you want to connect me: