The problem
Dokku is a small, self-hosted PaaS that builds and runs apps in Docker containers behind nginx. On a host whose kernel has no IPv6 support, two symptoms tend to appear after a fresh install:
- nginx refuses to start or reload, and the package or the first
dokku apps:createreports a failed nginx configuration check. - An app appears to deploy, but requests return 502 or never reach the container, because the proxy configuration for the app was never applied.
The nginx error that points at the real cause normally reads socket() [::]:80 failed (97: Address family not supported by protocol), followed by a line saying the configuration test failed. The configuration syntax is reported as fine. Only the socket creation fails, so the message is easy to mistake for an application problem.
A second, independent gap often appears on the same kind of minimal host: the machine is not running systemd as PID 1 (a container, a stripped-down VM, an OpenVZ-style guest). Dokku's package scripts and deploy hooks assume a service manager is available to reload nginx.
Why it happens
Two facts combine.
- The generated and shipped nginx configuration contains
listendirectives for IPv6, and the default address for them is[::]. - nginx treats a
listen [::]:PORTdirective as mandatory. If the kernel cannot create an AF_INET6 socket, the configuration test fails and nginx does not skip the line.
The kernel side needs precision, because it is a common source of wrong advice. Setting the sysctl net.ipv6.conf.all.disable_ipv6=1 does not cause this failure: IPv6 sockets can still be created, they just have no addresses. The failure appears when IPv6 is absent at module level, for example with ipv6.disable=1 on the kernel command line, a kernel built without IPv6, or a container or hypervisor guest that does not expose it.
The init-system gap is separate. Dokku chooses how it reloads services through the DOKKU_INIT_SYSTEM variable. When it is unset on a host without systemd, the package post-install step and the reload that follows a deploy fail.
Source of truth
The fix relies on the Dokku nginx proxy documentation: https://dokku.com/docs/networking/proxies/nginx/. It confirms the following points:
- On fresh apt installs Dokku ships a catch-all site at
/etc/nginx/conf.d/00-default-vhost.conf, and the page says to edit that file and reload nginx to change its behavior. The default copy is kept under/var/lib/dokku/core-plugins/available/nginx-vhosts/templates/asdefault-site.conf. - The
bind-address-ipv6property defaults to[::]and thebind-address-ipv4property defaults to0.0.0.0. These properties only choose which address is bound. The page lists no property that removes the IPv6 listener. - The
nginx-conf-sigil-pathproperty names the file in the app repository (defaultnginx.conf.sigil) that replaces the built-in per-app template.
The page does not discuss IPv6-less hosts or non-systemd init systems. Those parts of this article are general nginx and Linux behavior plus Dokku's DOKKU_INIT_SYSTEM convention, and should be checked against the scripts of the installed Dokku version.
Fix options
| Method | What it changes | Catch |
|---|---|---|
| Re-enable IPv6 on the host | Remove ipv6.disable=1 from the kernel command line and reboot | Needs control of the kernel or hypervisor settings. Impossible in many containers. |
Per-app nginx.conf.sigil | A copy of the built-in template without IPv6 listeners is committed in each app repository | Each repository carries a copy that can drift from newer Dokku releases. |
| Host-wide template override | A small plugin that implements the nginx-app-template-source trigger and returns a patched template path | The patched template is pinned to one Dokku version. Check the trigger's exact arguments in the plugin triggers reference of the Dokku docs before relying on it. |
| Disable the proxy for an app | dokku proxy:disable APP stops Dokku managing nginx for that app | No vhost routing. Not a fix for the catch-all file. |
Whichever app-level method is used, the catch-all file needs its own edit on a host without IPv6. It is a static file written by the package, not rendered from the per-app template.
Changing bind-address-ipv6 is not a solution: it changes the address inside the listen directive, but the directive and the IPv6 socket requirement remain.
Why this fix
The chosen fix is the edited catch-all file plus a per-app nginx.conf.sigil without IPv6 listeners. Reasons:
- Both mechanisms are described on the documentation page above, so no undocumented behavior is involved.
- Nothing in the Dokku package is modified. Removing the override later restores stock behavior.
- The template is copied from the installed Dokku, so it matches that version's syntax.
Re-enabling IPv6 is simpler and should be preferred whenever the kernel command line is under the operator's control. The host-wide plugin is better for many apps, but depends on a trigger interface that is outside the verified source.
Step by step
The commands below are instructions to apply on the affected host. Nothing here was executed while writing this article, so no output is shown. Expected results are described in prose.
1. Confirm the host really lacks IPv6 sockets
Check the kernel command line and the IPv6 proc entries:
grep -o 'ipv6.disable=1' /proc/cmdline
ls /proc/net/if_inet6
A match on the first command, or a missing file on the second, indicates IPv6 is unavailable at module level. To reproduce the failure without Dokku, save this minimal configuration as /tmp/v6test.conf:
events {}
http {
server {
listen 8080;
listen [::]:8080;
return 200 ok;
}
}
nginx -t -c /tmp/v6test.conf
On an IPv6-less kernel, nginx should report the socket() ... failed (97 ...) error even though the syntax is valid.
2. Remove the IPv6 listeners from the catch-all file
Delete the IPv6 listen lines and test the result:
sed -i '/listen[[:space:]]*\[::\]/d' /etc/nginx/conf.d/00-default-vhost.conf
cat /etc/nginx/conf.d/00-default-vhost.conf
nginx -t
Only IPv4 listen lines (for example for ports 80 and 443 with default_server) should remain. Keep a copy of the original if a later package upgrade needs comparing.
3. Install a per-app template without IPv6 lines
Copy the built-in template from the Dokku host into the app repository, and inspect where the IPv6 variable is used before deleting anything:
scp root@DOKKU_HOST:/var/lib/dokku/core-plugins/available/nginx-vhosts/templates/nginx.conf.sigil nginx.conf.sigil
grep -n 'IP6' nginx.conf.sigil
If each match is a complete, standalone listen line, delete those lines. If a match sits inside a template conditional, remove the whole statement by hand so the template stays balanced, because a stray end marker makes the template fail to render.
sed -i '/NGINX_BIND_ADDRESS_IP6/d' nginx.conf.sigil
git add nginx.conf.sigil
git commit -m 'Drop IPv6 listeners from nginx template'
git push dokku main
If the template lives under another path, point Dokku at it with the property named in the docs:
dokku nginx:set APP conf-sigil-path path/to/nginx.conf.sigil
4. Rebuild and verify the proxy configuration
dokku proxy:build-config APP
grep -n listen /home/dokku/APP/nginx.conf
Only IPv4 listen directives should appear, and nginx should reload without the socket error. If an app was created while validation was failing, also check its process formation, since an empty one yields zero containers and a 502:
dokku ps:scale APP
If the web count is missing, scale it explicitly with dokku ps:scale APP web=1, or destroy and recreate the app once nginx is healthy.
5. Hosts without systemd
On a host where PID 1 is not systemd, one common arrangement is to run nginx under runit and tell Dokku to use it. Set the variable before the package is configured, and persist it for later sessions, including the SSH session of the dokku user:
apt-get install runit
export DOKKU_INIT_SYSTEM=sv
echo 'export DOKKU_INIT_SYSTEM=sv' > /etc/default/dokku
Define an nginx service for runit as /etc/service/nginx/run (executable), and make sure runsvdir is running:
#!/bin/sh
exec nginx -g 'daemon off;'
chmod +x /etc/service/nginx/run
sv restart nginx
The deploy path reloads nginx through a privileged call, so verify that the dokku user is allowed to run the service-control command that matches the chosen init system. This depends on the installed Dokku version and is not covered by the linked page.
6. Test with real requests
If DNS does not resolve yet, send the Host header by hand:
curl -sS -i -H 'Host: APP.example.com' http://127.0.0.1/
curl -sS -i -H 'Host: unknown.example' http://127.0.0.1/
The first request should return the app's response. The second should be dropped without a body, because the catch-all file closes connections for unknown hosts.
Short review
What this approach achieves: nginx loads and reloads cleanly on a host without IPv6 sockets, apps deploy through the normal git push flow, and unknown hosts are still rejected by the catch-all.
Limits:
- The copied template is a snapshot of one Dokku version. After each Dokku upgrade, compare it with the shipped template and refresh the copy, otherwise new features and fixes in the stock template are silently missed.
- Every app needs its own copy, and one app without it can make a later nginx reload fail for all apps, since nginx loads all generated files together.
- Plain HTTP routing is the scope here. TLS certificates and multiple process types follow the same pattern but need their own checks, particularly if the template has separate IPv6 lines for port 443.
- The runit steps are a workaround for hosts without systemd, not part of the IPv6 fix.
When to choose another method: if the kernel command line or hypervisor settings can be changed, re-enable IPv6 and keep the stock Dokku templates, which removes all maintenance. With many apps and a stable Dokku version, a host-wide template override via a plugin avoids repeating the file in every repository, once the trigger interface has been confirmed in the Dokku documentation.