I used MAMP, then MAMP Pro, for years. In late March 2026, tired of diagnosing my breakdowns blindly, I asked Claude Code to rebuild the same thing natively: Apache, PHP-FPM and MySQL installed with Homebrew, 98 migrated vhosts, local HTTPS and a single command to drive it all. Here is the complete tutorial, written for my students who develop in PHP on a Mac.
Why I finally left MAMP Pro
MAMP has one real merit: it starts. You install an application, click a button, and an Apache server with PHP and MySQL answers on port 8888. For a first PHP course, that's perfect. I worked that way for years, upgrading to MAMP Pro to unlock virtual hosts.
Then the problems piled up. Not one spectacular bug: a permanent friction.
- Frozen versions - my MAMP Pro 6.8.1 install topped out at PHP 8.2.0, released in December 2022. To follow recent versions, you have to buy the application's major upgrade.
- An opaque configuration - MAMP Pro regenerates the Apache and PHP configuration files from its own templates every time the servers start. Any manual change ends up overwritten: the configuration does not belong to you.
- Impossible diagnosis - when a site stops responding, the interface offers one answer: restart everything and hope. No simple way to know which brick is at fault.
- Nothing is scriptable - no way to automate, version or properly document a configuration locked inside a graphical application. For handing an environment to students, that's a deal breaker.
In late March 2026, I stopped fighting. I opened Claude Code and asked it to rebuild my environment natively, brick by brick, with Homebrew. MAMP is still installed on my machine, out of caution. I have never opened it again.
Homebrew, the package manager macOS is missing
Homebrew is a package manager: a command line tool that installs, updates and uninstalls open source software on macOS. Where Linux has apt or dnf, macOS ships with nothing. Homebrew has been filling that gap since 2009 and has become the default tool of every developer on a Mac.
Three notions are enough to follow this tutorial:
- The formula - the installation recipe for a piece of software.
brew install phpdownloads and installs PHP with all its dependencies. - The Cellar - the folder where everything lands:
/opt/homebrewon Apple Silicon Macs. Nothing scatters across the system, everything uninstalls cleanly. - Services -
brew services start mysqlregisters MySQL as a service that starts with your session, through macOS launchd.
/usr/local there, not /opt/homebrew. Swap that prefix in every path of this article, or better: ask Homebrew itself with brew --prefix. The last Intel Mac laptops were sold in 2021, so a classroom may still have some.
Installing it takes one command, pasted into Terminal:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
The script asks for your password and installs Apple's Command Line Tools along the way if needed. Then check that everything is in order:
brew --version
brew doctor
For my students, the decisive argument is not technical: a Homebrew install can be documented. Every step of this tutorial is a command you can read, replay and fix. A click in an interface is none of that.
The target stack
Here is what we are going to build. Each brick is an independent Homebrew package, with its own configuration file and its own log:
Browser
|
v
Apache 2.4 ................. :80 / :443
| static files served directly
| *.php handed to PHP-FPM (fcgi proxy)
v
PHP-FPM 8.4 ................ 127.0.0.1:9000
|
v
MySQL 9.6 .................. 127.0.0.1:3306
(socket /tmp/mysql.sock)
PHP mail() -> msmtp -> MailHog :1025 (SMTP)
:8025 (web UI)
Two design choices matter. Apache runs as your macOS user (the User and Group directives): no more permission conflicts between the server and your files. And PHP does not live inside Apache: it runs as a separate service, PHP-FPM, to which Apache hands .php files over the FastCGI protocol. That is the architecture of modern production servers.
The versions as I write: Apache 2.4.66, PHP 8.4.12, MySQL 9.6.0, mkcert 1.4.4, MailHog 1.0.1. Yours may be newer, the commands do not change.
The installation, brick by brick
Everything below comes from my actual machine, not from a theoretical tutorial: the paths, ports and configuration blocks are the ones running on my Mac today. Allow about an hour the first time.
Install Apache
brew install httpd
Homebrew ships Apache configured on port 8080, to avoid asking for privileges. We want the real ports. Open /opt/homebrew/etc/httpd/httpd.conf and adjust these directives:
Listen 80
User yourname # your macOS username
Group staff
DirectoryIndex index.php index.html
In the same file, uncomment the modules the stack needs:
LoadModule proxy_module lib/httpd/modules/mod_proxy.so
LoadModule proxy_fcgi_module lib/httpd/modules/mod_proxy_fcgi.so
LoadModule ssl_module lib/httpd/modules/mod_ssl.so
LoadModule rewrite_module lib/httpd/modules/mod_rewrite.so
Since port 80 is privileged, Apache starts with sudo /opt/homebrew/bin/httpd -k start. It is the only service in the stack that requires sudo; all the others run as a plain user.
Then create the folder that will hold all your projects, with this command in Terminal:
mkdir -p ~/htdocs
Then reopen httpd.conf and paste the following block as is, at the end of the file, replacing yourname. Careful: it goes into httpd.conf, not into a .htaccess file. It is actually the other way around: its AllowOverride All line allows your future projects to use their own .htaccess files, which WordPress cannot live without. There is no .htaccess to create here.
<Directory "/Users/yourname/htdocs">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
Install PHP and PHP-FPM
brew install php
brew services start php
The formula installs PHP 8.4 together with PHP-FPM, its process manager. It listens on port 9000, as /opt/homebrew/etc/php/8.4/php-fpm.d/www.conf confirms:
listen = 127.0.0.1:9000
On the Apache side, one block is enough to hand every .php file to PHP-FPM. It is the bridge between the two bricks, added at the end of httpd.conf:
# === PHP-FPM Handler ===
Timeout 30
ProxyTimeout 30
<FilesMatch \.php$>
SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>
The two Timeout directives cap Apache's patience at 30 seconds. Remember that number: it will explain a classic trap later on.
Install MySQL
brew install mysql
brew services start mysql
MySQL listens over TCP on 127.0.0.1:3306 and on a UNIX socket, /tmp/mysql.sock. On a fresh install, root has no password: give it one.
mysql -u root
ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
Yes, root/root. A strong password brings nothing to a local database that only listens to your own machine; simplicity does. Every day-to-day command becomes mysql -u root -proot.
Create one vhost per project
A virtual host maps a local domain name to a folder. Each project gets its own .test domain, a reserved TLD that will never exist on the Internet. Three files to touch.
First, declare the domain in /etc/hosts:
127.0.0.1 nomduprojet.test
Next, the HTTP vhost in /opt/homebrew/etc/httpd/extra/httpd-vhosts.conf:
<VirtualHost *:80>
ServerName nomduprojet.test
DocumentRoot "/Users/yourname/htdocs/nomduprojet"
</VirtualHost>
Finally its HTTPS twin in httpd-ssl.conf, pointing to the shared certificate (we generate it right after):
<VirtualHost *:443>
ServerName nomduprojet.test
DocumentRoot "/Users/yourname/htdocs/nomduprojet"
SSLEngine on
SSLCertificateFile "/opt/homebrew/etc/httpd/ssl/local-dev.pem"
SSLCertificateKeyFile "/opt/homebrew/etc/httpd/ssl/local-dev-key.pem"
</VirtualHost>
Restart Apache (sudo /opt/homebrew/bin/httpd -k restart) and test with a diagnostic page:
echo '<?php phpinfo();' > ~/htdocs/nomduprojet/index.php
My configuration now holds 98 vhosts built on this template. I wrote none of them by hand: Claude Code read the MAMP Pro configuration and generated both files in one go. The header "Auto-generated from MAMP Pro configuration" is still visible at the top of the file.
Go HTTPS with mkcert
mkcert creates a local certificate authority, installs it in the macOS keychain, then issues certificates your browser accepts without a warning.
brew install mkcert
mkcert -install
I generate a single certificate for all my local domains, stored next to the Apache configuration:
mkdir -p /opt/homebrew/etc/httpd/ssl
cd /opt/homebrew/etc/httpd/ssl
mkcert -cert-file local-dev.pem -key-file local-dev-key.pem \
localhost nomduprojet.test autreprojet.test
Every SSL vhost points to that same pair of files. When a project lands, you add its domain to the list and regenerate: mine currently covers 104 domains. A *.test wildcard would also do the job if you would rather never touch it again.
Catch emails with MailHog
The last blind spot of local dev: emails. A cloned site sending real messages to real users during your tests is the textbook stupid accident. MailHog intercepts everything: it listens for SMTP on port 1025 and shows the captured messages in a web UI on port 8025.
brew install mailhog msmtp
brew services start mailhog
The bridge to PHP is msmtp, a minimal SMTP client. One line in /opt/homebrew/etc/php/8.4/php.ini redirects the mail() function:
sendmail_path = "/opt/homebrew/bin/msmtp --host=127.0.0.1 --port=1025 --from=dev@localhost -t"
Every mail() call in your projects now lands on http://localhost:8025. Not a single email ever leaves the machine.
devstack: the whole stack in one command
Four services, four ways to start them: exactly the kind of friction that drives people back to MAMP. So Claude Code wrote devstack, a 440-line bash script sitting in /usr/local/bin, that drives the whole thing:
devstack start # starts MySQL, PHP-FPM, MailHog, Apache
devstack stop # stops everything
devstack restart # hard restart (kills saturated PHP-FPM workers)
devstack status # tests the actual connectivity of each brick
devstack doctor site # diagnoses one specific project
devstack fix # unblocks stuck PHP-FPM workers
The subtlety is in status: it does not check that the processes exist, it tests that each service actually answers. A curl against port 80, a SELECT 1 against MySQL, an lsof on port 9000. A zombie process gets detected, not hidden.
=== Ports ===
:80 -> httpd (Apache)
:443 -> httpd (Apache)
:3306 -> mysqld (MySQL)
:9000 -> php-fpm (pool www)
:1025 -> MailHog (SMTP)
:8025 -> MailHog (web UI)
devstack doctor nomduprojet.test goes further: it detects a WordPress, reads the credentials from wp-config.php, tests the database connection, checks siteurl and home, counts the loaded stylesheets and surfaces the latest PHP errors from the Apache log. The kind of checks you run by hand, in no particular order, on a bad evening: the script runs them in order, every time.
The traps to know about
localhost is not 127.0.0.1
The nastiest trap of the migration. For MySQL, localhost means "go through the UNIX socket" and 127.0.0.1 means "go through TCP". A site configured under another environment, MAMP for instance, will look for the socket in the wrong place and fail with an incomprehensible "can't connect", while MySQL is running just fine. On this stack the rule is simple: 127.0.0.1 everywhere.
define('DB_HOST', '127.0.0.1'); // never 'localhost' locally
The Gateway Timeout of cloned WordPress sites
You clone a WordPress site from production, and everything answers 504 after 30 seconds, even wp-login. Remember the Timeout directives: Apache waits for PHP-FPM for 30 seconds, no more. The usual culprit is a security plugin like Wordfence, which attempts outbound calls to its API on every page load. Locally, the call drags on, the PHP-FPM worker stays busy, Apache gives up.
The cure: deactivate the plugin directly, without going through the admin, which is itself timing out:
wp plugin deactivate wordfence --path=/Users/yourname/htdocs/nomduprojet
brew services shows httpd as an error
That is normal. Apache is the only service started outside brew services, with sudo, because of port 80. The status column of brew services list is not authoritative for it; devstack status is.
The prompt to give Claude Code
My students all use Claude Code. So the temptation to delegate this whole tutorial to it is strong, and it is legitimate: that is exactly what I did. Here is the prompt I give them, to paste as is into a session:
Install a complete LAMP environment on this Mac with Homebrew,
without MAMP or any graphical application:
1. Apache (httpd formula) on ports 80 and 443, running as my
macOS user, with the proxy_fcgi, ssl and rewrite modules.
2. PHP, latest stable version, as PHP-FPM on 127.0.0.1:9000,
wired to Apache through a FilesMatch block.
3. MySQL as a brew service, with root/root locally.
4. One ~/htdocs folder for all my projects (AllowOverride All).
5. One HTTP vhost and one HTTPS vhost per project, on .test,
declared in /etc/hosts.
6. Local HTTPS with mkcert (authority installed in the keychain).
7. MailHog and msmtp to capture PHP's mail() function.
8. A devstack start|stop|restart|status script in /usr/local/bin.
Rules: explain each step to me before running it, ask for my
approval before any sudo command, and finish with a full test:
a phpinfo() page served over HTTPS on myproject.test, and a mail()
visible in MailHog on localhost:8025.
The rules at the end are not decorative. The AI will install everything, but the environment remains yours: every approved step is an understood step. I would recommend following this tutorial by hand once, then letting Claude Code replay it on the next machines. In that order.
What it changes day to day
- Transparency beats comfort. A configuration file you can read is worth more than an interface that hides it. My breakdowns have not disappeared: they have become diagnosable.
- Scriptable means teachable. This tutorial exists because the stack is text: commands, config files, a bash script. A MAMP configuration cannot be taught, only shown.
- The cost is zero. Every brick is free and open source. The MAMP Pro license mostly paid for the comfort of not learning.
- AI has changed the math. Building this stack by hand used to cost a day of scattered documentation; that is what made MAMP attractive. Claude Code built mine in late March 2026, 98-vhost migration included, while I was doing something else.
Who does what? The recap
Six bricks, six roles. Keep it handy:
- Homebrew - the package manager: it installs, updates and uninstalls every brick below. brew.sh
- Apache - the web server: it receives the browser's requests and serves your sites, one vhost per project. httpd.apache.org
- PHP and PHP-FPM - the language that runs your code; FPM, its process manager, answers Apache on port 9000. php.net
- MySQL - the database: it stores your applications' content, over TCP on port 3306. mysql.com
- mkcert - the certificate factory: it makes local HTTPS accepted by your browser. GitHub
- MailHog - the email net: it intercepts everything PHP sends and shows it on port 8025. GitHub