Problem: HAProxy Fails to Start in Docker Compose Because the Certificate Volume Is Missing or Mis‑mounted
When launching a local development stack with docker-compose up, the HAProxy service aborts during initialization with errors similar to:
ERROR: cannot open /etc/haproxy/certs/example.com.pem: No such file or directory
HAProxy: SSL: unable to load certificate from /etc/haproxy/certs/example.com.pem (error 2)
docker-compose: service "haproxy" failed to start: mount source path does not exist: ./certs
These messages indicate that the container cannot locate the TLS certificate files that are expected at /etc/haproxy/certs. The root cause is almost always a bind‑mount problem: the host directory either does not exist, is referenced with a relative path that resolves incorrectly, or has permission/SELinux issues that prevent the HAProxy process (running as a non‑root user) from reading the files.
Root Cause Analysis
How HAProxy Loads TLS Certificates
According to the HAProxy Configuration Manual – TLS/SSL section, the crt directive expects a directory containing PEM‑encoded certificate bundles. HAProxy scans the directory at startup and aborts if it cannot open any file that matches the pattern (e.g., /etc/haproxy/certs/*.pem).
Docker Bind‑mount Mechanics
The Docker Engine documentation on bind‑mount permission handling explains that the container sees the host directory with the same UID/GID as the host file system unless :ro or other mount options are used. If the host directory is owned by root and HAProxy runs as UID 1001 (the default for the official HAProxy image), the container will encounter Permission denied (error 13).
Typical Mis‑configurations
- Relative path resolved from the wrong working directory – Docker Compose resolves
./certsrelative to the directory wheredocker-composeis invoked. Running the command from a sibling directory results in an empty mount (see the incident where “a relative path (./ssl) … resolved to an empty directory”). - Case‑sensitivity mismatch – On macOS or Windows, the host file system may be case‑insensitive, but the container’s Linux FS is case‑sensitive. A file named
Example.Com.pemwill not be found asexample.com.peminside the container, leading to “No such file or directory”. - Missing host directory after OS updates – Docker Desktop updates can change the mount root (
//c/…vs/mnt/c/…) and break existing paths, as reported in the Windows‑WSL2 incident. - SELinux or Docker Desktop “cached” mount options – On SELinux‑enforcing hosts, the mount point must be relabeled (
:zor:Z) or HAProxy will receive “Permission denied”. The Reddit post about “:cached” illustrates this.
Investigation and Debugging Steps
1. Verify the Compose Volume Definition
# docker-compose.yml (problematic snippet)
services:
haproxy:
image: haproxy:2.8-alpine
ports:
- "443:443"
volumes:
- ./certs:/etc/haproxy/certs
Check that ./certs exists on the host and contains the expected PEM files.
2. Inspect the Container’s View of the Mount
# Run a temporary shell in the same service definition
docker-compose run --rm haproxy ls -l /etc/haproxy/certs
Typical output when the mount fails:
ls: cannot access '/etc/haproxy/certs': No such file or directory
If the directory exists but is empty, the host path is wrong or the relative path was resolved incorrectly.
3. Check File Ownership and Permissions
# On the host
ls -l ./certs
Expected output:
-rw-r--r-- 1 user user 3245 Mar 12 10:23 example.com.pem
If the files are owned by root:root with 600 permissions, HAProxy (UID 1001) will hit error 13.
4. Capture the HAProxy Startup Log
# docker logs
[ERROR] cannot open /etc/haproxy/certs/example.com.pem: No such file or directory
[ERROR] SSL: unable to load certificate from /etc/haproxy/certs/example.com.pem (error 2)
Match the exact path in the log with the mount point you defined.
5. Verify SELinux Context (Linux hosts only)
# ls -Z ./certs
system_u:object_r:container_file_t:s0 example.com.pem
If the context is not container_file_t, add the :z flag to the volume definition.
Solution: Correct the Volume Mount and Align Permissions
Before – Faulty Compose Definition
services:
haproxy:
image: haproxy:2.8-alpine
ports:
- "443:443"
volumes:
- ./certs:/etc/haproxy/certs
After – Robust Definition
services:
haproxy:
image: haproxy:2.8-alpine
user: "1001" # default non‑root user
ports:
- "443:443"
volumes:
- type: bind
source: ${PWD}/certs # absolute path guarantees correct resolution
target: /etc/haproxy/certs
read_only: true
bind:
propagation: rprivate
# On SELinux‑enforcing hosts add :z to relabel, on macOS/Windows add :cached if needed
environment:
- HAPROXY_TLS_CERT_DIR=/etc/haproxy/certs
Adjust Host Permissions (if needed)
# Ensure files are readable by UID 1001
chmod 644 ./certs/*.pem
# Or change ownership to match container user
chown 1001:1001 ./certs/*.pem
On SELinux hosts:
# Apply shared label
chcon -Rt svirt_sandbox_file_t ./certs
# Or use the :z flag in the compose file (Docker will relabel automatically)
Why the Fix Works
- Using an absolute
sourcepath eliminates ambiguity caused by runningdocker-composefrom different directories. - Marking the mount
read_onlymatches the HAProxy image’s expectation that certificates do not need to be written at runtime. - Correct file permissions (or running the container as
root) satisfy the UID/GID mismatch described in the Docker Engine docs. - The
:z(or:Z) SELinux flag ensures the kernel grants the container the required access, preventing “error 13”.
Verification – Confirm HAProxy Starts Successfully
1. Restart the Stack
docker-compose down -v
docker-compose up -d
2. Check Container Health
# docker ps -f name=haproxy
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a1b2c3d4e5f6 haproxy:2.8-alpine "docker-entrypoint.s…" 10 seconds ago Up 9 seconds 0.0.0.0:443->443/tcp myproject_haproxy_1
3. Verify No TLS Errors in Logs
# docker logs myproject_haproxy_1 2>&1 | grep -i ssl
# (no output – indicates successful certificate load)
4. Perform a TLS Handshake Test
openssl s_client -connect localhost:443 -servername example.com
Expected snippet from the OpenSSL output:
...
Certificate chain
0 s:/CN=example.com
i:/CN=Example CA
...
SSL handshake has read 3245 bytes and written 456 bytes
Verification: OK
Prevention – Guardrails for Future Development
- Use absolute paths or environment variables for bind mounts in
docker-compose.yml. Example:source: ${PWD}/certs. - Version‑control the certificate directory structure (e.g.,
certs/example.com.pem) and enforce naming conventions that match HAProxy’s case‑sensitive expectations. - Integrate a CI check that runs
docker-compose configand validates that the host path exists before pushing changes. - Enable a healthcheck that runs
haproxy -c -f /usr/local/etc/haproxy/haproxy.cfginside the container; Docker will automatically restart the service if the check fails. - Document SELinux requirements for Linux developers and add the
:zflag to the volume definition when the host enforces SELinux. - Pin the HAProxy image version (e.g.,
haproxy:2.8-alpine) to avoid unexpected changes in default user IDs.
FAQ – Common Follow‑up Questions
- Why does HAProxy start fine on Linux but fail on macOS?
macOS uses a case‑insensitive file system and Docker Desktop’s file‑sharing layer can change path prefixes (e.g.,//c/…vs/mnt/c/…). Ensure the host path is absolute and that certificate filenames match the case used in the HAProxy config. - Can I keep HAProxy running as the default non‑root user and still read certificates owned by root?
Yes, by either changing the host file permissions to be world‑readable (chmod 644) or by addinguser: rootto the service definition. The preferred approach is to adjust permissions because it preserves the security model of the official image. - What does error 13 (“Permission denied”) indicate in the HAProxy log?
It means the process could locate the file path but the kernel denied read access, typically due to mismatched UID/GID, SELinux restrictions, or the mount being read‑only without proper permissions. - How can I debug a “No such file or directory” error when the file clearly exists on the host?
Rundocker-compose exec haproxy ls -l /etc/haproxy/certsto view the container’s perspective. Verify the bind‑mount source path, confirm that the compose command is executed from the correct working directory, and check for case‑sensitivity mismatches. - Is it safe to mount the certificate directory as read‑only?
Yes. HAProxy only reads certificates at startup; a read‑only bind‑mount prevents accidental modification from within the container while satisfying the image’s security expectations.
Related Topic Hub: Distributed Systems Troubleshooting Hub