Run multiple local websites on a single Apache server — each with its own domain name, directory, and SSL certificate — on your MacBook Air M4 running macOS Golden Gate.
When your browser requests myapp.test, here is the chain of events that gets it to your project folder:
Apache reads the Host: header from the request, matches it against your ServerName directives in order, and serves files from the matching DocumentRoot. The /etc/hosts file (or dnsmasq) maps your custom domain to 127.0.0.1 so your Mac knows where to send the request.
These paths are identical on macOS 27 Golden Gate as on macOS 26 Tahoe. The built-in Apache config has not moved.
| File / Directory | Purpose |
|---|---|
/etc/apache2/httpd.conf | Main Apache config — enable modules and includes here |
/etc/apache2/extra/httpd-vhosts.conf | Virtual host definitions — add your sites here |
/etc/apache2/extra/httpd-ssl.conf | SSL/HTTPS virtual host config |
/etc/apache2/users/ | Per-user config files for ~/Sites access |
/Library/WebServer/Documents/ | Default document root (localhost) |
~/Sites/ | Your personal web projects folder |
/etc/hosts | Local DNS — maps .test domains to 127.0.0.1 |
/private/var/log/apache2/error_log | Apache error log |
/private/var/log/apache2/access_log | Apache access log |
Virtual hosts are disabled by default. You need to uncomment two lines in httpd.conf — one to enable name-based virtual hosts and one to include the vhosts config file.
# Always back up before editing sudo cp /etc/apache2/httpd.conf /etc/apache2/httpd.conf.bak sudo nano /etc/apache2/httpd.conf
# Remove the # from this line: LoadModule rewrite_module libexec/apache2/mod_rewrite.so
# Remove the # from this line: Include /private/etc/apache2/extra/httpd-vhosts.conf
In nano: use Ctrl+W to search for httpd-vhosts. Remove the # at the start of the line. Ctrl+O saves, Ctrl+X exits.
Every virtual host is a <VirtualHost> block inside httpd-vhosts.conf. Here is what each directive does:
Open the vhosts config file and add your site. Always keep a default catch-all as the first block — if no ServerName matches the request, Apache falls through to the first VirtualHost defined.
# Create the Sites directory if it doesn't exist mkdir -p ~/Sites/myapp # Create a quick test page echo '<h1>myapp.test is working on Golden Gate!</h1>' > ~/Sites/myapp/index.html # Find your actual username whoami
# Back up first sudo cp /etc/apache2/extra/httpd-vhosts.conf /etc/apache2/extra/httpd-vhosts.conf.bak sudo nano /etc/apache2/extra/httpd-vhosts.conf
# ── Default catch-all (MUST be first) ──────────────────── <VirtualHost *:80> ServerName localhost DocumentRoot "/Library/WebServer/Documents" <Directory "/Library/WebServer/Documents"> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory> </VirtualHost> # ── My First Project ───────────────────────────────────── <VirtualHost *:80> ServerName myapp.test ServerAlias www.myapp.test DocumentRoot "/Users/YOUR_USERNAME/Sites/myapp" ErrorLog "/private/var/log/apache2/myapp-error.log" CustomLog "/private/var/log/apache2/myapp-access.log" combined <Directory "/Users/YOUR_USERNAME/Sites/myapp"> Options Indexes FollowSymLinks MultiViews AllowOverride All Require all granted </Directory> </VirtualHost>
Replace YOUR_USERNAME with your actual macOS username (from whoami). The path must match exactly what's on disk.
sudo nano /etc/hosts # Add this line at the very bottom of the file: 127.0.0.1 myapp.test 127.0.0.1 www.myapp.test
# Test config for syntax errors first sudo apachectl configtest # Must say "Syntax OK" # Flush macOS DNS cache sudo dscacheutil -flushcache sudo killall -HUP mDNSResponder # Graceful restart sudo apachectl graceful # Test it open http://myapp.test
You should see your test page at http://myapp.test. Add more projects by repeating steps 1–5 with a new ServerName and DocumentRoot.
Add as many <VirtualHost> blocks as you need — one per project. Apache matches them top to bottom so the default catch-all must always remain first.
# ── 1. Default catch-all (always first) ────────────────── <VirtualHost *:80> ServerName localhost DocumentRoot "/Library/WebServer/Documents" </VirtualHost> # ── 2. Main app ─────────────────────────────────────────── <VirtualHost *:80> ServerName myapp.test DocumentRoot "/Users/YOU/Sites/myapp" ErrorLog "/private/var/log/apache2/myapp-error.log" CustomLog "/private/var/log/apache2/myapp-access.log" combined <Directory "/Users/YOU/Sites/myapp"> AllowOverride All Require all granted </Directory> </VirtualHost> # ── 3. Client site ──────────────────────────────────────── <VirtualHost *:80> ServerName client.test DocumentRoot "/Users/YOU/Sites/client" ErrorLog "/private/var/log/apache2/client-error.log" CustomLog "/private/var/log/apache2/client-access.log" combined <Directory "/Users/YOU/Sites/client"> AllowOverride All Require all granted </Directory> </VirtualHost> # ── 4. WordPress site ───────────────────────────────────── <VirtualHost *:80> ServerName wordpress.test DocumentRoot "/Users/YOU/Sites/wordpress" ErrorLog "/private/var/log/apache2/wp-error.log" <Directory "/Users/YOU/Sites/wordpress"> Options FollowSymLinks AllowOverride All Require all granted </Directory> </VirtualHost>
sudo nano /etc/hosts # Add all your local domains at the bottom: 127.0.0.1 myapp.test 127.0.0.1 client.test 127.0.0.1 wordpress.test
Add a second <VirtualHost *:443> block for each site that needs HTTPS. First generate a trusted cert with mkcert (see the HTTPS guide), then mirror your HTTP block with SSL directives added.
Generate one mkcert certificate covering all your local domains at once: sudo mkcert myapp.test client.test wordpress.test localhost 127.0.0.1
# HTTP block (port 80) — redirects to HTTPS <VirtualHost *:80> ServerName myapp.test Redirect permanent / https://myapp.test/ </VirtualHost> # HTTPS block (port 443) <VirtualHost *:443> ServerName myapp.test DocumentRoot "/Users/YOU/Sites/myapp" ErrorLog "/private/var/log/apache2/myapp-error.log" CustomLog "/private/var/log/apache2/myapp-access.log" combined ## SSL SSLEngine on SSLCertificateFile "/etc/apache2/ssl/myapp.test.pem" SSLCertificateKeyFile "/etc/apache2/ssl/myapp.test-key.pem" <Directory "/Users/YOU/Sites/myapp"> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory> </VirtualHost>
# First: install and set up mkcert if you haven't brew install mkcert nss mkcert -install # Generate a cert for your virtual host domain sudo mkcert -cert-file /etc/apache2/ssl/myapp.test.pem \ -key-file /etc/apache2/ssl/myapp.test-key.pem \ myapp.test www.myapp.test localhost
Instead of adding every new domain to /etc/hosts manually, dnsmasq automatically resolves every *.test domain to 127.0.0.1. Add a project, visit the domain — it just works.
brew install dnsmasq # Route all *.test domains to localhost echo 'address=/.test/127.0.0.1' >> /opt/homebrew/etc/dnsmasq.conf # Start dnsmasq as a system service sudo brew services start dnsmasq
sudo mkdir -p /etc/resolver echo 'nameserver 127.0.0.1' | sudo tee /etc/resolver/test # Flush DNS to activate sudo dscacheutil -flushcache sudo killall -HUP mDNSResponder
ping -c 1 anything.test # Should reply from 127.0.0.1 — no /etc/hosts entry needed! ping -c 1 newproject.test # Also 127.0.0.1 — any subdomain works automatically
Now when you add a new VirtualHost with ServerName newproject.test, you only need to create the folder and config — DNS resolves automatically. No more /etc/hosts edits needed.
| Problem | Cause | Fix |
|---|---|---|
| Domain loads wrong site | Default catch-all not first, or ServerName typo | Check block order in vhosts.conf. Default must be first. Check ServerName matches /etc/hosts exactly. |
| 403 Forbidden | Directory permissions wrong | Confirm Require all granted is inside the <Directory> block. Check the folder actually exists. |
| 404 Not Found | DocumentRoot path wrong or file missing | Run ls -la /path/to/your/docroot to verify the path and that index.html exists. |
| Domain doesn't resolve | Missing /etc/hosts entry or DNS cache stale | Add 127.0.0.1 yourdomain.test to /etc/hosts. Flush DNS: sudo dscacheutil -flushcache. |
| "It works!" always shows | vhosts include not uncommented in httpd.conf | Check Include /private/etc/apache2/extra/httpd-vhosts.conf is not commented out. |
| Apache won't start | Syntax error in config | Run sudo apachectl configtest — it will show the exact line number. |
| .htaccess not working | mod_rewrite not loaded or AllowOverride None | Uncomment mod_rewrite in httpd.conf. Set AllowOverride All in the Directory block. |
| Changes not reflected | Apache not restarted | Run sudo apachectl graceful after every config change. |
# Check config syntax sudo apachectl configtest # See which vhost Apache would use for a domain sudo apachectl -t -D DUMP_VHOSTS # See all active virtual hosts sudo httpd -t -D DUMP_VHOSTS 2>&1 # Watch error log in real time tail -f /private/var/log/apache2/error_log # Watch a site-specific error log tail -f /private/var/log/apache2/myapp-error.log # Verify /etc/hosts entry is correct grep myapp.test /etc/hosts # Test DNS resolution ping -c 1 myapp.test # Graceful restart (picks up all config changes) sudo apachectl graceful
sudo apachectl -t -D DUMP_VHOSTS # Example output: VirtualHost configuration: *:80 localhost (/etc/apache2/extra/httpd-vhosts.conf:3) *:80 myapp.test (/etc/apache2/extra/httpd-vhosts.conf:14) *:80 client.test (/etc/apache2/extra/httpd-vhosts.conf:26)
| Task | Command |
|---|---|
| Start Apache | sudo apachectl start |
| Stop Apache | sudo apachectl stop |
| Graceful restart (apply config changes) | sudo apachectl graceful |
| Test config for errors | sudo apachectl configtest |
| See all active virtual hosts | sudo apachectl -t -D DUMP_VHOSTS |
| Edit main config | sudo nano /etc/apache2/httpd.conf |
| Edit virtual hosts | sudo nano /etc/apache2/extra/httpd-vhosts.conf |
| Edit /etc/hosts | sudo nano /etc/hosts |
| Flush DNS cache | sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder |
| Watch error log live | tail -f /private/var/log/apache2/error_log |
| Watch access log live | tail -f /private/var/log/apache2/access_log |