1) Identify what’s installed vs used
which -a httpd apachectl
httpd -V
# shows SERVER_CONFIG_FILE and build path
brew --prefix
Read: /usr/sbin/httpd = Apple; /usr/local/opt/httpd/bin/httpd = Homebrew.
2) See what’s actually running (process + ports)
ps aux | egrep "[h]ttpd|[d]nsmasq|[m]ysqld|[m]ariadbd|[p]hp-fpm"
sudo lsof -nP -iTCP:80,443 -sTCP:LISTEN
sudo lsof -nP -iUDP:53
lsof -nP -iTCP:3306 -sTCP:LISTEN
Read: Path shows which binary owns the port (Apple vs Homebrew). Only one service should own each port.
3) Compare launchd jobs (system vs user)
brew services list
sudo launchctl list | egrep -i "httpd|dnsmasq|mysql|mariadb|php"
launchctl list | egrep -i "httpd|dnsmasq|mysql|mariadb|php"
launchctl print gui/$(id -u)/homebrew.mxcl.httpd 2>/dev/null | sed -n '1,120p'
sudo launchctl print system/homebrew.mxcl.httpd 2>/dev/null | sed -n '1,120p'
Read:
- system/ = LaunchDaemon (root), gui/ = LaunchAgent (user).
last exit code = 1/2= failed start (often privileged port or config mismatch).- Duplicates (same label in both domains) → conflicts & “Background Items” duplicates.
4) Validate Apache config & vhosts
/usr/local/opt/httpd/bin/httpd -t
/usr/local/opt/httpd/bin/httpd -t -D DUMP_VHOSTS | head -n 50
grep -nE '^[[:space:]]*Listen[[:space:]]+(80|443)\b' /usr/local/etc/httpd/*.conf /usr/local/etc/httpd/extra/*.conf
grep -nE '<VirtualHost[[:space:]]+\*:(80|443)>' /usr/local/etc/httpd/extra/*.conf
Read: If running as user, any Listen 80/443 or <VirtualHost *:80/443> will fail. After switching to 8080, make sure all vhosts match.
5) PHP-FPM wiring (common break)
ls -l /usr/local/var/run/php-fpm.sock 2>/dev/null
lsof -nP -iTCP:9000 -sTCP:LISTEN
# Check httpd.conf or included files for SetHandler proxy:unix:/usr/local/var/run/php-fpm.sock|fcgi://localhost/
6) Permissions on logs/run dirs
ls -ld /usr/local/var/log/httpd /usr/local/var/run/httpd
ls -l /usr/local/var/log/httpd | head
Read: If owned by root, user-mode httpd can’t open logs → instant exit.
7) DNS sanity (dnsmasq / dev TLDs)
scutil --dns | sed -n '1,150p' | grep -A2 'domain: test' || true
dig @127.0.0.1 proton.test +short
log show --style syslog --predicate 'process == "dnsmasq"' --last 1h
Read: Ensure a resolver for your dev TLD exists; dig should hit 127.0.0.1. VPNs/WARP/Screen Time can override DNS.
8) Error logs (ground truth)
sudo tail -n 200 /usr/local/var/log/httpd/error_log 2>/dev/null
# MySQL/MariaDB:
tail -n 200 /usr/local/var/mysql/*.err 2>/dev/null
Quick interpretations
brew services: httpd error 1(user) → trying to bind :80/443 or log dir perms.- Two httpd/dnsmasq entries in Background Items → duplicate LaunchAgent + LaunchDaemon.
- Nothing on :3306 but DB “running” → port conflict or different port; check
*.err. Syntax OKbut service dies → ports/permissions/duplicate launch jobs are the usual culprits.
Use the steps in order—by the time you finish #4–#6 you’ll know exactly why it fails and which single change will fix it.