Skip to main content

Docker Troubleshooting Case Study: Deploying Zusam on YunoHost

The goal was to deploy Zusam (a private social network) using Docker Compose on a VPS running YunoHost. The setup involved a Docker container exposing a specific port (18940) and a YunoHost "Custom Webapp" acting as a reverse proxy to make the service accessible via a public domain.

2. Issue #1: The Reboot Loop (Permission Denied)

Symptoms

Upon starting the container, the logs showed a successful database initialisation, but the container immediately entered a restart loop. The logs ended with chown and crontab commands, followed by a crash.

zusam-1  | + chown -R 1000:1000 /zusam /etc/s6.d ...
zusam-1  | + su-exec 1000:1000 /bin/s6-svscan /etc/s6.d
# Container restarts

Root Cause Analysis

This is a classic UID/GID Mismatch.

  • The Host: The directory ~/docker/zusam/data was owned by the host user (kalvin0x58c, UID: 23691).
  • The Container: The Zusam Docker image is hardcoded to switch to internal user 1000 (abc or www-data) to run the application.
  • The Conflict: When the container (UID 1000) tried to write to the mapped volume (owned by UID 23691), the Linux kernel denied permission.

Failed Attempt: Force User in YAML

We attempted to force the container to run as the host user by adding the user: directive to docker-compose.yml:

# ❌ INCORRECT APPROACH FOR THIS IMAGE
user: "23691:23691"

Result: The container failed even earlier with crontab: must be suid to work properly. Reason: The Zusam container acts as an init script. It must start as root to perform setup tasks (like fixing permissions and setting up cron) before it voluntarily drops privileges to user 1000. Forcing it to start as a non-root user broke this initialization chain.

✅ The Solution: Correct Host Permissions

Instead of changing the container's behavior, we aligned the host storage with the container's expectation.

  1. Remove user: directive from docker-compose.yml to let the container start as root.
  2. Change ownership of the host data directory to match the container's internal UID (1000).
# Run on host machine
sudo chown -R 1000:1000 ~/docker/zusam/data

  1. Rebuild and restart:
docker compose up -d --build --force-recreate --renew-anon-volumes


3. Issue #2: Reverse Proxy "Welcome to Nginx" Error

Symptoms

The container was running successfully (verified by docker ps and curl localhost:18940), but accessing the domain (zusam.nulu.my) displayed the default "Welcome to nginx!" page instead of the application.

Root Cause Analysis

The YunoHost Reverse Proxy (configured via the "Custom Webapp" tool) was not correctly forwarding traffic to the Docker container.

Potential misconfiguration candidates:

  1. Public IP Loopback: Pointing the proxy to the VPS public IP (46.x.x.x:18940) often fails due to Hairpin NAT or firewall blocking.
  2. Missing proxy_pass: If the configuration is incomplete, Nginx defaults to serving its static welcome page.

✅ The Solution: Localhost Proxying

When the Docker container and the Nginx proxy reside on the same physical server, the proxy destination must be the localhost loopback interface. This bypasses the firewall and ensures internal routing.

Configuration for YunoHost Custom Webapp:

  • Domain: zusam.nulu.my
  • Path: /
  • Port / URL: http://127.0.0.1:18940 (Do not use the public IP)

Verification

If the "Welcome to Nginx" page persists, the configuration file for the specific domain must be checked manually for the proxy_pass directive:

# /etc/nginx/conf.d/zusam.nulu.my.conf
location / {
    proxy_pass http://127.0.0.1:18940;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    # ...
}

4. Summary of Lessons

  1. Respect Container Init Scripts: Some containers require root at startup. Do not use the user: directive in Docker Compose unless the documentation explicitly supports it.
  2. IDs are Numbers: Linux file permissions care about UIDs, not usernames. chown 1000:1000 is often necessary for persistent volumes, even if that ID doesn't exist on the host system.
  3. Proxy Locally: Always use 127.0.0.1 or localhost when reverse proxying to a Docker container on the same server to avoid firewall and NAT issues.
  4. Rebuild Cleanly: When in doubt, use docker compose up -d --build --force-recreate to ensure no stale configuration or cached layers remain.