Use one idempotent Ansible playbook to install Apache, PHP, and MySQL on an Ubuntu 24.04 VPS, start both services, enable them at boot, and verify that Apache can execute PHP. The example keeps the VPS address, SSH account, key, and privilege settings in inventory rather than hard-coding provider-specific defaults.
Antihost offers Linux VPS plans with full root access, unlimited traffic on a 1 Gbps port and a 14-day free trial.
This guide assumes a newly provisioned Ubuntu server with SSH access and a control machine running Ansible. It installs distribution packages from Ubuntu’s configured repositories. Firewall rules, TLS certificates, application deployment, database backups, and application-specific hardening remain separate tasks because their correct settings depend on what you will host.
As an Amazon Associate I earn from qualifying purchases.
Recommended Free Tools
What this playbook installs
In this guide, LAMP means Linux (Ubuntu), Apache HTTP Server, a MySQL-compatible database server, and PHP. The core package set for Ubuntu 24.04 is:
| Package | Purpose |
|---|---|
apache2 |
Web server that accepts HTTP requests. |
php |
PHP runtime and the distribution’s default PHP metapackage. |
libapache2-mod-php |
Apache integration that allows Apache to pass PHP files to the PHP runtime. |
php-mysql |
PHP driver for MySQL-compatible databases. |
mysql-server |
Database server supplied for the selected Ubuntu release. |
Ubuntu’s recommended mechanism for Debian packages is APT. Ansible’s ansible.builtin.apt module manages those packages, while ansible.builtin.service controls services through the service manager detected on the host. Use state: started and enabled: true for a repeatable “running now and after reboot” result; use state: restarted only when a change actually requires a restart.
#1 Best Overall
Prerequisites and deployment decisions
VPS requirements
- A provisioned Ubuntu 24.04 VPS with a routable address and an SSH server.
- An SSH user that can use
sudo, or a root login for the initial bootstrap. - Inbound TCP access for SSH (normally port 22) from your administration network. Open HTTP (80) and HTTPS (443) only when the site is ready to serve them.
- Enough disk space for the operating system, package cache, web files, logs, and database growth. The exact amount depends on your application and data set.
Control-node requirements
Install Ansible on your workstation or automation runner, then confirm the executable is available with ansible --version. The managed host must have Python and python3-apt; the APT module requires that Python package on Debian and Ubuntu systems. Most Ubuntu images include it, but the bootstrap below checks for it and installs it when necessary.
Choose the database and PHP integration deliberately
MySQL and MariaDB are broadly compatible, but an application can depend on a particular SQL dialect, authentication plugin, or server version. Confirm the application’s support matrix before replacing mysql-server with another package. The example uses Apache’s PHP module because it is the shortest single-VPS setup. PHP-FPM is a separate architecture: Apache proxies PHP requests to a pool, giving you different process isolation and tuning controls. Do not mix the two approaches casually; install and configure the integration your application expects.
Create an inventory with provider-specific values
Make a project directory and create inventory.ini. Keep the hostname, address, account, key, and escalation settings here so the playbook is portable between VPS providers.
[lamp]
web01 ansible_host=203.0.113.20
[lamp:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/lamp_ed25519
ansible_become=true
ansible_python_interpreter=/usr/bin/python3
Replace the example address, user, and key path. If the image permits root SSH but does not yet have a sudo-capable account, set ansible_user=root for the first run and remove that exception after creating your administrative account. Test connectivity before installing anything:
ansible -i inventory.ini lamp -m ansible.builtin.ping
A successful response contains "ping": "pong". An unreachable result usually means the address, SSH username, key permissions, cloud firewall, or server-side sshd configuration is wrong; fix that before troubleshooting Ansible modules.
Write the idempotent LAMP playbook
Save the following as lamp.yml. The first pre-task uses a raw SSH command because Ansible cannot use the APT module until the managed host has python3-apt. It is safe to run repeatedly: the command installs the package only when importing the Python module fails.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minute---
- name: Install and verify a LAMP stack
hosts: lamp
become: true
gather_facts: false
vars:
lamp_packages:
- apache2
- php
- libapache2-mod-php
- php-mysql
- mysql-server
pre_tasks:
- name: Ensure the APT Python bindings exist
ansible.builtin.raw: >-
/usr/bin/python3 -c 'import apt' 2>/dev/null ||
(apt-get update -y &&
DEBIAN_FRONTEND=noninteractive apt-get install -y python3-apt)
changed_when: false
- name: Gather facts after Python support is ready
ansible.builtin.setup:
tasks:
- name: Refresh the local package index and install LAMP packages
ansible.builtin.apt:
name: '{{ lamp_packages }}'
state: present
update_cache: true
cache_valid_time: 3600
- name: Ensure Apache is running and enabled at boot
ansible.builtin.service:
name: apache2
state: started
enabled: true
- name: Ensure MySQL is running and enabled at boot
ansible.builtin.service:
name: mysql
state: started
enabled: true
- name: Install a temporary PHP execution check
ansible.builtin.copy:
dest: /var/www/html/ansible-health.php
content: '<?php echo "ansible-php-ok";'
owner: root
group: root
mode: '0644'
- name: Check the Apache endpoint and PHP execution
ansible.builtin.uri:
url: http://127.0.0.1/ansible-health.php
method: GET
status_code: 200
return_content: true
register: php_check
changed_when: false
- name: Fail when Apache did not execute PHP
ansible.builtin.assert:
that:
- "'ansible-php-ok' in php_check.content"
fail_msg: 'Apache returned the PHP file without the expected output.'
- name: Remove the temporary PHP check
ansible.builtin.file:
path: /var/www/html/ansible-health.php
state: absent
The package task refreshes APT metadata and installs all five packages in one transaction. state: present does not reinstall packages that are already installed. The service tasks likewise make no change when the daemons are already running and enabled. The verification file is deliberately temporary, so it does not become an accidental public diagnostic endpoint.
Rank #2
Run the playbook safely
- Run a syntax check:
ansible-playbook -i inventory.ini lamp.yml --syntax-check. - Preview changes with a dry run:
ansible-playbook -i inventory.ini lamp.yml --check. Package installation and the raw bootstrap may be reported differently in check mode because they need to inspect the remote package manager. - Apply the configuration:
ansible-playbook -i inventory.ini lamp.yml. - Run the same command again. A correctly converged host should report no changes except for tasks whose environment genuinely changed. Re-running is the practical idempotency test.
The playbook’s HTTP check runs on the VPS against loopback, so it does not depend on DNS or an external firewall rule. From your own machine, you can make a separate check with curl http://203.0.113.20/ after allowing port 80. Do not expose the temporary health file; the playbook removes it before completion.
What each Ansible module is doing
ansible.builtin.apt
The APT module manages Debian and Ubuntu packages. update_cache: true refreshes the local package index from configured repositories before dependency resolution. cache_valid_time: 3600 avoids refreshing the index more than once per hour during repeated runs. Keep package names explicit for the Ubuntu release you selected; metapackages can change their dependencies when the distribution changes.
ansible.builtin.service
The service module controls services through the host’s detected service manager. On a normal Ubuntu 24.04 VPS that is systemd, but the generic module keeps the playbook independent of systemd-specific options. state: started is idempotent. state: restarted is not: it performs a restart every time the task runs. If you need systemd-only controls such as daemon reload behavior, use ansible.builtin.systemd_service explicitly.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteRepository modules
The example intentionally uses Ubuntu’s configured repositories and does not add a third-party source. If your application requires a package unavailable in the release repositories, the current Ansible documentation recommends ansible.builtin.deb822_repository for new repository definitions. ansible.builtin.apt_repository can add repositories but is deprecated in favor of the deb822 module. Adding a repository also creates a supply-chain and upgrade decision: pin versions, import the correct signing key through the supported mechanism, and test upgrades before applying them to production.
Configure Apache and deploy an application document root
The base packages provide Apache’s default virtual host and document root. For a real site, manage a named virtual-host file with Ansible rather than editing files interactively. A minimal template can define ServerName, DocumentRoot, directory permissions, and log paths. Notify a handler only when that file changes:
- name: Install the application virtual host
ansible.builtin.template:
src: app.conf.j2
dest: /etc/apache2/sites-available/app.conf
owner: root
group: root
mode: '0644'
notify: Reload Apache
- name: Enable the application virtual host
ansible.builtin.command: a2ensite app.conf
args:
creates: /etc/apache2/sites-enabled/app.conf
notify: Reload Apache
handlers:
- name: Reload Apache
ansible.builtin.service:
name: apache2
state: reloaded
Use a dedicated template for your domain and application path. A reload applies configuration changes without needlessly terminating existing workers. Validate the configuration before reloading when you add more complex directives; a failed configuration should stop the deployment rather than take down a working site.
Create application database credentials without exposing them
Do not place a database password in plain inventory, a public repository, task output, or a shell command that appears in process listings. Store secrets in Ansible Vault or an approved secret manager:
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #3
ansible-vault create group_vars/lamp/vault.yml
Define variables such as app_db_name, app_db_user, and app_db_password in that encrypted file. Invoke the playbook with --ask-vault-pass or your organization’s configured Vault credential helper. Keep database creation and grants in a separate role if several applications share the host, and grant each application only the privileges it needs.
Ubuntu images may configure the local database administrator for socket-based authentication rather than a password. Do not assume that changing the administrator password is required for an application user. Create a dedicated account for the application, test its connection, and document the authentication method expected by your framework.
Make the VPS survive reboots and routine reruns
- Enable services: the
enabled: trueflags create boot-time enablement; confirm withsystemctl is-enabled apache2 mysql. - Check current state: use
systemctl is-active apache2 mysqland inspectjournalctl -u apache2orjournalctl -u mysqlwhen a service fails. - Separate changes from restarts: notify reload or restart handlers from templates and configuration tasks instead of restarting on every playbook run.
- Pin intent, not accidental versions: use distribution packages unless you have a tested reason to pin a version. If you pin, include the Ubuntu release and upgrade policy in the role documentation.
- Keep state out of ad-hoc commands: represent packages, files, services, users, and firewall rules as Ansible resources so the next run can detect drift.
Security and operations that this base playbook does not solve
A working LAMP installation is not a production security baseline. Add these controls according to the application’s risk profile:
- Firewall: allow SSH only from administration networks where possible; expose 80 and 443 as needed; do not publish the database port to the internet unless there is a compelling, protected requirement.
- TLS: obtain and renew a certificate for the real hostname, redirect HTTP to HTTPS where appropriate, and test renewal before the first certificate expires.
- SSH: use key authentication, remove unused accounts, disable direct root login when your recovery process permits, and keep the operating system patched.
- Database hardening: remove test data and anonymous accounts where present, restrict listening addresses, use separate application credentials, and review authentication plugins against your framework.
- Backups: back up database contents and uploaded files to storage independent of the VPS. A backup is not a recovery plan until you have restored it and measured how long restoration takes.
- Monitoring and logs: watch disk space, memory, Apache error rates, PHP errors, database availability, and certificate expiry. Configure log rotation before logs can fill the root filesystem.
- Updates: schedule and test security updates. Package upgrades can change PHP extensions, Apache modules, or database behavior, so use staging for significant application changes.
Common failures and precise fixes
“Module apt has no attribute” or an import error
The managed host lacks python3-apt, or Ansible is using a different interpreter. Confirm ansible_python_interpreter=/usr/bin/python3 in inventory and run the bootstrap pre-task. If Python itself is absent from an unusually minimal image, install it through the provider’s console or cloud-init before running Ansible.
SSH works manually but Ansible cannot connect
Run ansible -i inventory.ini lamp -m ansible.builtin.ping -vvv. Check that the inventory address resolves to the VPS, the key file is readable only by its owner, the SSH user exists, and sudo does not require an interactive password. If a password is required, configure Ansible’s become prompt rather than putting the password in inventory.
Apache is active but PHP source appears in the browser
Confirm that libapache2-mod-php is installed, inspect enabled modules with apache2ctl -M, and verify that the requested file ends in .php. If you chose PHP-FPM instead of the Apache module, configure the proxy integration and PHP-FPM service; do not install both integrations without a deliberate routing plan.
The PHP check returns 403 or 404
Check the document-root permissions, enabled virtual host, and Apache error log. A custom virtual host may point somewhere other than /var/www/html; place the temporary check in that site’s actual document root or replace the check with an application-specific endpoint.
Rank #4
MySQL does not start
Inspect systemctl status mysql and journalctl -u mysql -b. Common causes include an exhausted disk, a port conflict, corrupted or incompatible data files, and configuration copied from another MySQL or MariaDB release. Do not delete the data directory as a first response; preserve it and restore from a known backup if recovery is required.
The second playbook run restarts everything
Search for state: restarted tasks or unconditional shell commands. Replace them with state: started and handlers notified only by changed configuration tasks. Also check whether a template renders nondeterministically, for example by embedding a timestamp, which would mark the file changed on every run.
When to split the web and database tiers
| Deployment | Advantages | Costs and cautions |
|---|---|---|
| One VPS for Apache, PHP, and MySQL | Lowest operational complexity, one playbook target, and simple local database connectivity. | Web traffic and database workloads compete for CPU, memory, disk I/O, and failure recovery. A VPS outage affects every tier. |
| Separate web and database hosts | Independent scaling, tighter network boundaries, and maintenance isolation. | Requires private networking, firewall rules, encrypted database connections where appropriate, monitoring across hosts, and more involved backup and deployment coordination. |
Start with one host when the workload and risk justify its simplicity. Split tiers when resource contention, availability requirements, or security boundaries make the additional operations worthwhile. The same Ansible role can target separate inventory groups, but database connection variables and firewall policy must then be explicit.
Manual commands versus an Ansible role
| Approach | Best fit | Limitation |
|---|---|---|
| One-off APT and systemctl commands | A disposable experiment or emergency recovery. | Hard to audit, repeat, or reproduce accurately after drift. |
| Single playbook | One VPS or a small number of equivalent hosts. | Can become difficult to maintain if application, security, and backup concerns are mixed together. |
| Reusable Ansible roles | Several environments, staged releases, or multiple applications. | Requires role boundaries, variable conventions, testing, and version control discipline. |
A practical progression is to keep the package and service tasks in a LAMP role, put virtual-host and PHP settings in an application role, and manage firewall, TLS, backups, and monitoring through separate roles. That structure lets you rerun only the part that changed while preserving the same idempotent behavior.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Rebuild, upgrade, or remove the stack
To upgrade packages, run the playbook after reviewing the target Ubuntu updates; the present state ensures required packages remain installed but does not itself perform a full distribution upgrade. Test PHP and database compatibility in a staging VPS before a production upgrade.
To retire the stack, first export and verify the database, copy application files and certificates, and confirm that DNS and traffic can be moved. Then remove the virtual host and packages deliberately. Do not use an automatic purge on a server that may contain data you have not independently backed up.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




