From 5b72d174381a0ab5845bb13fe5d07e6947e4a255 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:42:47 +0000 Subject: [PATCH] feat: add dnsmasq community container for LAN DNS, remove ddclient, add NC_DOMAIN Docker alias Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/7bd0c60a-c5df-404a-a8a5-5cbb97c7a48c Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com> --- Containers/ddclient/Dockerfile | 16 -------- Containers/ddclient/ddclient-config-gen.sh | 17 --------- Containers/dnsmasq/Dockerfile | 17 +++++++++ Containers/dnsmasq/start.sh | 38 +++++++++++++++++++ community-containers/ddclient/ddclient.json | 27 ------------- community-containers/ddclient/readme.md | 36 ------------------ community-containers/dnsmasq/dnsmasq.json | 17 +++++++++ community-containers/dnsmasq/readme.md | 31 +++++++++++++++ php/src/Controller/DesecController.php | 5 ++- php/src/Docker/DockerActionManager.php | 25 +++++++++--- php/templates/containers.twig | 6 +-- .../includes/community-containers.twig | 2 +- 12 files changed, 130 insertions(+), 107 deletions(-) delete mode 100644 Containers/ddclient/Dockerfile delete mode 100644 Containers/ddclient/ddclient-config-gen.sh create mode 100644 Containers/dnsmasq/Dockerfile create mode 100644 Containers/dnsmasq/start.sh delete mode 100644 community-containers/ddclient/ddclient.json delete mode 100644 community-containers/ddclient/readme.md create mode 100644 community-containers/dnsmasq/dnsmasq.json create mode 100644 community-containers/dnsmasq/readme.md diff --git a/Containers/ddclient/Dockerfile b/Containers/ddclient/Dockerfile deleted file mode 100644 index 4c4aaa9c..00000000 --- a/Containers/ddclient/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -# syntax=docker/dockerfile:latest -FROM ghcr.io/linuxserver/ddclient:latest - -# Auto-configure ddclient for deSEC when NC_DOMAIN and DESEC_TOKEN are provided. -# The linuxserver base image executes all scripts in /custom-cont-init.d/ before -# the main service starts, which lets us generate ddclient.conf without any manual step. -COPY --chmod=755 ddclient-config-gen.sh /custom-cont-init.d/ddclient-config-gen.sh - -LABEL com.centurylinklabs.watchtower.enable="false" \ - wud.watch="false" \ - org.opencontainers.image.title="DDclient for Nextcloud AIO" \ - org.opencontainers.image.description="DDclient with automatic deSEC configuration for Nextcloud All-in-One" \ - org.opencontainers.image.url="https://github.com/nextcloud/all-in-one" \ - org.opencontainers.image.source="https://github.com/nextcloud/all-in-one" \ - org.opencontainers.image.vendor="Nextcloud" \ - org.opencontainers.image.documentation="https://github.com/nextcloud/all-in-one/blob/main/community-containers/ddclient/readme.md" diff --git a/Containers/ddclient/ddclient-config-gen.sh b/Containers/ddclient/ddclient-config-gen.sh deleted file mode 100644 index 46e826f1..00000000 --- a/Containers/ddclient/ddclient-config-gen.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# Automatically generate /config/ddclient.conf for deSEC dynamic DNS when -# NC_DOMAIN and DESEC_TOKEN are provided and no config file exists yet. -# -# This script is executed by the linuxserver base image from /custom-cont-init.d/ -# before ddclient starts, so no manual configuration step is required. - -if [[ -n "${NC_DOMAIN}" && -n "${DESEC_TOKEN}" && ! -f /config/ddclient.conf ]]; then - { - printf 'daemon=300\nsyslog=yes\nssl=yes\n\n' - printf 'use=web, web=https://checkipv4.dedyn.io/\n\n' - printf 'protocol=dyndns2\nserver=update.dedyn.io\n' - printf 'login=%s\npassword=%s\n%s\n' \ - "${NC_DOMAIN}" "${DESEC_TOKEN}" "${NC_DOMAIN}" - } > /config/ddclient.conf - echo "deSEC ddclient config auto-generated for domain ${NC_DOMAIN}" -fi diff --git a/Containers/dnsmasq/Dockerfile b/Containers/dnsmasq/Dockerfile new file mode 100644 index 00000000..95d7e19f --- /dev/null +++ b/Containers/dnsmasq/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:latest +FROM alpine:3.21 + +RUN apk add --no-cache dnsmasq iproute2 + +COPY --chmod=755 start.sh /start.sh + +ENTRYPOINT ["/start.sh"] + +LABEL com.centurylinklabs.watchtower.enable="false" \ + wud.watch="false" \ + org.opencontainers.image.title="Dnsmasq for Nextcloud AIO" \ + org.opencontainers.image.description="Lightweight DNS server that resolves NC_DOMAIN to the local server IP for LAN devices" \ + org.opencontainers.image.url="https://github.com/nextcloud/all-in-one" \ + org.opencontainers.image.source="https://github.com/nextcloud/all-in-one" \ + org.opencontainers.image.vendor="Nextcloud" \ + org.opencontainers.image.documentation="https://github.com/nextcloud/all-in-one/blob/main/community-containers/dnsmasq/readme.md" diff --git a/Containers/dnsmasq/start.sh b/Containers/dnsmasq/start.sh new file mode 100644 index 00000000..888857c3 --- /dev/null +++ b/Containers/dnsmasq/start.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -e + +if [ -z "$NC_DOMAIN" ]; then + echo "ERROR: NC_DOMAIN is not set" >&2 + exit 1 +fi + +# Determine the server's primary LAN IP - use the source address chosen by the kernel +# for a route to a well-known public IP (1.1.1.1 is used purely to query the routing table; +# no traffic is sent there). +LOCAL_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") {print $(i+1); exit}}') + +if [ -z "$LOCAL_IP" ]; then + LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}') +fi + +if [ -z "$LOCAL_IP" ]; then + echo "ERROR: Could not determine local IP address" >&2 + exit 1 +fi + +echo "Nextcloud AIO dnsmasq: resolving $NC_DOMAIN -> $LOCAL_IP" +echo "Configure your router's DHCP to hand out $LOCAL_IP as the DNS server for LAN clients." + +mkdir -p /etc/dnsmasq.d + +cat > /etc/dnsmasq.d/nextcloud-aio.conf << EOF +# Auto-generated by Nextcloud AIO dnsmasq container. +# Resolves NC_DOMAIN (and all its subdomains) to this server's local IP. +address=/$NC_DOMAIN/$LOCAL_IP + +# Bind only to the LAN interface to avoid conflicts with any system DNS resolver. +bind-interfaces +listen-address=$LOCAL_IP +EOF + +exec dnsmasq --no-daemon --log-queries --conf-dir=/etc/dnsmasq.d diff --git a/community-containers/ddclient/ddclient.json b/community-containers/ddclient/ddclient.json deleted file mode 100644 index 685dda50..00000000 --- a/community-containers/ddclient/ddclient.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "aio_services_v1": [ - { - "container_name": "nextcloud-aio-ddclient", - "display_name": "DDclient (deSEC DDNS)", - "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/ddclient", - "image": "ghcr.io/nextcloud-releases/aio-ddclient", - "image_tag": "%AIO_CHANNEL%", - "restart": "unless-stopped", - "environment": [ - "TZ=%TIMEZONE%", - "NC_DOMAIN=%NC_DOMAIN%", - "DESEC_TOKEN=%DESEC_TOKEN%" - ], - "volumes": [ - { - "source": "nextcloud_aio_ddclient", - "destination": "/config", - "writeable": true - } - ], - "backup_volumes": [ - "nextcloud_aio_ddclient" - ] - } - ] -} diff --git a/community-containers/ddclient/readme.md b/community-containers/ddclient/readme.md deleted file mode 100644 index 7bfb1dcc..00000000 --- a/community-containers/ddclient/readme.md +++ /dev/null @@ -1,36 +0,0 @@ -# DDclient community container - -This container runs [DDclient](https://ddclient.net/) pre-configured for use with [deSEC](https://desec.io/) dynamic DNS. - -## How it works - -When you register a free dedyn.io domain through the AIO interface the `NC_DOMAIN` and `DESEC_TOKEN` environment variables are automatically populated from the stored credentials. - -On first start, if `/config/ddclient.conf` does not yet exist and both `NC_DOMAIN` and `DESEC_TOKEN` are set, the container automatically generates a ready-to-use `ddclient.conf`: - -``` -daemon=300 -syslog=yes -ssl=yes - -use=web, web=https://checkipv4.dedyn.io/ - -protocol=dyndns2 -server=update.dedyn.io -login= -password= - -``` - -No manual configuration step is required. - -## Relationship to the AIO mastercontainer - -The AIO mastercontainer already updates the deSEC DNS record every time containers are started and once per cron cycle (roughly every minute). This container adds a second, independent layer of DNS updates that runs continuously every 5 minutes via the ddclient daemon — useful if the host IP can change while the containers are running between cron cycles. - -## Notes - -- The config volume (`nextcloud_aio_ddclient`) is included in AIO backups, so the configuration persists across updates and restores. -- If the config file already exists (e.g., you customised it previously), it will **not** be overwritten on restart. -- For IPv6 support, add a second `use` block pointing to `https://checkipv6.dedyn.io/` in the config file. See the [ddclient documentation](https://ddclient.net/protocols/dyndns2.html) for details. -- This image is derived from [ghcr.io/linuxserver/ddclient](https://github.com/linuxserver/docker-ddclient) and adds the auto-configuration script via the linuxserver `/custom-cont-init.d/` mechanism. diff --git a/community-containers/dnsmasq/dnsmasq.json b/community-containers/dnsmasq/dnsmasq.json new file mode 100644 index 00000000..d1f9280b --- /dev/null +++ b/community-containers/dnsmasq/dnsmasq.json @@ -0,0 +1,17 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-dnsmasq", + "display_name": "Dnsmasq (Local DNS)", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq", + "image": "ghcr.io/nextcloud-releases/aio-dnsmasq", + "image_tag": "%AIO_CHANNEL%", + "internal_port": "host", + "restart": "unless-stopped", + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "TZ=%TIMEZONE%" + ] + } + ] +} diff --git a/community-containers/dnsmasq/readme.md b/community-containers/dnsmasq/readme.md new file mode 100644 index 00000000..1a2e0bb1 --- /dev/null +++ b/community-containers/dnsmasq/readme.md @@ -0,0 +1,31 @@ +# Dnsmasq (Local DNS) community container + +This container runs [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) pre-configured to resolve your Nextcloud domain (`NC_DOMAIN`) to the server's local LAN IP address. + +## Why is this needed? + +By default, all devices on your LAN reach Nextcloud via the public internet (or require hairpin NAT on your router). With this container, LAN clients can resolve `NC_DOMAIN` directly to the server's private LAN IP, making local access faster and independent of your internet connection. + +This container is automatically enabled when you register a deSEC domain through the AIO interface. + +## How it works + +On startup the container: +1. Detects the server's primary LAN IP address automatically. +2. Configures dnsmasq to resolve `NC_DOMAIN` (and all its subdomains) to that IP. +3. Forwards all other DNS queries to the upstream nameservers from the host's `/etc/resolv.conf`. +4. Listens only on the LAN interface to avoid conflicts with any system DNS resolver (e.g. `systemd-resolved`). + +## Required router configuration + +⚠️ **You must change your router's DHCP settings** for this to take effect for LAN clients: + +Set the **DNS server** handed out by DHCP to the **local IP address of this server** (the same IP that is printed in the container logs on startup). After saving the change, LAN devices need to renew their DHCP lease (or be rebooted) before the new DNS setting takes effect. + +Most routers expose this under **DHCP settings → Primary DNS** or **LAN → DNS Server**. + +## Notes + +- The container runs in **host network mode** so it can bind directly to port 53 on the LAN interface. No additional port-forwarding is required. +- If `systemd-resolved` (or another DNS resolver) is already listening on port 53 on the LAN IP, there will be a conflict. In that case you need to disable or reconfigure that resolver first. +- IPv6 addresses are not handled by this container; extend the dnsmasq configuration manually if needed. diff --git a/php/src/Controller/DesecController.php b/php/src/Controller/DesecController.php index a750dcf3..e7132e9a 100644 --- a/php/src/Controller/DesecController.php +++ b/php/src/Controller/DesecController.php @@ -60,6 +60,9 @@ readonly class DesecController { if (!in_array('caddy', $enabled, true)) { $enabled[] = 'caddy'; } + if (!in_array('dnsmasq', $enabled, true)) { + $enabled[] = 'dnsmasq'; + } $this->configurationManager->aioCommunityContainers = $enabled; $this->configurationManager->commitTransaction(); @@ -132,7 +135,7 @@ readonly class DesecController { if (is_array($data) && isset($data['email'])) { throw new \Exception( 'This email address is already registered at deSEC. ' - . 'Please log in at https://desec.io to retrieve your token and configure ddclient manually.', + . 'Please log in at https://desec.io to retrieve your token and set up your domain manually.', ); } throw new \Exception('Registration at deSEC failed (HTTP 400): ' . $body); diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index 940814fe..2f5aa311 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -805,7 +805,7 @@ readonly class DockerActionManager { } } - private function ConnectContainerIdToNetwork(string $id, string $internalPort, string $network = 'nextcloud-aio', bool $createNetwork = true, string $alias = ''): void { + private function ConnectContainerIdToNetwork(string $id, string $internalPort, string $network = 'nextcloud-aio', bool $createNetwork = true, array $aliases = []): void { if ($internalPort === 'host') { return; } @@ -837,8 +837,8 @@ readonly class DockerActionManager { sprintf('networks/%s/connect', $network) ); $jsonPayload = ['Container' => $id]; - if ($alias !== '') { - $jsonPayload['EndpointConfig'] = ['Aliases' => [$alias]]; + if (count($aliases) > 0) { + $jsonPayload['EndpointConfig'] = ['Aliases' => $aliases]; } try { @@ -864,17 +864,30 @@ readonly class DockerActionManager { } public function ConnectContainerToNetwork(Container $container): void { + $aliases = []; + // Add a secondary alias for domaincheck container, to keep it as similar to actual apache controller as possible. // If a reverse-proxy is relying on container name as hostname this allows it to operate as usual and still validate the domain // The domaincheck container and apache container are never supposed to be active at the same time because they use the same APACHE_PORT anyway, so this doesn't add any new constraints. - $alias = ($container->identifier === 'nextcloud-aio-domaincheck') ? 'nextcloud-aio-apache' : ''; + if ($container->identifier === 'nextcloud-aio-domaincheck') { + $aliases[] = 'nextcloud-aio-apache'; + } - $this->ConnectContainerIdToNetwork($container->identifier, $container->internalPorts, alias: $alias); + // Add NC_DOMAIN as a Docker network alias so that intra-network traffic for the Nextcloud + // domain is forwarded directly to this container without leaving the Docker network. + if ($container->identifier === 'nextcloud-aio-apache' || $container->identifier === 'nextcloud-aio-domaincheck') { + $domain = $this->configurationManager->domain; + if ($domain !== '') { + $aliases[] = $domain; + } + } + + $this->ConnectContainerIdToNetwork($container->identifier, $container->internalPorts, aliases: $aliases); if ($container->identifier === 'nextcloud-aio-apache' || $container->identifier === 'nextcloud-aio-domaincheck') { $apacheAdditionalNetwork = $this->configurationManager->getApacheAdditionalNetwork(); if ($apacheAdditionalNetwork !== '') { - $this->ConnectContainerIdToNetwork($container->identifier, $container->internalPorts, $apacheAdditionalNetwork, false, $alias); + $this->ConnectContainerIdToNetwork($container->identifier, $container->internalPorts, $apacheAdditionalNetwork, false, $aliases); } } } diff --git a/php/templates/containers.twig b/php/templates/containers.twig index 37273ead..2b58d673 100644 --- a/php/templates/containers.twig +++ b/php/templates/containers.twig @@ -124,7 +124,7 @@
Click here for further hints

If you do not have a domain yet, you can get one for free e.g. from duckdns.org and others. Recommended is to use Tailscale

-

If you have a dynamic public IP-address, you can use e.g. DDclient with a compatible domain provider for DNS updates.

+

If you have a dynamic public IP address, you can use e.g. a DDNS client with a compatible domain provider for DNS updates.

If you only want to install AIO locally without exposing it to the public internet or if you cannot do so, feel free to follow this documentation.

If you should be using Cloudflare Proxy for your domain, make sure to disable the Proxy feature temporarily as it might block the domain validation attempts.

{% if apache_port != '443' %} @@ -134,7 +134,7 @@
Don't have a domain? Get a free one from deSEC -

deSEC offers free dynamic DNS subdomains under dedyn.io. AIO can register an account and a subdomain for you automatically. The caddy community container will be enabled as a reverse proxy, and the mastercontainer will keep your DNS record up to date automatically. You can additionally enable the ddclient community container for continuous DNS monitoring between cron cycles.

+

deSEC offers free dynamic DNS subdomains under dedyn.io. AIO can register an account and a subdomain for you automatically. The caddy community container will be enabled as a reverse proxy, the dnsmasq container will be enabled for local DNS resolution, and the mastercontainer will keep your DNS record up to date automatically.

Requirements: Your server must be reachable from the internet (a public IP address is needed). Port 80 and 443 must be open/forwarded in your firewall/router.

Please enter your email address. A deSEC account and a random subdomain.dedyn.io domain will be created for you.

@@ -143,7 +143,7 @@
-

Note: By submitting this form you agree to the deSEC terms of service. The registered domain and your deSEC account credentials are stored in the AIO configuration. After registration, finish the setup by configuring the ddclient container as described in its documentation.

+

Note: By submitting this form you agree to the deSEC terms of service. The registered domain and your deSEC account credentials are stored in the AIO configuration. After registration, set your router's DHCP DNS server to this machine's local IP address so LAN devices resolve the domain locally (see the dnsmasq documentation).

{% endif %} diff --git a/php/templates/includes/community-containers.twig b/php/templates/includes/community-containers.twig index b9f46209..0a39f483 100644 --- a/php/templates/includes/community-containers.twig +++ b/php/templates/includes/community-containers.twig @@ -2,7 +2,7 @@

In this section you can enable or disable optional Community Containers that are not included by default in the main installation. These containers are provided by the community and can be useful for various purposes and are automatically integrated in AIOs backup solution and update mechanisms.

⚠️ Caution: Community Containers are maintained by the community and not officially by Nextcloud. Some containers may not be compatible with your system, may not work as expected or may discontinue. Use them at your own risk. Please read the documentation for each container first before adding any as some are also incompatible between each other! Never add all of them at the same time!

{% if is_desec_domain == true %} -

ℹ️ Your Nextcloud domain ({{ domain }}) was registered via deSEC. The caddy community container has been automatically enabled as a reverse proxy. The mastercontainer keeps the DNS record up to date; you can optionally also enable the ddclient container for continuous DNS monitoring between cron cycles. Please see its documentation for details.

+

ℹ️ Your Nextcloud domain ({{ domain }}) was registered via deSEC. The caddy community container has been automatically enabled as a reverse proxy and the dnsmasq container has been automatically enabled so that LAN devices can resolve your Nextcloud domain to the server's local IP address. Please read the dnsmasq documentation for the required router change.

{% endif %} {% if isAnyRunning == true %}

Please note: You can enable or disable the options below only when your containers are stopped.