Compare commits

..
Author SHA1 Message Date
atlas
b12be26f20 docs(3191): drop the migration history from the gateway comments
Per mara on the PR: how a thing used to be deployed stops being
relevant the moment this is merged and the hives are rebuilt, so the
comments state the current constraint and nothing about the container
that used to hold it.

Removes the two ⚠️ blocks this PR added (hive-tls-ca's ordering, the
resign propagation), the matrix ordering's, dnsmasq's resolv.conf
paragraph, and the pre-existing 'REMOVED WITH THE CONTAINER' inventory
in the gateway module. The 🚨 do-not-simplify warning on the cert-copy
unit keeps both its reasons and loses the container framing.

Kept deliberately: the two operator-facing 'this used to default to
true' option notes (migration information for someone upgrading), and
the SupplementaryGroups block, which documents why an absence is
load-bearing rather than how a deployment used to look.
2026-08-12 13:26:58 +02:00
atlas
f85724a638 docs(3191): the gateway's comments describe a host service, not a container
Prose-only sweep of every remaining claim that nginx or dnsmasq lives
in a container: the port comment (root in a container -> root on the
host), upstreamHost's netns rationale, the ACME state dir, the store
path reachability note, the vhost tree header, dnsmasq's resolv.conf
paragraph (there is no copy and no path unit watching it any more),
the two hive-network bridge comments, and swarm-controller's socket
access-control note, which described a bind-mount that no longer
exists.

No behaviour change; all of it was describing a mechanism that was
deleted.
2026-08-12 12:20:28 +02:00
atlas
d70b1e0a97 fix(3191): order gateway TLS against the units that read it, not a dead container
Removing the gateway container retired two things that were still
depended on: an ordering edge and a delivery path.

- hive-tls-ca ordered itself before/requiredBy
  container@hive-gateway.service. That unit no longer exists, so the CA
  was sequenced against nothing while its real consumer,
  hive-gateway-self-signed-cert (which nginx Requires=), could win the
  race and fail its copy under set -eu, blocking nginx.

- hive-tls-resign propagated a rotated leaf with
  `systemctl -M hive-gateway … || true`. The machine is gone, so both
  calls failed and both failures were swallowed: the unit logged
  "propagating" and exited 0 while nginx served the stale copy until it
  expired. Host units now, so no -M and no || true — a failed
  propagation fails the timer.

- container@hive-matrix ordered after the gateway container to get the
  resolver up first. dnsmasq is a host service now, so it orders after
  dnsmasq.service, which is what the edge always meant.
2026-08-12 12:18:13 +02:00
9 changed files with 88 additions and 126 deletions

View file

@ -33,9 +33,8 @@ in
# gateway_nginx.rs uses this to generate split location # gateway_nginx.rs uses this to generate split location
# blocks in agents.conf — static HTML/CSS/JS served from the # blocks in agents.conf — static HTML/CSS/JS served from the
# nix store directly; dynamic API paths still proxied to the # nix store directly; dynamic API paths still proxied to the
# agent daemon. The nix store is shared across nspawn # agent daemon. nginx runs on the host, which is where this
# containers, so this path is reachable from inside the # store path already is, so it is reachable as written.
# gateway container's nginx.
HIVE_AGENT_FRONTEND_DIR = "${cfg.servedFrontend}/agent"; HIVE_AGENT_FRONTEND_DIR = "${cfg.servedFrontend}/agent";
# Path to the static runtime asset tree (branding + claude # Path to the static runtime asset tree (branding + claude
# prompts). `hive_sh4re::assets::*` reads paths underneath. # prompts). `hive_sh4re::assets::*` reads paths underneath.

View file

@ -26,9 +26,7 @@ let
forgeCfg = config.services.hyperhive.swarm.forge; forgeCfg = config.services.hyperhive.swarm.forge;
networkCfg = config.services.hyperhive.network; networkCfg = config.services.hyperhive.network;
# Dashboard SPA dist, static-served by nginx. Read in OUTER scope so # Dashboard SPA dist, static-served by nginx.
# `config` is the host's (inside the container block it'd be the
# container's).
dashboardDist = "${config.services.hyperhive.c0re.servedFrontend}/dashboard"; dashboardDist = "${config.services.hyperhive.c0re.servedFrontend}/dashboard";
# Full hyperhive-themed Swagger UI dist — nginx serves this whole # Full hyperhive-themed Swagger UI dist — nginx serves this whole
@ -157,32 +155,6 @@ in
"f /var/lib/hive-gateway/conf/gateway.htpasswd 0644 hive-core hive-core - -" "f /var/lib/hive-gateway/conf/gateway.htpasswd 0644 hive-core hive-core - -"
]; ];
# ⚠️ REMOVED WITH THE CONTAINER, and each one was a workaround for the
# boundary rather than a thing nginx or dnsmasq needed:
#
# - `privateNetwork = false` — the container already shared the host
# netns, which is why nginx bound host ports and `localhost`
# upstreams reached hive-c0re. On the host that is simply true.
# - `additionalCapabilities = [ "CAP_NET_ADMIN" ]` — dnsmasq refuses
# to start with a `dhcp-range` unless it holds NET_ADMIN, and
# nspawn's bounding set dropped it for a host-netns container.
# Host root has it.
# - `networking.firewall.enable = false` — a container sharing the
# host netns would run *its* firewall.service against the HOST
# ruleset, flushing nixos-fw and deleting the nixos-nat-* chains on
# every boot. With one machine there is one firewall (below).
# - `networking.resolvconf.enable = false` + the `hive-gateway-resolv`
# path/service pair — the container's /etc/resolv.conf was a
# one-shot copy frozen at start, so a host network change left
# dnsmasq forwarding to a resolver that was gone. The whole
# watch-copy-reload machine existed to bridge two files. There is
# now one.
# - three bind mounts — /run/hive-agent, /run/hive-state, and either
# /run/hive-tls (operator cert) or /run/hive-ca (self-signed);
# those last two are mutually exclusive mkIfs, so it was never
# four. All plain host paths now.
#
# See `docs/network.md::Resolver behaviour` for the resolver history.
# ACME (Let's Encrypt) integration. nginx vhosts set # ACME (Let's Encrypt) integration. nginx vhosts set
# `enableACME = true` via the vhost builder; this provides the # `enableACME = true` via the vhost builder; this provides the
# shared ACME config (acceptTerms + email). # shared ACME config (acceptTerms + email).
@ -193,15 +165,14 @@ in
# Import the hive-CA leaf into nginx's state dir before nginx starts. # Import the hive-CA leaf into nginx's state dir before nginx starts.
# #
# 🚨 THIS LOOKS LIKE A LEFTOVER OF THE CONTAINER AND IS NOT — do not # 🚨 DO NOT "simplify" this into pointing nginx at the CA dir. It
# "simplify" it into pointing nginx at the CA dir. It does TWO jobs: # does TWO jobs:
# #
# (1) It re-modes the leaf. `hive-tls-ca` writes the key 0600 # (1) It re-modes the leaf. `hive-tls-ca` writes the key 0600
# root:root; nginx's pre-start `nginx -t` runs as the nginx # root:root; nginx's pre-start `nginx -t` runs as the nginx
# *user*, so a 0600 key fails the config test with # *user*, so a 0600 key fails the config test with
# `BIO_new_file() … Permission denied` and blocks the unit — # `BIO_new_file() … Permission denied` and blocks the unit —
# hence the 0640 root:nginx copy below. That is a file-mode fact, # hence the 0640 root:nginx copy below.
# not a namespace one, and it did not go away with the container.
# (2) It guarantees that **every cert path the nginx config names # (2) It guarantees that **every cert path the nginx config names
# exists** — which is what the swarm-services fallback at the # exists** — which is what the swarm-services fallback at the
# bottom of the script is for. nginx refuses to load a config # bottom of the script is for. nginx refuses to load a config
@ -243,10 +214,10 @@ in
# mode behind. # mode behind.
chmod 0755 ${builtins.dirOf tlsDir} chmod 0755 ${builtins.dirOf tlsDir}
chmod 0755 ${tlsDir} chmod 0755 ${tlsDir}
# Copy the host leaf in. `install` writes atomically with the # Copy the leaf in. `install` writes atomically with the
# target mode; run as root (container root == host root, # target mode; runs as root so the 0600 root:root key written
# privateUsers=false) so the 0600 root:root host key is # by hive-tls-ca is readable. Key ends up root:nginx 0640 so
# readable. Key ends up root:nginx 0640 so nginx-pre-start # nginx-pre-start
# (which runs `nginx -t` as the nginx user, not root) can # (which runs `nginx -t` as the nginx user, not root) can
# read it — a 0600 root:root key passes the master load but # read it — a 0600 root:root key passes the master load but
# fails the pre-start config test with `BIO_new_file() … # fails the pre-start config test with `BIO_new_file() …

View file

@ -1,10 +1,10 @@
# Hive-internal DNS resolver + DHCP, co-located in the gateway # Hive-internal DNS resolver + DHCP, running on the host alongside the
# container — single front-door for both DNS and HTTP, saves a # gateway's nginx — single front-door for both DNS and HTTP, and no
# sibling container. Listens on the bridge interface from # container of its own. Listens on the bridge interface from
# `services.hyperhive.network`; authoritative for the hive domain + # `services.hyperhive.network`; authoritative for the hive domain +
# sub-domains, forwards everything else upstream. Returns the # sub-domains, forwards everything else upstream. Returns the
# `services.dnsmasq` value for the container config (see # `services.dnsmasq` value (see ./default.nix); the DHCP pool bounds
# ./default.nix); the DHCP pool bounds are computed by hive-network. # are computed by hive-network.
{ {
lib, lib,
networkCfg, networkCfg,
@ -68,14 +68,10 @@
dhcp-range = "${networkCfg.dhcpRangeStart},${networkCfg.dhcpRangeEnd},1h"; dhcp-range = "${networkCfg.dhcpRangeStart},${networkCfg.dhcpRangeEnd},1h";
dhcp-leasefile = "/var/lib/dnsmasq/dnsmasq.leases"; dhcp-leasefile = "/var/lib/dnsmasq/dnsmasq.leases";
# No explicit upstream: non-hive queries follow dnsmasq's # No explicit upstream: non-hive queries follow dnsmasq's
# resolv.conf default — the gateway container's `/etc/resolv.conf`, # resolv.conf default — the host's own `/etc/resolv.conf`, so the
# which nixos-container copies from the host at every start, so the # hive always uses the host's resolvers and follows them live with
# hive always uses the host's resolvers. resolvconf is disabled in # no copy to go stale. Deliberately no fallback
# the container (see ./default.nix) so nothing regenerates that # `server=`: dnsmasq queries
# copy; the host-side `hive-gateway-resolv` path unit (also in
# ./default.nix) pushes in a fresh copy and reloads dnsmasq whenever
# the host's resolvers change, so the copy can't go stale under a
# network switch. Deliberately no fallback `server=`: dnsmasq queries
# all known upstreams in parallel, so a hardcoded public resolver # all known upstreams in parallel, so a hardcoded public resolver
# would take a share of *normal* traffic, not just fill in when the # would take a share of *normal* traffic, not just fill in when the
# host file is empty. # host file is empty.

View file

@ -28,10 +28,10 @@ in
example = 8080; example = 8080;
description = '' description = ''
TCP port the gateway listens on. Default 80 (canonical web TCP port the gateway listens on. Default 80 (canonical web
port). nginx inside the container binds <1024 because the port). nginx runs on the host as root, so it can bind <1024;
container's init runs as root; if 80 is already taken on the if 80 is already taken on the host (existing nginx, traefik,
host (existing nginx, traefik, etc.) override to an unused etc.) override to an unused port like 8080 or move the
port like 8080 or move the conflicting service. conflicting service.
''; '';
}; };
@ -40,8 +40,8 @@ in
default = "127.0.0.1"; default = "127.0.0.1";
description = '' description = ''
Host the gateway proxies non-static requests to. Defaults to Host the gateway proxies non-static requests to. Defaults to
`127.0.0.1` because the gateway container shares the host `127.0.0.1` because nginx runs on the host itself, so loopback
netns, so loopback resolves directly to hive-c0re. resolves directly to hive-c0re.
''; '';
}; };
@ -180,11 +180,11 @@ in
default = false; default = false;
example = true; example = true;
description = '' description = ''
Let nginx inside the gateway container obtain and renew TLS Let the gateway's nginx obtain and renew TLS certificates
certificates automatically via ACME (Let's Encrypt). When automatically via ACME (Let's Encrypt). When enabled, each
enabled, each vhost calls out to Let's Encrypt using the vhost calls out to Let's Encrypt using the HTTP-01
HTTP-01 challenge on `port` (default 80) and stores certs challenge on `port` (default 80) and stores certs in the
inside the gateway container's persistent state dir. gateway's persistent state dir on the host.
Requirements: Requirements:
- `services.hyperhive.domain` must be set and publicly - `services.hyperhive.domain` must be set and publicly

View file

@ -1,9 +1,9 @@
# nginx virtual-host tree for the gateway container: the `_` default # nginx virtual-host tree for the gateway: the `_` default server
# server (dashboard, per-agent routing, matrix discovery), the forge # (dashboard, per-agent routing, matrix discovery), the forge, matrix
# and matrix sub-domain vhosts, and the Accept-header SPA map for the # and authelia sub-domain vhosts, and the Accept-header SPA map for the
# matrix GUI. Pure function — called from ./default.nix inside the # matrix GUI. Pure function — called from ./default.nix with the
# container config with the outer-scope config values as arguments; # outer-scope config values as arguments; returns
# returns `{ virtualHosts, appendHttpConfig }`. # `{ virtualHosts, appendHttpConfig }`.
{ {
lib, lib,
cfg, # services.hyperhive.gateway cfg, # services.hyperhive.gateway

View file

@ -581,25 +581,22 @@ in
]; ];
}; };
# The matrix container's resolver is the dnsmasq that runs in the # The matrix container's resolver is the hive's dnsmasq (bound at
# gateway container (bound at `bridgeIp`). Order the matrix # `bridgeIp`). Order the matrix container start after it so the
# container start after the gateway container so the resolver is up # resolver is up before tuwunel's first federation lookups. tuwunel
# before tuwunel's first federation lookups. tuwunel boots fine # boots fine without this — it configures the resolver from
# without this — it configures the resolver from `/etc/resolv.conf` # `/etc/resolv.conf` at startup and only queries on-demand (the boot
# at startup and only queries on-demand (the boot failure this # failure this module guards against is an *empty* resolv.conf, a
# module guards against is an *empty* resolv.conf, a parse error, # parse error, not a connectivity one) — so this is robustness, not a
# not a connectivity one) — so this is robustness, not a boot # boot requirement. Soft `after` ordering (not `requires`) keeps the
# requirement. Soft `after` ordering (not `requires`) keeps the # matrix container's lifecycle decoupled from the resolver's.
# matrix container's lifecycle decoupled from the gateway's. The #
# gateway always runs alongside hyperhive, so the gateway container
# unit always exists here. (Declarative `containers.<n>` →
# `container@<n>.service` — the nspawn template NixOS generates.)
# `mkMerge`, not a bare assignment: `caTrust.containerOrdering` also # `mkMerge`, not a bare assignment: `caTrust.containerOrdering` also
# sets `after`/`requires` (so the bound trust bundle exists before # sets `after`/`requires` (so the bound trust bundle exists before
# nspawn wires the mount up), and two plain assignments to the same # nspawn wires the mount up), and two plain assignments to the same
# unit would conflict rather than combine. # unit would conflict rather than combine.
systemd.services."container@hive-matrix" = lib.mkMerge [ systemd.services."container@hive-matrix" = lib.mkMerge [
{ after = [ "container@hive-gateway.service" ]; } { after = [ "dnsmasq.service" ]; }
caTrust.containerOrdering caTrust.containerOrdering
]; ];
}; };

View file

@ -46,9 +46,8 @@ in
'') '')
(lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "upstreamDns" ] '' (lib.mkRemovedOptionModule [ "services" "hyperhive" "network" "upstreamDns" ] ''
The hive resolver always follows the host's resolvers now The hive resolver always follows the host's resolvers now
(dnsmasq reads the gateway container's /etc/resolv.conf, the (dnsmasq runs on the host and reads its /etc/resolv.conf
host copy made at container start). Configure upstream DNS on directly). Configure upstream DNS on the host itself instead.
the host itself instead.
'') '')
]; ];
@ -71,8 +70,8 @@ in
example = "172.30.0.1"; example = "172.30.0.1";
description = '' description = ''
IPv4 address assigned to the bridge interface on the host IPv4 address assigned to the bridge interface on the host
side. Agents use this address as their DNS server (dnsmasq side. Agents use this address as their DNS server (the hive's
in the gateway container binds here). Default `10.42.0.1` dnsmasq binds here). Default `10.42.0.1`
is in RFC 1918 space and unlikely to clash with operator's is in RFC 1918 space and unlikely to clash with operator's
existing setup; override if a different range is already in existing setup; override if a different range is already in
use. use.
@ -123,7 +122,7 @@ in
# before broadcast). All containers — agents and service # before broadcast). All containers — agents and service
# containers alike — receive their IPs dynamically from this pool; # containers alike — receive their IPs dynamically from this pool;
# there are no hash-derived static assignments. Consumed by the # there are no hash-derived static assignments. Consumed by the
# dnsmasq that runs in the gateway container (hive-gateway module). # hive's dnsmasq (hive-gateway module, host-side).
dhcpRangeStart = lib.mkOption { dhcpRangeStart = lib.mkOption {
type = lib.types.str; type = lib.types.str;
internal = true; internal = true;
@ -212,7 +211,7 @@ in
# is unconditional now). # is unconditional now).
networking.bridges.${cfg.bridgeName}.interfaces = [ ]; networking.bridges.${cfg.bridgeName}.interfaces = [ ];
# Bridge IP — dnsmasq (in the gateway container) binds here. # Bridge IP — the hive's dnsmasq binds here.
networking.interfaces.${cfg.bridgeName}.ipv4.addresses = [ networking.interfaces.${cfg.bridgeName}.ipv4.addresses = [
{ {
address = cfg.bridgeIp; address = cfg.bridgeIp;

View file

@ -307,17 +307,19 @@ in
matrix.gatewayHost, authelia.domain}) or install the sub-CA. matrix.gatewayHost, authelia.domain}) or install the sub-CA.
''; '';
# Generate (and rotate) the hive CA + gateway leaf before the gateway # Generate (and rotate) the hive CA + gateway leaf before anything
# container starts. Idempotent: the CA is created once and reused; the # serves it. Idempotent: the CA is created once and reused; the leaf
# leaf is re-signed on expiry under the same CA so the anchor is stable. # is re-signed on expiry under the same CA so the anchor is stable.
systemd.services.hive-tls-ca = { systemd.services.hive-tls-ca = {
description = "Generate hive CA + gateway leaf TLS cert (self-signed mode)"; description = "Generate hive CA + gateway leaf TLS cert (self-signed mode)";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
# Gateway nginx reads the leaf from the bind-mount, so the cert must # The consumer is `hive-gateway-self-signed-cert`, which copies the
# exist before the container starts. Declarative nixos-containers are # leaf into the gateway's state dir at the mode nginx can read, and
# instances of the `container@.service` template. # which nginx in turn `Requires=`. So this must run first or that
before = [ "container@hive-gateway.service" ]; # copy fails under `set -eu` and takes nginx down with it — order
requiredBy = [ "container@hive-gateway.service" ]; # against the unit that reads the file.
before = [ "hive-gateway-self-signed-cert.service" ];
requiredBy = [ "hive-gateway-self-signed-cert.service" ];
# The issuance below needs the swarm root key on disk, and (for the # The issuance below needs the swarm root key on disk, and (for the
# services leaf) the services sub-CA it signs under. When this host # services leaf) the services sub-CA it signs under. When this host
# generates them (single-host swarm) both units must have run first; # generates them (single-host swarm) both units must have run first;
@ -466,21 +468,19 @@ in
# `hive-tls-ca` only re-signs at service activation (boot/rebuild); a # `hive-tls-ca` only re-signs at service activation (boot/rebuild); a
# long-uptime host would otherwise let a 30-day leaf lapse silently. # long-uptime host would otherwise let a 30-day leaf lapse silently.
# This service re-signs the leaf directly (not by bouncing hive-tls-ca) # This service re-signs the leaf directly (not by bouncing hive-tls-ca)
# and propagates the new leaf into the running gateway container when # and propagates the new leaf to nginx when the file actually changed.
# the file actually changed.
# #
# Propagation mechanism: nginx in the gateway container serves a *copy* # Propagation mechanism: nginx serves a *copy* of the leaf, written by
# of the leaf written by `hive-gateway-self-signed-cert` (which runs at # `hive-gateway-self-signed-cert` at the mode nginx can read. Re-signing
# container start). A host-side `systemctl -M hive-gateway` call # the source therefore changes nothing on its own — the copy has to be
# triggers the re-import + reload, mirroring how hive-c0re reloads the # remade and nginx reloaded, which is what the two calls below do.
# gateway after each agents.conf write. A path unit *inside* the
# container cannot do this: IN_MOVED_TO from an atomic rename on the
# host does not propagate across the nspawn mount-namespace boundary.
# #
# `|| true` on propagation so a stopped gateway never fails the unit — # ⚠️ Neither call is `|| true`: a swallowed propagation failure means
# its next boot will import the already-rotated leaf anyway. # the leaf rotates on disk while nginx keeps serving the old copy
# until it expires, with this unit reporting success the whole time.
# A failure here must fail the timer.
systemd.services.hive-tls-resign = { systemd.services.hive-tls-resign = {
description = "Re-sign the gateway TLS leaf and propagate it into the gateway container"; description = "Re-sign the gateway TLS leaf and reload nginx";
# hive-tls-ca must have run first so the CA key exists before we try # hive-tls-ca must have run first so the CA key exists before we try
# to re-sign under it. On first boot `Persistent=true` on the weekly # to re-sign under it. On first boot `Persistent=true` on the weekly
# timer fires immediately; without this ordering the resign could race # timer fires immediately; without this ordering the resign could race
@ -534,9 +534,9 @@ in
after="$(sha256sum "$leaf" "$svcleaf" 2>/dev/null || true)" after="$(sha256sum "$leaf" "$svcleaf" 2>/dev/null || true)"
if [ "$before" != "$after" ]; then if [ "$before" != "$after" ]; then
echo "gateway leaf rotated propagating into hive-gateway" echo "gateway leaf rotated re-importing and reloading nginx"
systemctl -M hive-gateway restart hive-gateway-self-signed-cert.service || true systemctl restart hive-gateway-self-signed-cert.service
systemctl -M hive-gateway reload nginx.service || true systemctl reload nginx.service
else else
echo "gateway leaf unchanged (already up to date)" echo "gateway leaf unchanged (already up to date)"
fi fi

View file

@ -4,9 +4,9 @@
# the agents on one host, this owns what is true across hives. # the agents on one host, this owns what is true across hives.
# #
# Serves HTTP over a unix socket rather than a TCP port: the gateway's # Serves HTTP over a unix socket rather than a TCP port: the gateway's
# nginx is the only intended client, it reaches the socket through a # nginx is the only intended client, it runs on this same host and so
# bind-mount, and a socket that is never bound to an address cannot be # reaches the socket by path, and a socket that is never bound to an
# reached from off-host by mistake. # address cannot be reached from off-host by mistake.
{ {
pkgs, pkgs,
lib, lib,
@ -103,13 +103,13 @@ in
the socket itself is `0666` (nginx runs as another user, and the socket itself is `0666` (nginx runs as another user, and
`connect(2)` needs write), exactly as hive-c0re publishes the `connect(2)` needs write), exactly as hive-c0re publishes the
per-agent sockets. What keeps that safe is that the directory holds per-agent sockets. What keeps that safe is that the directory holds
one socket and is bind-mounted into one container. Moving this path exactly one socket and is traverse-only (`0751`) for everyone else.
under a directory that carries anything else `/run/hyperhive`, Moving this path under a directory that carries anything else
which holds the host admin socket, above all hands whatever else `/run/hyperhive`, which holds the host admin socket, above all
lives there to every consumer that mounts it. exposes whatever else lives there to the same reachability.
Changing this therefore means re-checking the gateway bind-mount, Changing this therefore means re-checking what else lives in the
not just the daemon. new directory, not just the daemon.
''; '';
}; };
}; };
@ -147,9 +147,9 @@ in
# the socket path without being able to list the directory. Same # the socket path without being able to list the directory. Same
# shape (and same reason) as hive-c0re's runtime dir. # shape (and same reason) as hive-c0re's runtime dir.
RuntimeDirectoryMode = "0751"; RuntimeDirectoryMode = "0751";
# Preserved across restarts so the bind-mount source never vanishes # Preserved across restarts so the path never vanishes from under a
# from under a running gateway container. The daemon unlinks a stale # running nginx. The daemon unlinks a stale socket on start, which
# socket on start, which is what makes preservation safe. # is what makes preservation safe.
RuntimeDirectoryPreserve = "yes"; RuntimeDirectoryPreserve = "yes";
StateDirectory = "swarm-controller"; StateDirectory = "swarm-controller";