Setting up a multi-service Docker Compose stack on Coolify is usually a breeze. But recently, while deploying a Next.js frontend and an internal API, I ran into a brutal gauntlet of 404s, “No available server” errors, and broken Let’s Encrypt SSL certificates.
The traffic was hitting the server, but Traefik — Coolify’s reverse proxy — was completely ignoring the web container and dropping back to a default self-signed certificate.
If you’re pulling your hair out over Coolify routing issues, here’s the exact step-by-step process I used to untangle the Docker networks, fix the proxy labels, and get the Next.js app online.
1. The Coolify UI trap: missing internal ports
When you assign a custom domain in the Coolify UI (e.g. https://example.com), Traefik assumes your container is listening on port 80. If your Next.js app runs on port 3000, Traefik routes the traffic to a dead end, resulting in a 404 Page Not Found.
The fix: explicitly tell Coolify which internal port to route to by appending it to the domain in the UI settings.
- Web domain:
https://example.com:3000 - API domain:
https://api.example.com:8000
Your users won’t see the port in their browser — this just configures the internal Docker network.
2. Docker Compose: ports vs expose
In my docker-compose.yml, I had mapped the web container using ports: - '3000:3000'.
That’s a red flag when you’re running behind Traefik. The ports directive maps the container directly onto the host’s physical network, which bypasses or confuses Traefik’s internal routing table.
The fix: change ports to expose. This makes the port available to Traefik and other containers on the internal Docker network, without exposing it raw to the internet.
services:
web:
expose:
- "3000"
3. Clearing the Traefik SSL cache
While chasing the routing problem, I’d restarted the proxy multiple times, which corrupted Traefik’s Let’s Encrypt cache (acme.json). When this file gets corrupted or loses its strict 600 permissions, Let’s Encrypt silently fails to generate new certificates.
The fix: SSH into the target server and wipe the ACME file to force a fresh start.
docker stop coolify-proxy
sudo rm -f /data/coolify/proxy/acme.json
sudo touch /data/coolify/proxy/acme.json
sudo chmod 600 /data/coolify/proxy/acme.json
docker start coolify-proxy
4. Bypassing the Let’s Encrypt rate limit
Because of the repeated failures and restarts, I eventually hit Let’s Encrypt’s Duplicate Certificate rate limit (error 429) — it only allows five identical certificates per week.
The fix: instead of waiting a week, I changed the API subdomain slightly (api.example.com → api-prd.example.com). Let’s Encrypt treated it as a brand-new request and issued the certificate instantly.
5. The final boss: Next.js standalone host binding
Even after fixing the proxy, the SSL cache, and the compose files, Traefik still refused to route traffic to the Next.js container.
The culprit was the Dockerfile. When you build a Next.js app with output: "standalone", the generated Node server defaults to binding to localhost (127.0.0.1). That means the app only accepts connections from inside its own container. Traefik lives outside the container, so when it tries to reach port 3000, Node just ignores it — and because Traefik can’t connect, it drops the routing rule entirely rather than surfacing a clear error.
The fix: force the Next.js runner to bind to 0.0.0.0 (all network interfaces) by setting HOSTNAME in the final runner stage of the Dockerfile.
# ---- runner: minimal runtime image ----
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production \
PORT=3000 \
HOSTNAME="0.0.0.0"
That one environment variable was the actual root cause the whole time — everything else in this post was necessary, but this was the line that made Traefik able to see the container at all.
The order that would’ve saved me time
If I were doing this again, I’d check these in reverse order of how confident I felt about them: HOSTNAME binding first, expose vs ports second, then the Coolify port suffix, and only reach for the ACME cache wipe once routing is confirmed to actually work — restarting the proxy before routing is fixed just gives you a second problem to debug at the same time as the first.