") per entry, in the directory dnsmasq's * --hostsdir watches via inotify (see vpn-dns in docker-compose.yml). No * restart, no reload call: the file landing on disk IS the publish. * * The directory is shared with the app's own containers over a plain Docker * volume, not SSH — unlike SshTraefikWriter, everything that touches it runs * on the same Docker host. Left world-writable/readable (0775/0664) because * whichever container touches it first (queue-provisioning as root in * production, www-data under `app`) must not lock the other out. */ class FileHostDnsDirectory implements HostDnsDirectory { public function write(string $name, string $fqdn, string $ip): void { $this->ensureDir(); $path = $this->path($name); if (@file_put_contents($path, "{$ip} {$fqdn}\n") === false) { throw new RuntimeException("Could not write DNS hosts entry: {$path}"); } @chmod($path, 0664); } public function remove(string $name): void { $path = $this->path($name); // Nothing to do is not a failure — a host whose name was never // written (or already removed) still has to purge cleanly. if (is_file($path) && ! @unlink($path)) { throw new RuntimeException("Could not remove DNS hosts entry: {$path}"); } } private function ensureDir(): void { $dir = $this->dir(); if (is_dir($dir)) { return; } if (! @mkdir($dir, 0775, true) && ! is_dir($dir)) { throw new RuntimeException("Could not create DNS hosts directory: {$dir}"); } @chmod($dir, 0775); } private function path(string $name): string { return rtrim($this->dir(), '/')."/{$name}.hosts"; } private function dir(): string { return (string) config('provisioning.dns.hosts_dir'); } }