We always tell new engineers the same thing about Ansible. A tutorial shows you one playbook that installs one thing. Actual work is never like that. An actual deployment is a list of playbooks, each one doing a single job, run in a clear order, with actual decisions made at every step. Here is how we set up a PHP and MySQL server on a fresh Ubuntu box, and what we think about at each step.
Before we touch the web stack, we set up the basics on the host. A deploy user that is not root, locked down SSH, a firewall with only the ports we need open, and automatic security updates. We treat this as its own playbook and run it first on every new server, no matter what that server will end up doing. That way every machine starts from the same known baseline. We can also run it again later on an old server to check that nothing has drifted.
- name: Install PHP and extensions
apt:
name:
- "php{{ php_version }}"
- "php{{ php_version }}-mysql"
- "php{{ php_version }}-opcache"
- "php{{ php_version }}-mbstring"
# list every extension the app actually needs, by name
state: present
We always set the PHP version as a variable, not as a fixed package name, and we name every extension the app needs instead of installing a generic php package. Six months later, when we set up a second server, this is the difference between a playbook that builds the same stack every time and one that quietly drifts depending on whatever PHP version the system happens to ship that month. We also tune memory limit, upload size, and the error log in the same playbook. These are the settings everyone forgets when a server is set up by hand, and they are exactly the ones that cause a strange failure three weeks later, the first time someone tries to upload a large file.
set +o history
export DB_ROOT_PASSWORD='your-actual-password'
set -o history
ansible-playbook -i hosts.ini install-mysql.yml --ask-become-pass
unset DB_ROOT_PASSWORD
history -c && history -w
vars:
db_root_password: "{{ lookup('env', 'DB_ROOT_PASSWORD') | default('', true) }}"
pre_tasks:
- name: Refuse to run without a root password
ansible.builtin.fail:
msg: "Set DB_ROOT_PASSWORD before running this play."
when: db_root_password == ''
This habit is worth keeping. The password lives in an environment variable for the length of one command, never inside the playbook file, and we switch off shell history around the export so it never ends up saved anywhere. The setup step refuses to run at all if the variable is empty, which is a safe failure instead of a quiet one with a blank password. We pair the database install with the usual hardening steps too, removing test accounts, dropping the test database, and limiting root access to the local machine only.
A database admin tool, a process supervisor, system monitoring, and automatic backups are all separate jobs. We treat each one as its own playbook instead of one giant script that tries to finish the whole server in one go. That way each piece can be tested on its own, run again on its own, and skipped on servers that do not need it. A worker machine that only runs background jobs does not need phpMyAdmin. A database server does not need a process supervisor meant for application workers.
Every playbook in our list ends with something that proves it worked, not just a message saying Ansible found no errors. A request to a test page that shows the right PHP version and extensions. A database connection test that shows the login and grants are correct. A simple request to the monitoring agent. We treat the final state as something we check, not something we assume.
The actual test of a playbook list is not that it worked once. It is that it builds the same working server a second time, on a machine nobody has touched by hand first. If the middle steps need any manual fix on the second run that was not needed on the first, the list is not finished yet. Something was set up by hand the first time and never written down in a playbook.
That is the whole habit, really. Write it down, run it twice, and trust the second run more than the first.
Maybeach Tech builds and runs deployment pipelines like this one for teams running their own servers. Get in touch if you want help with yours.