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: A Practical Guide

By Kokil Thapa | Last reviewed: September 2026

You maintain a handful of Ubuntu servers running Laravel, WordPress, or WooCommerce. Every new PHP version or SSL renewal still means SSH, copy-paste, and crossed fingers. Ansible Playbooks: A Practical Guide shows how to replace that manual work with repeatable YAML automation. Ansible is agentless, uses SSH, and fits teams already doing Linux system administration in Nepal or managing shared EC2 boxes. This page walks from your first playbook to production patterns I use alongside GitLab CI and Deployer.

What Is Ansible Playbooks: A Practical Guide for Server Automation?

An Ansible playbook is a ordered list of plays. Each play targets a host group and runs tasks through modules. You do not install an agent on servers. The control machine—your laptop or a CI runner—connects over SSH and pushes changes.

That model matches how many small teams operate. You already have root or sudo access on production. You already use Git for application code. Playbooks extend Git to infrastructure: versioned, reviewable, repeatable.

Core pieces you will use daily:

Ansible Playbook ArchitectureControl NodeLaptop or CI runnerInventoryHosts and groupsPlaybook YAMLPlays and tasksWeb Server GroupPHP-FPM, Nginx, LaravelDatabase GroupMySQL 9.7 or PostgreSQLAgentless SSH — no daemon on targets
Ansible Playbooks: control node reads inventory and YAML, then applies tasks over SSH to grouped servers

Official docs describe playbooks as the core configuration method in Ansible. Read the Ansible playbook introduction when you need module-level detail beyond this guide.

Where Ansible fits in a Laravel shop: Terraform or your cloud panel creates the VM. Ansible configures it—PHP 8.4, Composer 2.10, Redis 8.10, UFW, fail2ban. Deployer or GitLab CI deploys application code after the box is ready. That split is explained well in Terraform vs Ansible: provisioning vs configuration management.

How Do You Write Your First Ansible Playbook?

Install Ansible on Ubuntu 24.04 from the control machine:

sudo apt update
sudo apt install -y ansible
ansible --version

Create a project layout:

mkdir -p ~/ansible-laravel/{inventory,playbooks,roles,group_vars}
cd ~/ansible-laravel

Define inventory

File: inventory/production.ini

[web]
web1 ansible_host=203.0.113.10 ansible_user=deploy

[web:vars]
ansible_python_interpreter=/usr/bin/python3
php_version=8.4
app_user=deploy

Use IP addresses or DNS names. Match the SSH user you already use for automating server setup with Ansible playbooks.

Write a minimal playbook

File: playbooks/base.yml

---
- name: Base Ubuntu hardening and packages
  hosts: web
  become: true
  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install base packages
      ansible.builtin.apt:
        name:
          - curl
          - git
          - ufw
          - fail2ban
        state: present

    - name: Allow OpenSSH through UFW
      community.general.ufw:
        rule: allow
        name: OpenSSH

    - name: Enable UFW
      community.general.ufw:
        state: enabled

Run it

ansible-playbook -i inventory/production.ini playbooks/base.yml --check
ansible-playbook -i inventory/production.ini playbooks/base.yml

Always dry-run with --check first on unfamiliar hosts. Add -l web1 to limit scope during testing.

Playbook Execution FlowRun CommandParse YAMLMatch HostsSSH ConnectTask: okAlready correctTask: changedState updatedTask: failedStop playIdempotent modules return ok when nothing changesRe-run safely after manual edits or drift
Each Ansible playbook task reports ok, changed, or failed — the basis of safe repeat runs

How Do You Structure Ansible Playbooks for Production PHP and Laravel Servers?

A single 400-line playbook becomes unmaintainable fast. Split by concern. This mirrors how I organise automation for sites like Notary Kathmandu and other sister properties on shared infrastructure.

ansible-laravel/
├── ansible.cfg
├── inventory/
│   ├── production.ini
│   └── staging.ini
├── group_vars/
│   ├── all.yml
│   └── web.yml
├── playbooks/
│   ├── site.yml
│   ├── web.yml
│   └── db.yml
└── roles/
    ├── common/
    ├── php_fpm/
    ├── nginx/
    └── deploy_user/

Entry playbook site.yml imports others:

---
- import_playbook: web.yml
- import_playbook: db.yml

Variables in group_vars

File: group_vars/web.yml

php_packages:
  - "php{{ php_version }}-fpm"
  - "php{{ php_version }}-mysql"
  - "php{{ php_version }}-redis"
  - "php{{ php_version }}-xml"
  - "php{{ php_version }}-mbstring"
  - "php{{ php_version }}-curl"

nginx_server_name: example.com
laravel_path: /var/www/example/current

Validate variable files with the JSON formatter tool when converting from other formats. YAML indentation errors are the top cause of playbook failures.

Template a config file

Roles/nginx/templates/site.conf.j2:

server {
    listen 80;
    server_name {{ nginx_server_name }};
    root {{ laravel_path }}/public;

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php{{ php_version }}-fpm.sock;
    }
}

Task using the template module:

- name: Deploy Nginx vhost
  ansible.builtin.template:
    src: site.conf.j2
    dest: "/etc/nginx/sites-available/{{ nginx_server_name }}.conf"
    owner: root
    group: root
    mode: "0644"
  notify: Reload nginx

Handlers run once at the end of the play when notified. That avoids reloading Nginx twelve times in one run.

PHP-FPM role task example

- name: Install PHP-FPM packages
  ansible.builtin.apt:
    name: "{{ php_packages }}"
    state: present

- name: Ensure PHP-FPM is running
  ansible.builtin.service:
    name: "php{{ php_version }}-fpm"
    state: started
    enabled: true

For Laravel 13 you need PHP 8.3 or higher. Laravel 12 runs on PHP 8.2+. Pin the version in group vars and keep staging aligned with production.

Deeper PHP-specific examples live in Ansible playbooks for PHP server provisioning. Pair that with application deploy docs from web development services when onboarding a new client server.

How Do You Run Ansible Playbooks Safely on Live Servers?

Production automation demands discipline. A playbook run as root can lock you out or wipe data. Treat playbooks like application code: branches, reviews, and staged rollout.

  1. Test on staging first. Mirror production PHP, MySQL, and OS versions.
  2. Use --check and --diff. Preview changes before applying them.
  3. Limit blast radius with -l. Run one host, verify, then the group.
  4. Encrypt secrets with Vault. Never commit plain database passwords.
  5. Tag tasks. Run subsets: --tags nginx during a cert renewal.
  6. Log runs in CI. GitLab CI or Jenkins archives output for audits.

Ansible Vault for secrets

ansible-vault create group_vars/web/vault.yml
ansible-vault edit group_vars/web/vault.yml

Reference vaulted vars normally in tasks. Run with:

ansible-playbook -i inventory/production.ini playbooks/site.yml --ask-vault-pass

Store the vault password in your CI secret store. See also Ansible Vault for secrets for rotation patterns.

ansible.cfg quality-of-life

[defaults]
inventory = inventory/production.ini
roles_path = roles
retry_files_enabled = False
host_key_checking = True

[privilege_escalation]
become = True
become_method = sudo

Disable host key checking only in disposable lab VMs. On client production boxes, verify host keys.

Idempotency: Desired vs Actual StateBefore Playbook RunPHP 8.3 installedUFW disabled manuallyStale Nginx configAfter Playbook RunPHP 8.4 upgradedUFW enabled againNginx reloadedSecond run: all tasks report okNo duplicate installs or service restarts unless drift returnsSafe to schedule nightly or after deploys
Ansible Playbooks converge servers toward declared state and stay safe on repeat execution

On a booking platform like Adventure Third Pole Trek, drift happens when someone hot-fixes PHP-FPM pool settings over SSH. A weekly playbook run restores known-good config without redeploying Laravel code.

Ansible Playbooks vs Shell Scripts: Which Should You Use?

Shell scripts feel faster on day one. Playbooks pay off by month three when you manage three or more servers and repeat the same stack.

CriterionAnsible PlaybookAd-hoc Shell Script
IdempotencyBuilt into modules; safe re-runsYou must write checks yourself
Agent requiredNo — SSH onlyNo — SSH only
Multi-host rolloutNative inventory and parallelismLoop hosts manually
Secret handlingAnsible Vault integratedEnvironment vars, often leaked
Learning curveYAML + module docsBash familiarity
CI integrationClean in GitLab CI, JenkinsWorks but harder to lint
Best fitOngoing config managementOne-off migrations

For a broader tool comparison, read Ansible vs Puppet vs Chef vs Salt. Puppet and Chef need agents. Ansible wins for lean teams running Ubuntu and Laravel.

Ansible does not replace Terraform for creating VPCs or RDS instances. Use Infrastructure as Code with Terraform for cloud resources. Use Ansible for what lives inside the VM.

Real Stack: Laravel Server PipelineTerraformAnsiblePlaybooksGitLab CIDeployer 7Live AppAnsible configures once per server lifecyclePHP-FPM, Nginx, Redis, UFW, deploy user, logrotateCI plus Deployer run on every application releasePlaybook re-run after OS upgrades or security patchesPattern used on shared EC2 legal-tech and eCommerce sites
Ansible Playbooks handle server baseline; GitLab CI and Deployer ship Laravel application code on top

CI example snippet

GitLab CI job to lint and dry-run:

ansible_lint:
  stage: test
  script:
    - ansible-lint playbooks/
    - ansible-playbook playbooks/site.yml -i inventory/staging.ini --check

Wire this into a full pipeline using ideas from building a CI/CD pipeline with Jenkins or your existing GitLab config. Ongoing server care belongs in support and maintenance services when you hand off to a client team.

Common gotchas I see in production

  • Wrong Python on target. Set ansible_python_interpreter explicitly on minimal images.
  • Becoming root without NOPASSWD sudo. Test ansible web -m ping -b before long plays.
  • Using command or shell everywhere. Prefer dedicated modules; they report changed status correctly. See the copy module documentation for file tasks.
  • Skipping handlers. Use meta: flush_handlers when the next task depends on a reload.
  • Mixing app deploy into Ansible. Keep Composer and artisan migrate in Deployer. Ansible owns the platform layer.

Database recovery playbooks differ from config playbooks. For Postgres backups and restore drills, cross-read PostgreSQL point-in-time recovery playbook. Container teams may later add GKE or EKS, but bare-metal and VPS Laravel hosting remains common in Nepal.

Key Takeaways

  • Ansible Playbooks declare desired server state in YAML and apply it over SSH with no agent.
  • Start with inventory, a small base playbook, --check, then expand into roles for PHP, Nginx, and security.
  • Split Terraform (cloud resources) from Ansible (OS and middleware) from Deployer (Laravel releases).
  • Encrypt secrets with Ansible Vault and run playbooks from CI with linting and staged inventory.
  • Re-run playbooks safely after drift; idempotent modules return ok when nothing needs changing.
  • Prefer modules over raw shell tasks, tag heavily, and test on staging before production limits go wide.

People Also Ask

Do I need to install anything on my servers to use Ansible playbooks?

No agent is required. Target hosts need SSH access, Python 3 (usually preinstalled on Ubuntu), and sudo for tasks that manage packages or services. The control node runs Ansible and connects outbound.

Can Ansible playbooks deploy Laravel application code?

They can, but most teams should not. Use Ansible for PHP-FPM, web server, firewall, and system users. Use Deployer, GitLab CI, or similar for Composer install, migrations, and symlink releases. Mixing both in one playbook blurs rollback boundaries.

How is Ansible Vault different from putting secrets in .env?

.env lives on the server and holds runtime app config. Vault encrypts infrastructure secrets—root DB passwords, API keys for provisioning—in Git so playbooks stay versioned without exposing credentials in plain text.

Is Ansible still relevant if we use Docker or Kubernetes?

Yes for hybrid setups. Many Laravel and WordPress clients still run on VPS or EC2 instances. Ansible configures those nodes efficiently. Containers shift some duties, but someone must still prepare hosts or golden images. Compare approaches in multi-cloud architecture guide before you over-engineer a small fleet.

Ship Repeatable Servers, Not Repeatable SSH Sessions

Ansible Playbooks: A Practical Guide is only complete once it runs in your repo—not after you read it once. Build a staging inventory this week. Convert your last manual LAMP setup into roles. Vault the passwords. Hook a dry-run into CI. The payoff is fewer midnight SSH fixes and servers that match each other.

If you want help designing playbooks for a Laravel fleet, PHP upgrade, or migration off manual setup, review the portfolio of production sites I maintain and reach out via contact us. For automation beyond servers—AI workflows, API glue, or full platform builds—see AI integration and automation services and read more on the blog, including notes from about me on fifteen years of shipping production systems. Client feedback on reliable delivery appears in customer reviews.

Frequently Asked Questions

An Ansible playbook is an ordered list of plays in YAML. Each play targets a host group and runs tasks through modules over SSH to declare desired server state.

No agent is required. Target hosts need SSH access, Python 3 (usually preinstalled on Ubuntu), and sudo for tasks that manage packages or services. The control node runs Ansible and connects outbound.

It dry-runs the playbook and previews changes without applying them. Always run with --check first on unfamiliar hosts, and add --diff when you want to see file-level changes before applying.

Install Ansible on your control machine with apt on Ubuntu 24.04, then create a project layout with inventory, playbooks, roles, and group_vars folders. Define hosts in an inventory file such as production.ini with ansible_host, ansible_user, and group variables like php_version. Write a small playbook with apt, ufw, and fail2ban tasks, dry-run with ansible-playbook --check, then apply without --check. Limit scope during testing with -l and a single host name.

Split concerns instead of one long file. Use an entry playbook like site.yml that imports web.yml and db.yml, keep variables in group_vars, and move reusable work into roles such as common, php_fpm, nginx, and deploy_user. Pin PHP in group vars—for Laravel 13 use PHP 8.3 or higher; Laravel 12 runs on PHP 8.2+. Template Nginx vhosts with the template module, notify handlers for service reloads, and keep staging aligned with production versions.

Treat playbooks like application code: branch, review, and test on staging that mirrors production PHP, MySQL, and OS versions. Preview with --check and --diff, limit blast radius with -l on one host first, encrypt secrets with Ansible Vault, tag tasks for partial runs, and archive output from GitLab CI or Jenkins. Verify host keys in ansible.cfg on client production boxes rather than disabling host_key_checking. Re-run weekly to fix SSH drift without redeploying application code.

Shell scripts feel faster on day one, but playbooks pay off around month three when you manage three or more servers and repeat the same stack. Playbooks give built-in idempotency, native multi-host inventory, Ansible Vault for secrets, and cleaner CI linting. Shell scripts suit one-off migrations. For ongoing Ubuntu and Laravel config management, Ansible wins for lean teams that already operate over SSH without wanting agents.

They can, but most teams should not. Use Ansible for the platform layer: PHP-FPM, Nginx, firewall rules, system users, and baseline packages. Use Deployer or GitLab CI for Composer install, artisan migrate, and symlinked releases. Mixing application deploy into Ansible blurs rollback boundaries and makes it harder to separate server config from code shipping.

A Laravel .env file lives on the server and holds runtime application config such as APP_KEY and database credentials the app reads at request time. Ansible Vault encrypts infrastructure secrets—root database passwords, provisioning API keys—inside Git so playbooks stay versioned and reviewable without plain-text credentials in the repository. Reference vaulted vars normally in tasks and pass the vault password at run time or from your CI secret store.

Terraform or your cloud panel creates the VM, VPC, and other cloud resources. Ansible configures what lives inside the VM: PHP 8.4, Composer 2.10, Redis 8.10, UFW, and fail2ban. Deployer or GitLab CI then ships Laravel application code after the box is ready. Ansible does not replace Terraform for provisioning; the split keeps cloud resource creation separate from OS and middleware configuration.

Yes for hybrid setups. Many Laravel and WordPress clients still run on VPS or EC2 instances, and Ansible configures those nodes efficiently over SSH. Containers shift some duties, but hosts or golden images still need preparation. For a small fleet in Nepal running bare-metal or shared EC2 boxes, Ansible remains practical without over-engineering Kubernetes for problems a handful of playbooks solve cleanly.

Idempotency means re-running the same playbook converges servers toward declared state without breaking what already matches. Ansible modules report ok, changed, or failed on each task, so a second run skips work that is already correct. That is why weekly playbook runs can restore PHP-FPM or Nginx settings after someone hot-fixes over SSH, without redeploying Laravel code or duplicating package installs.

Roles are reusable bundles of tasks, templates, handlers, and defaults grouped by concern. Once a base playbook grows past a few dozen lines, split PHP-FPM install, Nginx vhost templating, and deploy-user setup into roles like php_fpm and nginx. Your entry playbook imports smaller playbooks, and roles keep repeated automation consistent across staging and production inventories. Ansible Galaxy also offers community roles when you do not want to write everything from scratch.

Wrong Python on minimal images—set ansible_python_interpreter explicitly in inventory. Becoming root without NOPASSWD sudo—test ansible web -m ping -b before long plays. Using command or shell everywhere instead of apt, copy, template, and service modules breaks correct changed reporting. Skipping handlers when the next task depends on a reload—use meta flush_handlers. Mixing Composer and artisan migrate into Ansible instead of Deployer creates unclear rollback boundaries. YAML indentation errors remain the top cause of playbook failures.

Add a test-stage job that runs ansible-lint on playbooks and ansible-playbook site.yml against a staging inventory with --check. Store the Ansible Vault password in your CI secret store and pass it at run time. Archive job output for audits. Wire this alongside your existing Deployer deploy pipeline: CI validates and dry-runs infrastructure YAML before application code ships, so server baseline and Laravel releases stay in separate, reviewable workflows.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: