What happened
The site stopped working because the original /opt/sites/example.org directory was renamed to /opt/sites/example.org-old, and a new /opt/sites/example.org directory was created in its place.
Docker bind mounts attach to a specific inode on the filesystem, not just a path name. When you renamed the original directory and created a new one at the same path, the new directory had a different inode. The running nginx container was still bound to the old inode (now at -old), so from inside the container /opt/sites/example.org appeared completely empty — even though the files were clearly visible on the host.
A simple nginx -s reload cannot fix this because the volume mount is managed by Docker, not nginx. Only a full container restart causes Docker to re-resolve the path and bind to the correct inode.
Symptoms
- Site shows 403 Forbidden or a blank/old page after updating files
ls -la /opt/sites/example.org/on the host shows files correctlydocker exec mailcowdockerized-nginx-mailcow-1 ls -la /opt/sites/example.org/showstotal 0— directory appears empty inside the containerdocker inspect mailcowdockerized-nginx-mailcow-1 | grep -A 10 "Mounts"shows the mount is missing or stale
Diagnosis steps
1. Check files exist on the host:
ls -la /opt/sites/example.org/
2. Find the correct nginx container name:
docker ps | grep nginx
3. Check files are visible inside the container:
docker exec mailcowdockerized-nginx-mailcow-1 ls -la /opt/sites/example.org/
4. Check the mount is present and correct:
docker inspect mailcowdockerized-nginx-mailcow-1 | grep -A 10 "Mounts"
5. Check nginx config is valid:
docker exec mailcowdockerized-nginx-mailcow-1 nginx -t
6. Check nginx logs for errors:
docker logs mailcowdockerized-nginx-mailcow-1 --tail=50
Fix
If the container can’t see the files, a full mailcow restart is required to force Docker to re-read the override file and re-bind the volume mounts:
cd /opt/mailcow-dockerized
docker compose down
docker compose up -d
This causes a brief outage of all mailcow services (email, nginx, SOGo etc) — typically less than two minutes.
After restart, confirm the mount is now correctly bound:
docker inspect mailcowdockerized-nginx-mailcow-1 | grep -A 5 "example.org"
And confirm the files are visible inside the container:
docker exec mailcowdockerized-nginx-mailcow-1 ls -la /opt/sites/example.org/
Then visit https://example.org to confirm the site is back up.
How to avoid this in future
When updating the site, never rename and recreate the directory. Instead, update files in place:
# Good — updates files without touching the directory inode
cp /path/to/new/index.html /opt/sites/example.org/index.html
# Also fine — removes and replaces individual files
rm /opt/sites/example.org/index.html
cp /path/to/new/index.html /opt/sites/example.org/
# Bad — breaks the Docker bind mount
mv /opt/sites/example.org /opt/sites/example.org-old
mkdir /opt/sites/example.org
If you do need to back up the old site first, copy rather than move:
# Safe backup that preserves the original directory and its inode
cp -r /opt/sites/example.org /opt/sites/example.org-old
# then update files in place