Understanding the WordPress Docker “403 Forbidden” .htaccess Error on macOS: A Deep Dive
Executive Summary
When deploying a WordPress site using Docker Compose on macOS, developers frequently encounter a frustrating 403 Forbidden error with the message: “Server unable to read htaccess file, denying access to be safe.” This issue is particularly maddening because the .htaccess file exists, has the correct permissions, and works perfectly on production servers or Linux systems. This comprehensive guide explains exactly why this happens and how to fix it permanently.
Part 1: Understanding the Root Causes
Root Cause #1: Apache’s AllowOverride Configuration
What is AllowOverride?
Apache’s AllowOverride directive is a security feature that controls whether the server will process directives found in .htaccess files. Think of it as a gatekeeper: even if a .htaccess file exists on disk with perfect syntax, Apache simply refuses to read it if AllowOverride is set to None.
The Default Problem
In most standard Apache installations (including official Docker images like php:apache), the default Apache configuration looks like this:
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride None # ← THE CULPRIT
Require all granted
</Directory>
Why This Creates a 403 Error
When AllowOverride None is set, Apache behaves defensively:
- A request comes in for a URL (e.g.,
/blog/my-post/) - Apache looks for the file
/var/www/html/blog/my-post/– it doesn’t exist - WordPress uses
.htaccessURL rewriting to rewrite this toindex.php?p=123 - But Apache refuses to read the
.htaccessfile becauseAllowOverride None - Apache has no rewrite rules to apply
- Apache returns a generic 403 Forbidden error
The Security Reasoning
Apache’s developers set AllowOverride None as a default for security reasons:
- If a malicious actor gains write access to a
.htaccessfile, they could modify it to execute code, bypass authentication, or redirect traffic - By requiring
AllowOverrideto be explicitly enabled in the main Apache configuration (which typically requires root/admin access), the security burden is moved to a single, more protected location - This prevents unauthorized
.htaccessfiles from modifying server behavior
The WordPress Problem
WordPress absolutely depends on .htaccess file rewriting for several critical features:
- Pretty Permalinks: Converting
/blog/my-post/into/index.php?p=123 - Category/Tag URLs: Handling
/category/technology/URLs - Hiding index.php: Making URLs cleaner
- REST API routing: Rewriting requests to
/wp-json/endpoints - Password protection: Certain security-related features rely on
.htaccess
Root Cause #2: Missing mod_rewrite Module
What is mod_rewrite?
mod_rewrite is an Apache module that provides URL rewriting functionality. It processes the rewrite rules contained in .htaccess files. Even if Apache reads your .htaccess file, it cannot execute the rewrite rules without this module.
Why It’s Often Missing
Many minimal Docker images (including Alpine Linux-based PHP images) don’t include mod_rewrite by default to reduce image size. The assumption is that developers will add what they need.
The Cascading Failure
This is a classic two-layer problem:
- Layer 1: Apache doesn’t read
.htaccess(AllowOverride issue) → 403 error - Layer 2: Even if Layer 1 is fixed, if
mod_rewriteisn’t enabled, the rewrite rules in.htaccesswon’t execute
Both problems can exist simultaneously and must both be fixed for WordPress to function correctly.
Root Cause #3: macOS Docker Bind Mount File Locking Issues
Understanding the Architecture
This is where things get truly complex. Let’s understand how Docker works on macOS:
macOS (Darwin) Kernel
↓
Docker Desktop for Mac (native application)
↓
Linux VM (using Hypervisor Framework)
↓
Docker containers (Linux processes)
Docker is fundamentally a Linux technology. On Linux, running Docker is native because the kernel is Linux. On macOS, Docker Desktop creates a lightweight Linux virtual machine using the Hypervisor Framework and runs containers inside that VM.
The File System Bridging Problem
When you use a bind mount on macOS:
volumes:
- ./wordpress:/var/www/html
Here’s what actually happens:
- Docker Desktop intercepts the mount request
- It cannot simply mount the macOS filesystem into the Linux VM (different kernels, different filesystems – APFS vs ext4)
- Instead, Docker Desktop uses a file sharing mechanism to synchronize files between macOS and the Linux VM
- This mechanism has changed over Docker versions: originally it used
osxfs, then moved togRPC FUSE, now usesVirtioFS
The File Locking Issue
Different operating systems have fundamentally different file locking semantics:
macOS (BSD-style locking):
- Uses BSD advisory locks
- Locks are “advisory” – a process can ignore them
- Multiple readers can hold locks simultaneously
- Locks are more permissive
Linux (POSIX-style locking):
- Uses POSIX mandatory locks
- Locks are enforced by the kernel
- More restrictive behavior
- Different lock conflict resolution
When VirtioFS (or previous mechanisms) tries to bridge these two systems, conflicts arise.
When the Deadlock Occurs
The specific error message in the document reveals the exact moment:
[core:alert] /var/www/html/.htaccess: Error reading /var/www/html/.htaccess
at line 1: Resource deadlock avoided
This occurs when:
- PHP processes (FPM, running in the container) execute code that reads files from
/var/www/html/ - Apache processes simultaneously try to read the same files
- Multiple requests come in at the same time, each trying to access
.htaccessand other files - The file sharing mechanism tries to enforce consistency between macOS and Linux locking semantics
- It detects a potential deadlock condition (circular lock dependencies)
- To be safe, it reports “Resource deadlock avoided” and denies access
Common Patterns That Trigger This:
- WordPress loading plugins (many
requirestatements) - WordPress theme functions.php loading (file includes)
- Repeated
file_get_contents()calls on the same files - Multiple HTTP requests hitting the server simultaneously
- File modification triggers (especially plugin/theme updates through WordPress admin)
Why This Doesn’t Happen on Linux
On Linux servers (your production environment), the filesystem is native ext4, the kernel is native Linux, and all locking semantics are consistent. There’s no translation layer causing conflicts.
On your macOS development machine with Docker, the translation layer introduces this fragility.
Part 2: Detailed Solutions and Implementations
Solution 1: Fixing Apache Configuration in the Dockerfile
What We’re Doing
We’re making two Apache configuration changes inside the Dockerfile so they apply automatically whenever the container starts:
- Enabling the
mod_rewritemodule - Changing
AllowOverride NonetoAllowOverride All
The Dockerfile Solution
# At the appropriate place in your Dockerfile (typically after the base image)
# This works for the official PHP Apache images
RUN a2enmod rewrite && \
sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
Breaking Down the Commands
Command 1: a2enmod rewrite
a2enmod rewrite
a2enmodstands for “Apache 2 enable module”- This enables the mod_rewrite Apache module
- It works by creating a symbolic link in Apache’s mods-enabled directory
- The module must be present in the Apache installation (it usually is in official PHP images)
Command 2: The sed replacement
sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
Let’s break this down piece by piece:
sed -i: Stream editor with in-place file modification'/<Directory \/var\/www\/>/,/<\/Directory>/': Address range – this tells sed to only apply the following command between the line containing<Directory /var/www/>and the line containing</Directory>s/AllowOverride None/AllowOverride All/: Substitute command – replace the first occurrence of “AllowOverride None” with “AllowOverride All”
What Gets Changed
Before:
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
After:
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
Security Considerations
You might wonder: if AllowOverride None is a security best practice, are we making our Docker container less secure by changing it to AllowOverride All?
The answer is nuanced:
- In production, ideally you’d want
AllowOverride LimitorAllowOverride FileInfo(more restrictive thanAll) - However, for Docker containers, the security model is different:
- The container filesystem is ephemeral and isolated
- Only your code (or code you explicitly add) exists in the container
- There’s no concept of “untrusted users” with write access to the WordPress directory
- The container is typically run as a non-root user anyway
- A more secure but slightly more complex approach:
RUN a2enmod rewrite && \
sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride FileInfo/' /etc/apache2/apache2.conf
AllowOverride FileInfo permits only certain safe directives in .htaccess files (like mod_rewrite rules) but not dangerous ones (like php_value directives).
Complete Example Dockerfile
FROM php:8.2-apache
# Install PHP extensions needed for WordPress
RUN apt-get update && apt-get install -y \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
mysql-client \
git \
&& rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install gd mysqli
# Enable Apache modules and configure AllowOverride
RUN a2enmod rewrite && \
sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
# Set up WordPress directory
WORKDIR /var/www/html
Solution 2: Using Named Volumes Instead of Bind Mounts
Understanding the Difference
Bind Mounts:
volumes:
- ./wordpress:/var/www/html
- Maps a directory from your macOS machine directly into the container
- Changes on macOS appear in the container (through the file sharing mechanism)
- Changes in the container appear on macOS (through the file sharing mechanism)
- The file sharing mechanism causes the locking issues we discussed
Named Volumes:
volumes:
- wordpress_files:/var/www/html
volumes:
wordpress_files:
- Creates a managed volume inside the Docker Desktop’s Linux VM
- The volume exists only within Docker’s VM, not synced with macOS
- Eliminates the file sharing mechanism entirely
- No locking conflicts between macOS and Linux semantics
Why Named Volumes Solve the Problem
By using named volumes, you remove the macOS → Linux file system translation layer entirely. Here’s the comparison:
With Bind Mounts (Problematic):
macOS filesystem (APFS)
↓ [VirtioFS Translation + Locking]
Linux VM filesystem (ext4)
↓
Container process trying to read .htaccess
↓
Locking conflict detection
↓
"Resource deadlock avoided"
With Named Volumes (Solved):
Linux VM filesystem (ext4)
↓
Container process trying to read .htaccess
↓
Normal Linux file locking
↓
✅ Works perfectly
Docker Compose Configuration
Before (Problematic):
version: '3.8'
services:
wordpress:
image: php:8.2-apache
container_name: wordpress
ports:
- "8000:80"
volumes:
- ./wordpress:/var/www/html # ❌ Bind mount
- ./wp-config.php:/var/www/html/wp-config.php
environment:
- WORDPRESS_DB_HOST=mysql
- WORDPRESS_DB_NAME=wordpress
After (Fixed):
version: '3.8'
services:
wordpress:
image: php:8.2-apache
container_name: wordpress
ports:
- "8000:80"
volumes:
- wordpress_files:/var/www/html # ✅ Named volume
environment:
- WORDPRESS_DB_HOST=mysql
- WORDPRESS_DB_NAME=wordpress
mysql:
image: mysql:8.0
container_name: wordpress-db
volumes:
- mysql_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=wordpress
volumes:
wordpress_files:
mysql_data:
Trade-offs Analysis
Advantages of Named Volumes:
- ✅ Eliminates file locking issues completely
- ✅ Better performance (no translation layer overhead)
- ✅ Works consistently across Windows, macOS, Linux
- ✅ Docker manages volume lifecycle
- ✅ Easy to backup:
docker run --rm -v wordpress_files:/data -v $(pwd):/backup ubuntu tar czf /backup/wordpress.tar.gz -C /data .
Disadvantages of Named Volumes:
- ❌ Files are stored inside Docker Desktop’s Linux VM
- ❌ Can’t easily edit files from macOS (can’t open in VS Code directly)
- ❌ Requires
docker cpordocker execto access files - ❌ Slightly more complex workflow during development
Accessing Files in Named Volumes
Method 1: Using docker exec to run editor inside container
docker exec -it wordpress bash
cd /var/www/html
nano wp-config.php
Method 2: Using docker cp to copy files
# Copy FROM container TO macOS
docker cp wordpress:/var/www/html/wp-config.php ./wp-config.php
# Edit on macOS, then copy back
docker cp ./wp-config.php wordpress:/var/www/html/wp-config.php
Method 3: Using docker exec with a text editor command
docker exec wordpress bash -c 'cat > /var/www/html/test.txt << EOF
Your content here
EOF'
Solution 3: Using Mutagen for Live File Synchronization (Alternative)
What is Mutagen?
Mutagen is a tool specifically designed for development workflows that need the flexibility of bind mounts but without the file locking issues. It’s a file synchronization system that runs alongside Docker.
How Mutagen Works
Instead of the kernel’s file sharing mechanism, Mutagen:
- Runs as a background daemon
- Uses intelligent bidirectional sync (not just copying)
- Understands file changes on both sides
- Syncs changes when it detects them
- Handles conflicts intelligently
- Works around the file locking issues because it doesn’t use real-time kernel integration
Installation
# Using Homebrew on macOS
brew install mutagen
brew install mutagen-compose # For Docker Compose integration
Configuration File (mutagen.yml)
Create a mutagen.yml file in your project root:
# Mutagen configuration for WordPress development
sync:
# Default synchronization settings
defaults:
mode: "two-way-resolved" # Bidirectional sync, auto-resolve conflicts
# WordPress core files and uploads
wordpress-files:
alpha: "./wordpress" # Your local macOS directory
beta: "docker://wordpress/var/www/html" # Container target
# What to sync
includes:
- "/" # Sync everything by default
# What NOT to sync
ignores:
- ".git"
- ".gitignore"
- ".DS_Store"
- "node_modules"
- "vendor"
- "wp-content/cache"
- "wp-content/backup"
# Watch for changes and sync automatically
watch-mode: "portable" # Works cross-platform
watch-poll-interval: 5 # Poll every 5 seconds
# Optional: Just sync your theme if you have massive uploads
theme-only:
alpha: "./wordpress/wp-content/themes/mudos-theme"
beta: "docker://wordpress/var/www/html/wp-content/themes/mudos-theme"
ignores:
- ".DS_Store"
- ".git"
Using Mutagen
# Start Mutagen sync
mutagen project start
# Check sync status
mutagen project status
# Watch for sync issues
mutagen project monitor
# Stop Mutagen
mutagen project stop
# View detailed logs
mutagen project logs
Comparing Approaches
| Approach | Setup | Performance | File Access | Best For |
|---|---|---|---|---|
| Bind Mount | Simple | ❌ Slow on macOS | ✅ Direct | Quick testing (if it works) |
| Named Volume | Moderate | ✅ Fast | ❌ Need docker exec | Production-like setup |
| Mutagen | Complex | ✅ Fast | ✅ Direct + Synced | Active development |
Part 3: Step-by-Step Implementation Guide
Complete Example: Fixed WordPress Docker Setup
Directory Structure
project/
├── docker-compose.yml
├── Dockerfile
├── mutagen.yml (optional)
└── wordpress/
└── (WordPress files will be stored here or in named volume)
Step 1: Create the Dockerfile
# Dockerfile
FROM php:8.2-apache
# Install system dependencies
RUN apt-get update && apt-get install -y \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
mysql-client \
libmcrypt-dev \
zip \
unzip \
git \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install -j$(nproc) \
gd \
mysqli \
pdo \
pdo_mysql \
json
# Copy PHP configuration
COPY php.ini /usr/local/etc/php/conf.d/php.ini
# Enable Apache modules
RUN a2enmod rewrite && \
a2enmod headers && \
a2enmod http2
# Fix Apache configuration for .htaccess
RUN sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
# Set working directory
WORKDIR /var/www/html
# Set proper permissions
RUN chown -R www-data:www-data /var/www/html
Step 2: Create php.ini for WordPress
# php.ini
upload_max_filesize = 100M
post_max_size = 100M
memory_limit = 256M
max_execution_time = 300
default_charset = UTF-8
Step 3: Create Docker Compose File (Named Volumes Approach)
# docker-compose.yml
version: '3.8'
services:
wordpress:
build:
context: .
dockerfile: Dockerfile
container_name: mudos-wordpress
restart: unless-stopped
ports:
- "8000:80"
- "443:443"
volumes:
- wordpress_files:/var/www/html
- ./ssl:/etc/apache2/ssl:ro # Optional: SSL certificates
environment:
- WORDPRESS_DB_HOST=mysql
- WORDPRESS_DB_NAME=wordpress
- WORDPRESS_DB_USER=wordpress
- WORDPRESS_DB_PASSWORD=wordpress_pass
- WORDPRESS_AUTH_KEY=your-key-here
- WORDPRESS_SECURE_AUTH_KEY=your-key-here
depends_on:
- mysql
networks:
- wordpress-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
mysql:
image: mysql:8.0
container_name: mudos-mysql
restart: unless-stopped
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=root_password
- MYSQL_DATABASE=wordpress
- MYSQL_USER=wordpress
- MYSQL_PASSWORD=wordpress_pass
networks:
- wordpress-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
phpmyadmin:
image: phpmyadmin:latest
container_name: mudos-phpmyadmin
restart: unless-stopped
ports:
- "8080:80"
environment:
- PMA_HOST=mysql
- PMA_USER=wordpress
- PMA_PASSWORD=wordpress_pass
depends_on:
- mysql
networks:
- wordpress-network
volumes:
wordpress_files:
driver: local
mysql_data:
driver: local
networks:
wordpress-network:
driver: bridge
Step 4: Initialize Everything
# Navigate to project directory
cd your-project
# Build the Docker image
docker compose build --no-cache
# Start all services
docker compose up -d
# Check if services are running
docker compose ps
# View logs to debug any issues
docker compose logs -f wordpress
# Download WordPress (if using named volume)
docker exec mudos-wordpress bash -c 'cd /tmp && \
wget https://wordpress.org/latest.tar.gz && \
tar -xzf latest.tar.gz && \
cp -r wordpress/* /var/www/html/ && \
chown -R www-data:www-data /var/www/html'
# Or if you have WordPress locally, copy it
docker cp ./wordpress/. mudos-wordpress:/var/www/html/
docker exec mudos-wordpress chown -R www-data:www-data /var/www/html
Step 5: Verification
# Test HTTP response
curl -I http://localhost:8000/
# Should return something like:
# HTTP/1.1 301 Moved Permanently
# OR for fresh WordPress:
# HTTP/1.1 302 Found
# Check mod_rewrite is enabled
docker exec mudos-wordpress apache2ctl -M | grep rewrite
# Output: rewrite_module (shared)
# Check AllowOverride setting
docker exec mudos-wordpress grep -A 5 '<Directory /var/www/>' /etc/apache2/apache2.conf
# Should show: AllowOverride All
# Check .htaccess is readable
docker exec mudos-wordpress ls -la /var/www/html/.htaccess
# Test WordPress is responding
docker exec mudos-wordpress curl -I http://localhost/
# Check for Apache errors
docker exec mudos-wordpress tail -20 /var/log/apache2/error.log
Part 4: Troubleshooting Guide
Issue: Still Getting 403 Error After Fixing
Diagnostic Steps:
# Step 1: Verify the Dockerfile changes applied
docker exec wordpress grep AllowOverride /etc/apache2/apache2.conf
# Should show: AllowOverride All (not None)
# Step 2: Check if you actually rebuilt
docker images
# Your image should have a recent timestamp
# Step 3: Verify the container is using the new image
docker ps
# Check the image column
# Step 4: Check Apache error log for specific error
docker exec wordpress tail -50 /var/log/apache2/error.log
Solution: If AllowOverride still shows “None”, you didn’t rebuild. Run:
docker compose down
docker compose build --no-cache
docker compose up -d
Issue: .htaccess Still Says Permission Denied
Possible Causes:
- File ownership issue:
docker exec wordpress ls -la /var/www/html/.htaccess # Should show: -rw-r--r-- www-data:www-dataFix:docker exec wordpress chown www-data:www-data /var/www/html/.htaccess docker exec wordpress chmod 644 /var/www/html/.htaccess - Directory permissions issue:
docker exec wordpress ls -la /var/www/html/ | head -1 # Should show drwxr-xr-xFix:docker exec wordpress chmod 755 /var/www/html - Still using bind mount: This is the most common issue. If you’re still using
./wordpress:/var/www/html, switch to named volumes.
Issue: WordPress Admin Dashboard Loads But Frontend Shows 404
This is a classic mod_rewrite problem:
# Verify mod_rewrite is actually enabled
docker exec wordpress apache2ctl -M | grep rewrite
# If not present, rebuild didn't work
# If present, check that .htaccess exists
docker exec wordpress cat /var/www/html/.htaccess
Possible .htaccess content:
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
If .htaccess is empty or missing, WordPress didn’t install correctly.
Issue: Performance is Still Slow
If you’re using Mutagen:
# Check sync status
mutagen project status
# If stuck, force a resync
mutagen project pause
mutagen project resume
# Monitor detailed activity
mutagen project monitor
If using named volumes: Performance should be good. If slow:
# Check Docker Desktop stats
# In Docker Desktop UI, look at CPU and Memory usage
# Restart Docker
docker compose restart wordpress
Issue: Files Modified in Container Don’t Appear on macOS (With Named Volumes)
This is expected behavior. Use docker cp:
# Copy modified file from container to macOS
docker cp wordpress:/var/www/html/wp-config.php ./wp-config.php
# Edit on macOS, then copy back
nano wp-config.php
docker cp ./wp-config.php wordpress:/var/www/html/wp-config.php
Or use docker exec with inline editing:
docker exec wordpress bash -c 'sed -i "s/old_value/new_value/g" /var/www/html/wp-config.php'
Part 5: Production Considerations
Should You Use These Solutions in Production?
Short answer: Partially.
Production Setup Should:
✅ Use AllowOverride FileInfo (not All) – more secure ✅ Use named volumes ✅ NOT use bind mounts ✅ Enable SSL/HTTPS ✅ Use environment-specific configs ✅ Implement proper backup strategy
Production Should NOT:
❌ Use Mutagen (adds unnecessary overhead) ❌ Have direct file editing capability ❌ Have debug mode enabled ❌ Expose phpmyadmin
Production-Ready Dockerfile
FROM php:8.2-apache
# Install dependencies
RUN apt-get update && apt-get install -y \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
mysql-client \
&& rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install gd mysqli pdo pdo_mysql
# Security: Use FileInfo instead of All
RUN a2enmod rewrite && \
sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride FileInfo/' /etc/apache2/apache2.conf && \
a2enmod ssl
# Copy WordPress (assuming you build context includes WordPress)
COPY wordpress/ /var/www/html/
# Set proper permissions (non-root user)
RUN chown -R www-data:www-data /var/www/html && \
chmod -R 755 /var/www/html
WORKDIR /var/www/html
Production Docker Compose (Simplified)
version: '3.8'
services:
wordpress:
build: .
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- wordpress_files:/var/www/html
- ./ssl:/etc/apache2/ssl:ro
environment:
WORDPRESS_DB_HOST: mysql
WORDPRESS_DB_NAME: wordpress
depends_on:
- mysql
mysql:
image: mysql:8.0
restart: always
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: wordpress
volumes:
wordpress_files:
mysql_data:
Summary: What We Learned
The Three Root Causes:
- Apache Configuration:
AllowOverride Noneprevents Apache from reading.htaccess - Missing Modules:
mod_rewriteisn’t enabled to process rewrite rules - macOS Virtualization: File sharing mechanism causes locking conflicts
The Three Solutions:
- Fix Apache: Enable mod_rewrite and set
AllowOverride Allin Dockerfile - Use Named Volumes: Eliminate the macOS file sharing layer entirely
- Alternative – Mutagen: Sync files intelligently without kernel integration
The Key Takeaway:
The 403 error isn’t actually about file permissions or missing files—it’s about Apache’s security policy preventing it from reading your .htaccess file, combined with macOS’s virtualization layer creating file locking conflicts. Both problems must be addressed for WordPress to work reliably in Docker on macOS.
Next Steps:
- Update your Dockerfile with the Apache configuration fix
- Convert bind mounts to named volumes in your docker-compose.yml
- Rebuild and restart:
docker compose build --no-cache && docker compose up -d - Verify the fix works with the provided curl and docker exec commands
- For active development, consider adding Mutagen