Compare commits

..

23 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
e1b13cda63 refactor(nextcloud): extract inline OAuth2 client PHP into standalone script
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/b35a7dbb-ff0a-483c-9bc7-1cf43a192e92

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-28 16:37:23 +00:00
copilot-swe-agent[bot]
8925baf12f nc entrypoint: improve Windmill OAuth failure diagnostic message
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/ad1489a0-4da0-41fd-b1d0-46da7ef545c3

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-28 12:31:37 +00:00
copilot-swe-agent[bot]
b3f66b4c50 nc entrypoint: replace fake windmill_admin_token with real NC OAuth2 SSO setup
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/ad1489a0-4da0-41fd-b1d0-46da7ef545c3

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-28 12:29:43 +00:00
copilot-swe-agent[bot]
e1e85c5ca9 nc entrypoint: include HTTP status in Windmill auth failure message
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/b977bac7-8311-4877-ba23-92438ed0a4fe

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-28 12:04:58 +00:00
copilot-swe-agent[bot]
1ee3db8195 nc entrypoint: set windmill_admin_token and verify API auth against Windmill
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/b977bac7-8311-4877-ba23-92438ed0a4fe

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-28 12:03:44 +00:00
copilot-swe-agent[bot]
56afde115d windmill: secret protection, socket-only postgres, volume rename/backup
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/8bfb1fa8-7878-434e-ab4d-1034067e5ad0

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-28 11:30:53 +00:00
copilot-swe-agent[bot]
b205e46976 windmill: move dump to dedicated volume, remove unix_http_server, simplify upgrade path
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/d87bac25-9c31-4c46-ad37-1e6426a3af2e

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-28 11:23:20 +00:00
copilot-swe-agent[bot]
e077aea86d Fix windmill postgres upgrade reliability: persistent-volume staging dir, tighten sentinels, supervisorctl
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/5e1df9cc-b63e-4a5a-9148-38907b6d8db9

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-28 11:15:38 +00:00
copilot-swe-agent[bot]
c60861d79c Replace pg_upgrade with pg_dump/restore for windmill postgres major-version upgrade
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/58741c1d-7822-425b-a995-deab87e5901c

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 14:54:28 +00:00
copilot-swe-agent[bot]
1823336f89 Pin exact postgres/windmill versions, add auto pg_upgrade, extend healthcheck
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/505f902b-a447-4fcc-9203-f11119207409

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 14:26:08 +00:00
copilot-swe-agent[bot]
d63f0afc63 Remove WINDMILL_ENABLED from apache start.sh, simplify windmill cert config, add cache invalidation on image update
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/6ba2c0de-f503-4989-9dea-4bc64a51e4f9

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 09:45:42 +00:00
copilot-swe-agent[bot]
9f90afbc5e Add Windmill as optional container, served on /windmill subpath
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/e78b6157-a4ea-4185-bf37-1659a458d4db

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 09:41:09 +00:00
copilot-swe-agent[bot]
51124784c2 Serve windmill on /windmill subpath instead of port 3100; add to disable-containers.js
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/3c346202-c2e6-4e97-bf61-a1f5d42670fd

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 09:30:29 +00:00
copilot-swe-agent[bot]
f08103ca15 Fix rootless windmill container: use uid=1000, fix tmpfs modes, CA certs, WINDMILL_DIR
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/81d1b5cc-ab2d-4337-801b-25755355183e

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 09:16:25 +00:00
copilot-swe-agent[bot]
312acddf27 Changes before error encountered
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/6f198732-63c3-41b7-8b2e-1b5fa565ee21

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 02:12:13 +00:00
copilot-swe-agent[bot]
5cbdb00ff4 WIP: windmill derived image builds successfully, adding rootless + read-only
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/6f198732-63c3-41b7-8b2e-1b5fa565ee21

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 02:10:58 +00:00
copilot-swe-agent[bot]
4b27d6954f WIP: implementing windmill container changes
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/6f198732-63c3-41b7-8b2e-1b5fa565ee21

Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-04-27 01:58:39 +00:00
Simon L.
8727df147d Add disclaimer and clarify migration steps for snap installation
Added a disclaimer regarding the accuracy of the migration guide and clarified the use of a temporary Docker container for database conversion.

Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-04-26 19:59:36 +02:00
Simon L.
a747ab1c5e migration.md: add snap-to-AIO migration procedure using temporary Docker container (#8003) 2026-04-26 18:11:17 +02:00
copilot-swe-agent[bot]
f28d94c30c migration.md: add snap-to-AIO migration procedure using temporary Docker container
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/64e677c8-17ac-4c15-93db-e7375d2a2084

docs: address code review feedback on snap migration section

Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/64e677c8-17ac-4c15-93db-e7375d2a2084

docs: improve snap migration section per feedback

Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/f71332ba-4e6b-465b-8278-6767fe3a62d3

docs: add snap removal step after successful migration to AIO

Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/b3c5c0c5-79dd-4fa3-b617-6db88ee99431

Apply suggestion from @szaimen

Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-04-26 18:08:40 +02:00
Simon L.
6dc1cd6ebd adjust community-container readme
Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-04-25 17:09:23 +02:00
Simon L.
670e38969c docs: add Pangolin to reverse proxy guide (#7996) 2026-04-25 16:25:04 +02:00
copilot-swe-agent[bot]
ae5a21eadf docs: add Pangolin to reverse proxy guide
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/c7f8b130-4edd-4fcc-b218-1522ba88eae8

docs: add Pangolin option to local-instance docs

Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/0ad32b82-c0ae-46f4-9849-af64ae130dbd

docs: address review feedback on Pangolin section

Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/c7f8b130-4edd-4fcc-b218-1522ba88eae8
Co-Authored-By: szaimen <42591237+szaimen@users.noreply.github.com>
Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-04-25 16:24:13 +02:00
38 changed files with 787 additions and 628 deletions

View File

@@ -63,6 +63,12 @@ http://{$APACHE_HOST}.nextcloud-aio:23973, # For Collabora callback and WOPI req
reverse_proxy {$WHITEBOARD_HOST}:3002
}
# Windmill
route /windmill/* {
uri strip_prefix /windmill
reverse_proxy {$WINDMILL_HOST}:8000
}
# HaRP (ExApps)
route /exapps/* {
reverse_proxy {$HARP_HOST}:8780

View File

@@ -1,17 +0,0 @@
# 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"

View File

@@ -1,40 +0,0 @@
#!/bin/sh
set -e
if [ -z "$NC_DOMAIN" ]; then
echo "ERROR: NC_DOMAIN is not set" >&2
exit 1
fi
LOCAL_IP=""
# 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

View File

@@ -51,9 +51,6 @@ while true; do
# Check if AIO is outdated
sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/OutdatedNotification.php
# Update deSEC DNS IP record (no-op when IP is unchanged or deSEC is not configured)
sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/UpdateDesecIp.php
# Remove sessions older than 24h
find "/mnt/docker-aio-config/session/" -mindepth 1 -mmin +1440 -delete

View File

@@ -263,6 +263,8 @@ RUN set -ex; \
mkdir -p /nc-updater; \
chmod -R 777 /nc-updater
COPY --chmod=775 Containers/nextcloud/create-oauth2-client.php /create-oauth2-client.php
# hadolint ignore=DL3002
USER root
ENTRYPOINT ["/start.sh"]

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* Creates a Nextcloud OAuth2 client and prints its credentials to stdout.
*
* Usage: php create-oauth2-client.php <name> <redirect-uri>
*
* Output (two lines):
* <client_id>
* <client_secret>
*
* Any existing client with the same name is deleted first so that the
* operation is idempotent (stale clients whose secret has been lost are
* replaced by a fresh one).
*/
if ($argc !== 3) {
fwrite(STDERR, "Usage: php create-oauth2-client.php <name> <redirect-uri>\n");
exit(1);
}
$name = $argv[1];
$redirectUri = $argv[2];
define('OC_CONSOLE', 1);
require_once '/var/www/html/lib/base.php';
\OC_App::loadApp('oauth2');
$container = \OC::getContainer();
/** @var \OCA\OAuth2\Db\ClientMapper $mapper */
$mapper = $container->get(\OCA\OAuth2\Db\ClientMapper::class);
/** @var \OCP\Security\ICrypto $crypto */
$crypto = $container->get(\OCP\Security\ICrypto::class);
/** @var \OCP\Security\ISecureRandom $random */
$random = $container->get(\OCP\Security\ISecureRandom::class);
// Delete any stale client with the same name that might have lost its secret.
foreach ($mapper->getClients() as $existing) {
if ($existing->getName() === $name) {
$mapper->delete($existing);
}
}
$client = new \OCA\OAuth2\Db\Client();
$client->setName($name);
$client->setRedirectUri($redirectUri);
$secret = $random->generate(64, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
$client->setSecret(bin2hex($crypto->calculateHMAC($secret)));
$clientId = $random->generate(64, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
$client->setClientIdentifier($clientId);
$mapper->insert($client);
echo $clientId . "\n";
echo $secret . "\n";

View File

@@ -1090,5 +1090,41 @@ else
fi
fi
# Windmill app
if [ "$WINDMILL_ENABLED" = 'yes' ]; then
if ! [ -d "/var/www/html/custom_apps/windmill" ]; then
php /var/www/html/occ app:install windmill
elif [ "$(php /var/www/html/occ config:app:get windmill enabled)" != "yes" ]; then
php /var/www/html/occ app:enable windmill
elif [ "$SKIP_UPDATE" != 1 ]; then
php /var/www/html/occ app:update windmill
fi
php /var/www/html/occ config:app:set windmill windmill_url --value="https://$NC_DOMAIN/windmill"
php /var/www/html/occ config:app:set windmill windmill_instance_url --value="http://$WINDMILL_HOST:8000"
# Create an NC OAuth2 client for Windmill (idempotent: reuse stored credentials if already created)
WINDMILL_NC_OAUTH_CLIENT_ID="$(php /var/www/html/occ config:app:get windmill nc_oauth_client_id 2>/dev/null)"
WINDMILL_NC_OAUTH_CLIENT_SECRET="$(php /var/www/html/occ config:app:get windmill nc_oauth_client_secret 2>/dev/null)"
if [ -z "$WINDMILL_NC_OAUTH_CLIENT_ID" ] || [ -z "$WINDMILL_NC_OAUTH_CLIENT_SECRET" ]; then
WINDMILL_NC_REDIRECT_URI="https://$NC_DOMAIN/windmill/user/login_callback/nextcloud"
WINDMILL_OAUTH_OUTPUT="$(php /create-oauth2-client.php "Windmill" "$WINDMILL_NC_REDIRECT_URI" 2>/dev/null)"
WINDMILL_NC_OAUTH_CLIENT_ID="$(printf '%s' "$WINDMILL_OAUTH_OUTPUT" | head -1)"
WINDMILL_NC_OAUTH_CLIENT_SECRET="$(printf '%s' "$WINDMILL_OAUTH_OUTPUT" | tail -1)"
php /var/www/html/occ config:app:set windmill nc_oauth_client_id --value="$WINDMILL_NC_OAUTH_CLIENT_ID"
php /var/www/html/occ config:app:set windmill nc_oauth_client_secret --value="$WINDMILL_NC_OAUTH_CLIENT_SECRET"
fi
# Configure Windmill to use NC as OAuth SSO provider
WINDMILL_OAUTH_BODY="{\"value\":{\"nextcloud\":{\"id\":\"$WINDMILL_NC_OAUTH_CLIENT_ID\",\"secret\":\"$WINDMILL_NC_OAUTH_CLIENT_SECRET\",\"login_config\":{\"auth_url\":\"https://$NC_DOMAIN/apps/oauth2/authorize\",\"token_url\":\"https://$NC_DOMAIN/apps/oauth2/api/v1/token\",\"userinfo_url\":\"https://$NC_DOMAIN/ocs/v2.php/cloud/user?format=json\",\"scopes\":[]}}}}"
WINDMILL_OAUTH_RESPONSE="$(curl -s -w '\n%{http_code}' -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $WINDMILL_SECRET" -d "$WINDMILL_OAUTH_BODY" "http://$WINDMILL_HOST:8000/api/settings/global/oauths")"
WINDMILL_OAUTH_STATUS="$(echo "$WINDMILL_OAUTH_RESPONSE" | tail -1)"
if [ "$WINDMILL_OAUTH_STATUS" != "200" ]; then
echo "Failed to configure Windmill OAuth against http://$WINDMILL_HOST:8000 (HTTP $WINDMILL_OAUTH_STATUS). Response: $(echo "$WINDMILL_OAUTH_RESPONSE" | head -1). Exiting!"
exit 1
fi
else
if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/windmill" ]; then
php /var/www/html/occ app:remove windmill
fi
fi
# Remove the update skip file always
rm -f "$NEXTCLOUD_DATA_DIR"/skip.update

View File

@@ -0,0 +1,116 @@
# syntax=docker/dockerfile:latest
# Stage 1: Current PostgreSQL server version is pinned to a full patch
# release so builds are reproducible and the exact version is auditable.
# (matches the Debian bookworm base used by windmill-labs/windmill)
FROM postgres:17.9-bookworm AS postgres-base
# Final stage: derive from the official Windmill image and bundle PostgreSQL
FROM ghcr.io/windmill-labs/windmill:1.691.1
USER root
ARG DEBIAN_FRONTEND=noninteractive
# Copy PostgreSQL server binaries, libraries, and utilities from the postgres stage
COPY --from=postgres-base /usr/lib/postgresql /usr/lib/postgresql
COPY --from=postgres-base /usr/share/postgresql /usr/share/postgresql
COPY --from=postgres-base /usr/bin/pg_dump \
/usr/bin/pg_dumpall \
/usr/bin/pg_restore \
/usr/bin/
# Install supervisor from standard Debian repos (remove broken external sources first)
# hadolint ignore=DL3008
RUN set -ex; \
rm -f \
/etc/apt/sources.list.d/nodesource.sources \
/etc/apt/sources.list.d/pgdg.list; \
apt-get update; \
apt-get upgrade -y; \
apt-get install -y --no-install-recommends \
ca-certificates \
supervisor \
tzdata \
netcat-openbsd; \
rm -rf /var/lib/apt/lists/*; \
update-ca-certificates; \
# Copy the CA bundle to a world-readable path so uid=1000 can access it.
# /etc/ssl/certs/ is persistently mode 700 in the base image's layer and
# cannot be chmod'd in a derived image, so we copy the regenerated bundle.
cp /etc/ssl/certs/ca-certificates.crt /etc/ssl/ca-bundle.crt; \
chmod 644 /etc/ssl/ca-bundle.crt; \
\
# The base image already ships a 'windmill' user/group at uid/gid 1000.
# Create the directories our services need and hand them to that user.
# /tmp/windmill/cache is intentionally excluded: the base image pre-creates
# it as 777 so any UID can write to it; a recursive chown on ~340 MB of
# pre-installed runtimes would create a huge and unnecessary image layer.
mkdir -p \
/var/lib/postgresql/data \
/var/lib/windmill-dump \
/var/run/postgresql \
/var/log/supervisord \
/var/run/supervisord; \
chown -R windmill:windmill \
/var/lib/postgresql \
/var/lib/windmill-dump \
/var/run/postgresql \
/var/log/supervisord \
/var/run/supervisord; \
chmod 1777 \
/var/run/postgresql \
/var/log/supervisord \
/var/run/supervisord; \
\
# Create symlinks so postgres tools are on PATH
ln -sf /usr/lib/postgresql/17/bin/postgres /usr/local/bin/postgres; \
ln -sf /usr/lib/postgresql/17/bin/initdb /usr/local/bin/initdb; \
ln -sf /usr/lib/postgresql/17/bin/pg_ctl /usr/local/bin/pg_ctl; \
ln -sf /usr/lib/postgresql/17/bin/pg_isready /usr/local/bin/pg_isready; \
\
# Record the current PostgreSQL major version so start.sh can detect when
# the bundled major version has changed and trigger an automatic dump/restore upgrade.
echo "17" > /etc/postgres-major-version; \
\
# Write a build-time marker so start.sh can detect image updates and
# clear the cache volume when a new image version is deployed.
date -u +%s > /etc/windmill-image-build-epoch
COPY --chmod=775 start.sh /start.sh
COPY --chmod=775 healthcheck.sh /healthcheck.sh
COPY --chmod=775 windmill-start.sh /windmill-start.sh
COPY --chmod=664 supervisord.conf /supervisord.conf
# Redirect writable tool dirs to the persistent cache volume so the
# container can run with a read-only root filesystem.
# HOME is already correct (/home/windmill) for the pre-existing uid=1000 user.
# WINDMILL_DIR is set to the cache volume so Windmill's logs/worker dirs
# are inside the volume and writable (Docker creates the volume mount-point
# parent /tmp/windmill as root:root, making /tmp/windmill/logs inaccessible
# for uid=1000 when /tmp is a tmpfs).
ENV UV_TOOL_BIN_DIR=/tmp/windmill/cache/uv/bin \
UV_TOOL_DIR=/tmp/windmill/cache/uv/tools \
WINDMILL_DIR=/tmp/windmill/cache \
SSL_CERT_FILE=/etc/ssl/ca-bundle.crt \
CURL_CA_BUNDLE=/etc/ssl/ca-bundle.crt \
REQUESTS_CA_BUNDLE=/etc/ssl/ca-bundle.crt \
NODE_EXTRA_CA_CERTS=/etc/ssl/ca-bundle.crt
VOLUME ["/var/lib/postgresql/data", "/var/lib/windmill-dump", "/tmp/windmill/cache"]
# Use the pre-existing windmill user (uid=1000) from the base image
USER 1000
EXPOSE 8000
ENTRYPOINT ["/start.sh"]
HEALTHCHECK CMD /healthcheck.sh
LABEL com.centurylinklabs.watchtower.enable="false" \
wud.watch="false" \
org.opencontainers.image.title="Windmill for Nextcloud AIO" \
org.opencontainers.image.description="Windmill workflow engine with bundled PostgreSQL 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/readme.md"

View File

@@ -0,0 +1,11 @@
#!/bin/bash
# Check if PostgreSQL is accepting connections on the Unix socket
if ! pg_isready -h /var/run/postgresql -q 2>/dev/null; then
exit 1
fi
# Check if Windmill is accepting connections on port 8000
if ! nc -z localhost 8000; then
exit 1
fi

View File

@@ -0,0 +1,202 @@
#!/bin/bash
set -e
# Validate required environment variables
if [ -z "$BASE_URL" ]; then
echo "BASE_URL must be provided. Exiting!"
exit 1
fi
export TZ="${TZ:-Etc/UTC}"
# Clear the cache volume when the image has been updated.
# /etc/windmill-image-build-epoch is written at image build time.
# A copy is stored in the cache volume after first start.
# If the two differ the image was updated and any stale cached artefacts
# (uv tools, worker dirs) should be removed so Windmill starts clean.
IMAGE_EPOCH_FILE="/etc/windmill-image-build-epoch"
CACHE_EPOCH_FILE="/tmp/windmill/cache/.image-build-epoch"
if [ -f "$IMAGE_EPOCH_FILE" ]; then
IMAGE_EPOCH="$(cat "$IMAGE_EPOCH_FILE")"
if [ -f "$CACHE_EPOCH_FILE" ]; then
CACHE_EPOCH="$(cat "$CACHE_EPOCH_FILE")"
if [ "$IMAGE_EPOCH" != "$CACHE_EPOCH" ]; then
echo "Windmill image updated (was $CACHE_EPOCH, now $IMAGE_EPOCH). Clearing cache..."
find /tmp/windmill/cache -mindepth 1 -maxdepth 1 ! -name '.image-build-epoch' -exec rm -rf {} +
fi
fi
echo "$IMAGE_EPOCH" > "$CACHE_EPOCH_FILE"
fi
PGDATA="/var/lib/postgresql/data"
# The dump and its sentinel/log files live on a dedicated Docker volume that is
# separate from the postgres data directory. This means the dump survives a
# complete PGDATA wipe (which happens during a major-version upgrade) and there
# is no need for a staging subdirectory or complex file exclusion logic.
DUMP_DIR="/var/lib/windmill-dump"
DUMP_FILE="$DUMP_DIR/windmill-db-dump.sql"
# Current PG major version as shipped in this image
CURRENT_PG_MAJOR=$(cat /etc/postgres-major-version 2>/dev/null)
# ── Don't start if previous import failed ────────────────────────────────────
if [ -f "$DUMP_DIR/import.failed" ]; then
echo "The database import failed the last time. Please restore a backup and try again."
echo "For further clues on what went wrong, look at the logs above."
exit 1
fi
# ── Don't start if previous export failed ────────────────────────────────────
if [ -f "$DUMP_DIR/export.failed" ]; then
echo "Database export failed the last time. Most likely was the export time not high enough."
echo "Please report this to https://github.com/nextcloud/all-in-one/issues. Thanks!"
exit 1
fi
# Write the standard pg_hba.conf and listen_addresses settings into a data directory.
configure_pg() {
local datadir="$1"
cat > "$datadir/pg_hba.conf" << 'HBAEOF'
# TYPE DATABASE USER ADDRESS METHOD
local all all trust
HBAEOF
# Disable TCP entirely; all communication uses the Unix socket.
echo "listen_addresses = ''" >> "$datadir/postgresql.conf"
}
# ── PostgreSQL major-version upgrade via dump/restore ────────────────────────
if [ -f "$PGDATA/PG_VERSION" ]; then
DATA_PG_MAJOR=$(cat "$PGDATA/PG_VERSION")
if [ "$DATA_PG_MAJOR" -gt "$CURRENT_PG_MAJOR" ]; then
echo "ERROR: Data directory was created by PostgreSQL $DATA_PG_MAJOR but this image ships $CURRENT_PG_MAJOR."
echo "Downgrade is not supported. Please use a newer image version."
exit 1
fi
if [ "$DATA_PG_MAJOR" -lt "$CURRENT_PG_MAJOR" ]; then
echo "PostgreSQL major-version upgrade required: $DATA_PG_MAJOR$CURRENT_PG_MAJOR"
if ! [ -f "$DUMP_FILE" ]; then
echo "Unable to upgrade because the database dump is missing."
echo "Please restore a backup and try again."
exit 1
fi
# Write output to logfile so the import can be inspected later
exec > >(tee -i "$DUMP_DIR/database-import.log")
exec 2>&1
echo "Restoring database from dump into new PostgreSQL $CURRENT_PG_MAJOR cluster."
# Set the sentinel BEFORE any destructive operation so that a crash at
# any point leaves the guard in place and blocks the next start.
# The sentinel lives on the dump volume and therefore survives the PGDATA wipe.
touch "$DUMP_DIR/import.failed"
set -ex
# Wipe the old cluster and initialise a fresh one.
# The dump file is on a separate volume and is not affected.
rm -rf "${PGDATA:?}/"*
initdb -D "$PGDATA" \
--username=windmill \
--auth-local=trust \
--auth-host=trust \
--no-instructions
configure_pg "$PGDATA"
# Start postgres temporarily on a socket in /tmp so we can import.
# No TCP port is needed since we connect via the socket.
postgres -D "$PGDATA" -k /tmp -h "" &
TEMP_PG_PID=$!
# Wait until postgres accepts connections
while ! psql -h /tmp -U windmill -d postgres -c "select now()" > /dev/null 2>&1; do
echo "Waiting for the temporary database to start..."
sleep 5
done
# Create the windmill database
psql -h /tmp -U windmill -d postgres \
-c "CREATE DATABASE windmill OWNER windmill;"
# Restore from dump
echo "Restoring the database from dump..."
psql -h /tmp -U windmill -d windmill < "$DUMP_FILE"
# Stop the temporary postgres cleanly
pg_ctl -D "$PGDATA" stop -m smart -t 1800
wait "$TEMP_PG_PID" 2>/dev/null || true
set +ex
# Remove the sentinel only after the restore has fully completed
rm "$DUMP_DIR/import.failed"
echo "PostgreSQL upgrade to $CURRENT_PG_MAJOR complete."
fi
fi
# ── End of major-version upgrade section ─────────────────────────────────────
# ── Initialize PostgreSQL data directory on first run ────────────────────────
if [ -z "$(ls -A "$PGDATA" 2>/dev/null)" ]; then
echo "Initializing PostgreSQL database for Windmill..."
initdb -D "$PGDATA" \
--username=windmill \
--auth-local=trust \
--auth-host=trust \
--no-instructions
configure_pg "$PGDATA"
# Start PostgreSQL temporarily to create the windmill database, then stop it.
# supervisord will restart it properly afterward.
pg_ctl -D "$PGDATA" start -w -o "-k /var/run/postgresql"
psql -h /var/run/postgresql -U windmill postgres \
-c "CREATE DATABASE windmill OWNER windmill;"
pg_ctl -D "$PGDATA" stop -w
echo "PostgreSQL initialization complete."
fi
# ── Dump database and shut down on container stop ────────────────────────────
do_database_dump() {
# pg_dump uses a consistent transaction snapshot, so it is safe to run
# while Windmill is still connected — no need to stop it beforehand.
# Verify postgres is still accepting connections before attempting the dump.
if ! pg_isready -h /var/run/postgresql -U windmill -q; then
echo "WARNING: postgres is not ready; skipping dump."
kill "$SUPERVISORD_PID" 2>/dev/null || true
wait "$SUPERVISORD_PID" 2>/dev/null || true
return
fi
set -x
touch "$DUMP_DIR/export.failed"
rm -f "$DUMP_FILE.temp"
if pg_dump -h /var/run/postgresql -U windmill windmill > "$DUMP_FILE.temp"; then
mv "$DUMP_FILE.temp" "$DUMP_FILE"
rm "$DUMP_DIR/export.failed"
echo "Database dump successful!"
else
rm -f "$DUMP_FILE.temp"
echo "Database dump unsuccessful!"
fi
set +x
# Stop supervisord (which stops postgres and windmill)
kill "$SUPERVISORD_PID" 2>/dev/null || true
wait "$SUPERVISORD_PID" 2>/dev/null || true
}
trap do_database_dump SIGTERM SIGINT
# ── Start services ────────────────────────────────────────────────────────────
supervisord -c /supervisord.conf &
SUPERVISORD_PID=$!
wait "$SUPERVISORD_PID"

View File

@@ -0,0 +1,29 @@
[supervisord]
nodaemon=true
logfile=/var/log/supervisord/supervisord.log
pidfile=/var/run/supervisord/supervisord.pid
childlogdir=/var/log/supervisord/
logfile_maxbytes=50MB
logfile_backups=10
loglevel=error
[program:postgresql]
command=postgres -D /var/lib/postgresql/data -k /var/run/postgresql
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
# Start first: Windmill depends on PostgreSQL being ready
priority=10
autorestart=true
startsecs=5
[program:windmill]
command=/windmill-start.sh
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
priority=20
autorestart=true
startsecs=10

View File

@@ -0,0 +1,10 @@
#!/bin/bash
# Wait for PostgreSQL to accept connections
until pg_isready -h /var/run/postgresql -q 2>/dev/null; do
echo "Waiting for PostgreSQL to be ready..."
sleep 2
done
# Start Windmill
exec windmill

View File

@@ -20,8 +20,7 @@
"NC_DOMAIN=%NC_DOMAIN%",
"APACHE_PORT=%APACHE_PORT%",
"APACHE_IP_BINDING=%APACHE_IP_BINDING%",
"NEXTCLOUD_EXPORTER_CADDY_PASSWORD=%NEXTCLOUD_EXPORTER_CADDY_PASSWORD%",
"DESEC_TOKEN=%DESEC_TOKEN%"
"NEXTCLOUD_EXPORTER_CADDY_PASSWORD=%NEXTCLOUD_EXPORTER_CADDY_PASSWORD%"
],
"volumes": [
{

View File

@@ -1,17 +0,0 @@
{
"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%"
]
}
]
}

View File

@@ -1,31 +0,0 @@
# 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.

View File

@@ -61,7 +61,8 @@ flowchart TD
## How to use this?
Starting with v11 of AIO, the management of Community Containers is done via the AIO interface (it is the last section in the AIO interface, so only visible if you scroll down).
⚠️⚠️⚠️ Please review the folder for documentation on each of the containers before adding them! Not reviewing the documentation for each of them first might break starting the AIO containers because e.g. fail2ban only works on Linux and not on Docker Desktop! **Hint:** If the containers where running already, in order to actually start the added container, you need to click on `Stop containers` and the `Update and start containers` in order to actually start it.
⚠️⚠️⚠️ Please review the folder for documentation on each of the containers before adding them! Not reviewing the documentation for each of them first might break starting the AIO containers because some containers are not compatible with each other and more.
## How to add containers?
Simply submit a PR by creating a new folder in this directory: https://github.com/nextcloud/all-in-one/tree/main/community-containers with the name of your container. It must include a json file with the same name and with correct syntax and a readme.md with additional information. You might get inspired by caddy, fail2ban, local-ai, libretranslate, plex, pi-hole or vaultwarden (subfolders in this directory). For a full-blown example of the json file, see https://github.com/nextcloud/all-in-one/blob/main/php/containers.json. The json-schema that it validates against can be found here: https://github.com/nextcloud/all-in-one/blob/main/php/containers-schema.json.

View File

@@ -41,7 +41,7 @@ services:
# WATCHTOWER_DOCKER_SOCKET_PATH: /var/run/docker.sock # Needs to be specified if the docker socket on the host is not located in the default '/var/run/docker.sock'. Otherwise mastercontainer updates will fail. For macos it needs to be '/var/run/docker.sock'
# # Optional: Caddy reverse proxy. See https://github.com/nextcloud/all-in-one/discussions/575
# # Alternatively, if you don't have a domain yet, use the built-in deSEC free domain registration in the AIO interface, or use Tailscale. See https://github.com/nextcloud/all-in-one#how-to-get-a-free-domain-via-desec and https://github.com/nextcloud/all-in-one/discussions/6817
# # Alternatively, use Tailscale if you don't have a domain yet. See https://github.com/nextcloud/all-in-one/discussions/6817
# # Hint: You need to uncomment APACHE_PORT: 11000 above, adjust cloud.example.com to your domain and uncomment the necessary docker volumes at the bottom of this file in order to make it work
# # You can find further examples here: https://github.com/nextcloud/all-in-one/discussions/588
# caddy:

View File

@@ -2,25 +2,22 @@
It is possible due to several reasons that you do not want or cannot open Nextcloud to the public internet. Perhaps you were hoping to access AIO directly from an `ip.add.r.ess` (unsupported) or without a valid domain. However, AIO requires a valid certificate to work correctly. Below is discussed how you can achieve both: Having a valid certificate for Nextcloud and only using it locally.
### Content
- [1. deSEC free domain (recommended)](#1-desec-free-domain-recommended)
- [2. Tailscale](#2-tailscale)
- [1. Tailscale](#1-tailscale)
- [2. Pangolin](#2-pangolin)
- [3. The normal way](#3-the-normal-way)
- [4. Use the ACME DNS-challenge](#4-use-the-acme-dns-challenge)
- [5. Use Cloudflare](#5-use-cloudflare)
- [6. Buy a certificate and use that](#6-buy-a-certificate-and-use-that)
## 1. deSEC free domain (recommended)
[deSEC](https://desec.io) offers free dynamic-DNS subdomains under `dedyn.io`. AIO can register an account and a subdomain for you automatically — directly from the domain-entry page of the AIO interface. After registration:
- The [Caddy](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy) community container is enabled automatically as a reverse proxy and handles TLS via Let's Encrypt.
- The [dnsmasq](https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq) community container is enabled automatically so that LAN clients resolve your Nextcloud domain to the server's local IP address — no separate Pi-hole or local DNS server required.
- The mastercontainer keeps the DNS record up to date automatically when your public IP changes.
## 1. Tailscale
This is the recommended way. For a reverse proxy example guide for Tailscale, see this guide by [@Perseus333](https://github.com/Perseus333): https://github.com/nextcloud/all-in-one/discussions/6817
**How to set it up:** Open the AIO interface, expand the **"Don't have a domain? Get a free one from deSEC"** section, enter your email and an optional subdomain slug, and click **Register free domain via deSEC**. See the full documentation at [How to get a free domain via deSEC](https://github.com/nextcloud/all-in-one#how-to-get-a-free-domain-via-desec).
## 2. Pangolin
[Pangolin](https://pangolin.net/) is an open-source, WireGuard-based remote access platform similar in concept to Tailscale. It uses the **Newt** connector to create outbound-only encrypted tunnels — no inbound ports need to be opened on your firewall. Pangolin handles TLS automatically, providing a valid certificate for your Nextcloud domain.
After registration, follow the [dnsmasq documentation](https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq) to point your router's DHCP DNS server to the AIO host so that all LAN devices resolve the domain locally.
You can use either [Pangolin Cloud](https://app.pangolin.net/) (free tier available) or [self-host your own Pangolin server](https://docs.pangolin.net/self-host/quick-install) on a VPS. For private/local-only access, self-hosting Pangolin on a machine within your local network means that Nextcloud never needs to be exposed to the public internet.
## 2. Tailscale
For a reverse proxy example guide for Tailscale, see this guide by [@Perseus333](https://github.com/Perseus333): https://github.com/nextcloud/all-in-one/discussions/6817
For the reverse proxy configuration details and a step-by-step setup guide, see the [Pangolin section in the reverse proxy documentation](./reverse-proxy.md#pangolin).
## 3. The normal way
The normal way is the following:

View File

@@ -3,7 +3,7 @@
There are basically three ways how to migrate from an already existing Nextcloud installation to Nextcloud AIO (if you ran AIO on the former installation already, you can follow [these steps](https://github.com/nextcloud/all-in-one#how-to-migrate-from-aio-to-aio)):
1. Migrate only the files which is the easiest way (this excludes all calendar data for example)
1. Migrate the files and the database which is much more complicated (and doesn't work on former snap installations)
1. Migrate the files and the database which is much more complicated (with special handling required for former snap installations, see [below](#migrating-from-a-snap-installation))
1. Use the user_migration app that allows to migrate some of the user's data from a former instance to a new instance but needs to be done manually for each user
## Migrate only the files
@@ -21,7 +21,7 @@ The procedure for migrating only the files works like this:
1. If the restored data is older than any clients you want to continue to sync, for example if the server was down for a period of time during migration, you may want to take a look at [Synchronising with clients after migration](/migration.md#synchronising-with-clients-after-migration) below.
## Migrate the files and the database
**Please note**: this is much more complicated than migrating only the files and also not as failproof so be warned! Also, this will not work on former snap installations as the snap is read-only and thus you cannot install the necessary `pdo_pgsql` PHP extension. So if migrating from snap, you will need to use one of the other methods. However you could try to ask if the snaps maintainer could add this one small PHP extension to the snap here: https://github.com/nextcloud-snap/nextcloud-snap/issues which would allow for an easy migration.
**Please note**: this is much more complicated than migrating only the files and also not as failproof so be warned! If you are migrating from a snap installation, please first follow the [dedicated snap migration steps](#migrating-from-a-snap-installation) below, which show you how to perform the database conversion using a temporary Docker container. Once done, you can continue from step 5 of this guide.
The procedure for migrating the files and the database works like this:
1. Make sure that your old instance is on exactly the same version like the version used in Nextcloud AIO. (e.g. 23.0.0) You can find the used version here: [click here](https://github.com/nextcloud/all-in-one/search?l=Dockerfile&q=NEXTCLOUD_VERSION&type=). If not, simply upgrade your former installation to that version or wait until the version used in Nextcloud AIO got updated to the same version of your former installation or the other way around.
@@ -87,6 +87,130 @@ If not, feel free to restore the AIO instance from backup and start at step 8 ag
If the restored data is older than any clients you want to continue to sync, for example if the server was down for a period of time during migration, you may want to take a look at [Synchronising with clients after migration](/migration.md#synchronising-with-clients-after-migration) below.
### Migrating from a snap installation
**Disclaimer:** it might be possible that the guide below is not working 100% correctly, yet. Improvements to it are very welcome!
Since the Nextcloud snap is read-only, it is not possible to install the `pdo_pgsql` PHP extension inside the snap to perform the MySQL-to-PostgreSQL database conversion required by AIO. As a workaround, a temporary [nextcloud/docker](https://github.com/nextcloud/docker) container can be used as an intermediate environment that already includes `pdo_pgsql` and can convert the snap's MySQL database to PostgreSQL for you.
This procedure covers steps 13 of the regular migration above (version matching, app updates, and database conversion) and also produces the `database-dump.sql` needed for step 4. Once finished, continue from step 5 of the [Migrate the files and the database](#migrate-the-files-and-the-database) procedure above.
1. **Create a backup of the snap before doing anything else**, so you can restore the snap to its current state if anything goes wrong:
```
sudo snap save nextcloud
```
This creates a snapshot that can be restored later with `sudo snap restore <snapshot-id>`. The snapshot ID is shown in the output of `snap save`. You can also list existing snapshots with `snap saved`.
1. Note the exact Nextcloud version of your snap installation:
```
sudo nextcloud.occ -V
```
1. Make sure that this version matches exactly the version used in Nextcloud AIO. You can find the AIO version here: [click here](https://github.com/nextcloud/all-in-one/search?l=Dockerfile&q=NEXTCLOUD_VERSION&type=). If they do not match, upgrade your snap with `sudo snap refresh nextcloud --channel=<major-version>/stable` or wait for AIO to be updated to the same version.
1. Update all installed Nextcloud apps to their latest versions:
```
sudo nextcloud.occ app:update --all
```
1. Retrieve the necessary configuration values from the snap using `nextcloud.occ` and store them in environment variables. Do this **before** stopping the snap, as `nextcloud.occ` requires the snap services to be running:
```
export INSTANCEID=$(sudo nextcloud.occ config:system:get instanceid)
export PASSWORDSALT=$(sudo nextcloud.occ config:system:get passwordsalt)
export SECRET=$(sudo nextcloud.occ config:system:get secret)
export TABLE_PREFIX=$(sudo nextcloud.occ config:system:get dbtableprefix || echo "oc_")
export SNAP_DATA=$(sudo nextcloud.occ config:system:get datadirectory)
# Note down SNAP_DATA — you will need it later when copying files
echo "Snap data directory: $SNAP_DATA"
```
1. Export a dump of the snap's MySQL database:
```
sudo nextcloud.mysqldump > ~/mysql-dump.sql
```
1. Stop the snap to prevent further writes during the migration:
```
sudo snap stop nextcloud
```
1. Set up environment variables for the temporary containers (adjust the version and passwords as needed):
```
export NEXTCLOUD_VERSION="29.0.0" # Replace with the exact version from step 2
export MYSQL_PASSWORD="mysql-temp-password"
export PG_USER="ncadmin"
export PG_PASSWORD="my-temporary-pg-password"
export PG_DATABASE="nextcloud_db"
```
1. Create a Docker network for the temporary migration containers:
```
docker network create nextcloud-migration
```
1. Start a temporary MySQL container and import the snap database dump into it:
```
docker run -d \
--name mysql-migration \
--network nextcloud-migration \
-e MYSQL_ROOT_PASSWORD="mysql-root-temp" \
-e MYSQL_DATABASE="nextcloud" \
-e MYSQL_USER="nextcloud" \
-e MYSQL_PASSWORD="$MYSQL_PASSWORD" \
mysql:8
# Wait for MySQL to finish starting up before importing
until docker exec mysql-migration mysqladmin ping -h localhost --silent 2>/dev/null; do sleep 1; done
docker exec -i mysql-migration mysql -u nextcloud -p"$MYSQL_PASSWORD" nextcloud < ~/mysql-dump.sql
```
1. Start a temporary PostgreSQL container as the migration target:
```
docker run -d \
--name postgres-migration \
--network nextcloud-migration \
-e POSTGRES_USER="$PG_USER" \
-e POSTGRES_PASSWORD="$PG_PASSWORD" \
-e POSTGRES_DB="$PG_DATABASE" \
postgres:16
```
1. Create a temporary config file for the migration container using the values retrieved in step 5:
```
cat > /tmp/migration-config.php << EOF
<?php
\$CONFIG = array(
'instanceid' => '$INSTANCEID',
'passwordsalt' => '$PASSWORDSALT',
'secret' => '$SECRET',
'dbtype' => 'mysql',
'dbname' => 'nextcloud',
'dbhost' => 'mysql-migration',
'dbport' => '',
'dbtableprefix' => '$TABLE_PREFIX',
'dbuser' => 'nextcloud',
'dbpassword' => '$MYSQL_PASSWORD',
'datadirectory' => '$SNAP_DATA',
'installed' => true,
);
EOF
```
1. Run a temporary nextcloud/docker container to convert the MySQL database to PostgreSQL. Note that the container image version must match the Nextcloud version you noted in step 2, and that `pdo_pgsql` is already included in the `nextcloud` Docker image:
```
docker run --rm \
--network nextcloud-migration \
--entrypoint bash \
-v /tmp/migration-config.php:/var/www/html/config/config.php:rw \
-v "${SNAP_DATA}:${SNAP_DATA}:ro" \
nextcloud:${NEXTCLOUD_VERSION}-apache \
-c "php /var/www/html/occ db:convert-type --all-apps --password '$PG_PASSWORD' pgsql '$PG_USER' postgres-migration '$PG_DATABASE'"
```
**Please note:** The `occ` command may print a warning about being run as root — this can be safely ignored for this migration step.
1. Export the converted PostgreSQL database:
```
docker exec postgres-migration pg_dump -U "$PG_USER" "$PG_DATABASE" > ~/database-dump.sql
```
**Please note:** The exact name of the database export file is important! (`database-dump.sql`)
1. Clean up the temporary containers and network:
```
docker rm -f mysql-migration postgres-migration
docker network rm nextcloud-migration
```
1. You now have a `~/database-dump.sql`. Continue from step 5 of the [Migrate the files and the database](#migrate-the-files-and-the-database) procedure above. When those steps ask for your old data directory path, use the `$SNAP_DATA` value noted in step 5 (typically `/var/snap/nextcloud/common`). If you have opened a new shell session since then, you can retrieve it again with `sudo nextcloud.occ config:system:get datadirectory` (requires the snap to be running) or read it directly from `/var/snap/nextcloud/current/nextcloud/config/config.php`.
1. Once you have verified that the migration to AIO was successful and everything is working correctly, you can permanently remove the Nextcloud snap from your system:
```
sudo snap remove --purge nextcloud
```
The `--purge` flag removes the snap along with all its saved snapshots and data. Omit it if you want to keep the snap snapshots as a fallback. **Only do this after you are fully satisfied that your AIO instance is working correctly**, as this action cannot be undone.
## Use the user_migration app
A new way since the Nextcloud update to 24 is to use the new [user_migration app](https://apps.nextcloud.com/apps/user_migration#app-gallery). It allows to export the most important data on one instance and import it on a different Nextcloud instance. For that, you need to install and enable the user_migration app on your old instance, trigger the export for the user, create the user on the new instance, log in with that user and import the archive that was created during the export. This then needs to be done for each user that you want to migrate.

View File

@@ -11,6 +11,7 @@
"nextcloud-aio-notify-push",
"nextcloud-aio-whiteboard",
"nextcloud-aio-harp",
"nextcloud-aio-windmill",
"nextcloud-aio-nextcloud"
],
"display_name": "Apache & Caddy",
@@ -51,7 +52,8 @@
"APACHE_MAX_TIME=%NEXTCLOUD_MAX_TIME%",
"NOTIFY_PUSH_HOST=nextcloud-aio-notify-push",
"WHITEBOARD_HOST=nextcloud-aio-whiteboard",
"HARP_HOST=nextcloud-aio-harp"
"HARP_HOST=nextcloud-aio-harp",
"WINDMILL_HOST=nextcloud-aio-windmill"
],
"volumes": [
{
@@ -148,7 +150,8 @@
"nextcloud-aio-fulltextsearch",
"nextcloud-aio-talk-recording",
"nextcloud-aio-imaginary",
"nextcloud-aio-docker-socket-proxy"
"nextcloud-aio-docker-socket-proxy",
"nextcloud-aio-windmill"
],
"display_name": "Nextcloud",
"image": "ghcr.io/nextcloud-releases/aio-nextcloud",
@@ -175,7 +178,8 @@
"FULLTEXTSEARCH_PASSWORD",
"IMAGINARY_SECRET",
"WHITEBOARD_SECRET",
"HP_SHARED_KEY"
"HP_SHARED_KEY",
"WINDMILL_SECRET"
],
"volumes": [
{
@@ -263,7 +267,10 @@
"WHITEBOARD_SECRET=%WHITEBOARD_SECRET%",
"WHITEBOARD_ENABLED=%WHITEBOARD_ENABLED%",
"HARP_ENABLED=%HARP_ENABLED%",
"HP_SHARED_KEY=%HP_SHARED_KEY%"
"HP_SHARED_KEY=%HP_SHARED_KEY%",
"WINDMILL_ENABLED=%WINDMILL_ENABLED%",
"WINDMILL_HOST=nextcloud-aio-windmill",
"WINDMILL_SECRET=%WINDMILL_SECRET%"
],
"stop_grace_period": 600,
"restart": "unless-stopped",
@@ -949,6 +956,74 @@
"cap_drop": [
"NET_RAW"
]
},
{
"container_name": "nextcloud-aio-windmill",
"image_tag": "%AIO_CHANNEL%",
"display_name": "Windmill",
"image": "ghcr.io/nextcloud-releases/aio-windmill",
"user": "1000",
"init": true,
"internal_port": "8000",
"expose": [
"8000"
],
"healthcheck": {
"start_period": "0s",
"test": "/healthcheck.sh",
"interval": "30s",
"timeout": "30s",
"start_interval": "5s",
"retries": 3
},
"environment": [
"BASE_URL=https://%NC_DOMAIN%/windmill",
"TZ=%TIMEZONE%",
"NUM_WORKERS=1",
"MODE=standalone",
"DISABLE_NSJAIL=true",
"DATABASE_URL=postgresql://windmill@localhost/windmill?host=/var/run/postgresql",
"SUPERADMIN_SECRET=%WINDMILL_SECRET%"
],
"secrets": [
"WINDMILL_SECRET"
],
"volumes": [
{
"source": "nextcloud_aio_windmill_db",
"destination": "/var/lib/postgresql/data",
"writeable": true
},
{
"source": "nextcloud_aio_windmill_dump",
"destination": "/var/lib/windmill-dump",
"writeable": true
},
{
"source": "nextcloud_aio_windmill_cache",
"destination": "/tmp/windmill/cache",
"writeable": true
}
],
"backup_volumes": [
"nextcloud_aio_windmill_db",
"nextcloud_aio_windmill_dump"
],
"restart": "unless-stopped",
"stop_grace_period": 1800,
"read_only": true,
"tmpfs": [
"/tmp",
"/var/run/postgresql",
"/var/log/supervisord",
"/var/run/supervisord"
],
"profiles": [
"windmill"
],
"cap_drop": [
"NET_RAW"
]
}
]
}

View File

@@ -41,4 +41,10 @@ document.addEventListener("DOMContentLoaded", function(event) {
// Whiteboard
let whiteboard = document.getElementById("whiteboard");
whiteboard.disabled = true;
// Windmill
let windmill = document.getElementById("windmill");
if (windmill) {
windmill.disabled = true;
}
});

View File

@@ -109,7 +109,6 @@ $app->post('/api/auth/login', AIO\Controller\LoginController::class . ':TryLogin
$app->get('/api/auth/getlogin', AIO\Controller\LoginController::class . ':GetTryLogin');
$app->post('/api/auth/logout', AIO\Controller\LoginController::class . ':Logout');
$app->post('/api/configuration', \AIO\Controller\ConfigurationController::class . ':SetConfig');
$app->post('/api/desec/register', \AIO\Controller\DesecController::class . ':Register');
// Views
$app->get('/containers', function (Request $request, Response $response, array $args) use ($container) {
@@ -178,13 +177,10 @@ $app->get('/containers', function (Request $request, Response $response, array $
'is_docker_socket_proxy_enabled' => $configurationManager->isDockerSocketProxyEnabled,
'is_harp_enabled' => $configurationManager->isHarpEnabled,
'is_whiteboard_enabled' => $configurationManager->isWhiteboardEnabled,
'is_windmill_enabled' => $configurationManager->isWindmillEnabled,
'community_containers' => $configurationManager->listAvailableCommunityContainers(),
'community_containers_enabled' => $configurationManager->aioCommunityContainers,
'bypass_container_update' => $bypass_container_update,
'desec_email' => $configurationManager->desecEmail,
'desec_password' => $configurationManager->desecPassword,
'is_desec_domain' => $configurationManager->isDesecDomain(),
'desec_account_registered' => $configurationManager->isDesecAccountRegistered(),
]);
})->setName('profile');
$app->get('/login', function (Request $request, Response $response, array $args) use ($container) {

View File

@@ -109,6 +109,10 @@ readonly class ContainerDefinitionFetcher {
if (!$this->configurationManager->isWhiteboardEnabled) {
continue;
}
} elseif ($entry['container_name'] === 'nextcloud-aio-windmill') {
if (!$this->configurationManager->isWindmillEnabled) {
continue;
}
}
$ports = new ContainerPorts();
@@ -222,6 +226,10 @@ readonly class ContainerDefinitionFetcher {
if (!$this->configurationManager->isWhiteboardEnabled) {
continue;
}
} elseif ($value === 'nextcloud-aio-windmill') {
if (!$this->configurationManager->isWindmillEnabled) {
continue;
}
} else {
// Skip dependencies on community containers that are not currently enabled.
// Only apply this when the current entry is itself a community container,

View File

@@ -98,6 +98,7 @@ readonly class ConfigurationController {
$this->configurationManager->isDockerSocketProxyEnabled = isset($request->getParsedBody()['docker-socket-proxy']);
$this->configurationManager->isHarpEnabled = isset($request->getParsedBody()['harp']);
$this->configurationManager->isWhiteboardEnabled = isset($request->getParsedBody()['whiteboard']);
$this->configurationManager->isWindmillEnabled = isset($request->getParsedBody()['windmill']);
}
if (isset($request->getParsedBody()['community-form'])) {

View File

@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace AIO\Controller;
use AIO\Desec\DesecManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
readonly class DesecController {
public function __construct(
private DesecManager $desecManager,
) {
}
public function Register(Request $request, Response $response, array $args): Response {
try {
$email = (string)($request->getParsedBody()['desec_email'] ?? '');
$slug = (string)($request->getParsedBody()['desec_slug'] ?? '');
$password = (string)($request->getParsedBody()['desec_password'] ?? '');
$this->desecManager->register($email, $slug, $password);
return $response->withStatus(201)->withHeader('Location', '.');
} catch (\Exception $ex) {
$response->getBody()->write($ex->getMessage());
return $response->withStatus(422);
}
}
}

View File

@@ -6,7 +6,6 @@ namespace AIO\Controller;
use AIO\Container\Container;
use AIO\Container\ContainerState;
use AIO\ContainerDefinitionFetcher;
use AIO\Desec\DesecManager;
use AIO\Docker\DockerActionManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
@@ -19,8 +18,7 @@ readonly class DockerController {
public function __construct(
private DockerActionManager $dockerActionManager,
private ContainerDefinitionFetcher $containerDefinitionFetcher,
private ConfigurationManager $configurationManager,
private DesecManager $desecManager,
private ConfigurationManager $configurationManager
) {
}
@@ -265,9 +263,6 @@ readonly class DockerController {
// Stop domaincheck since apache would not be able to start otherwise
$this->StopDomaincheckContainer();
// Refresh the deSEC DNS record with the current public IP before starting containers
$this->desecManager->updateIpIfDesecDomain();
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStart($id, $pullImage, $addToStreamingResponseBody);

View File

@@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
// increase memory limit to 2GB
ini_set('memory_limit', '2048M');
// Log whole log messages
ini_set('log_errors_max_len', '0');
require __DIR__ . '/../../vendor/autoload.php';
$container = \AIO\DependencyInjection::GetContainer();
/** @var \AIO\Desec\DesecManager $desecManager */
$desecManager = $container->get(\AIO\Desec\DesecManager::class);
$desecManager->updateIpIfDesecDomain();

View File

@@ -8,8 +8,6 @@ use AIO\Controller\DockerController;
class ConfigurationManager
{
public const string DEDYN_SUFFIX = '.dedyn.io';
private array $secrets = [];
private array $config = [];
@@ -47,6 +45,12 @@ class ConfigurationManager
set { $this->set('isWhiteboardEnabled', $value); }
}
public bool $isWindmillEnabled {
// Type-cast because old configs could have 1/0 for this key.
get => (bool) $this->get('isWindmillEnabled', false);
set { $this->set('isWindmillEnabled', $value); }
}
public bool $restoreExcludePreviews {
// Type-cast because old configs could have '1'/'' for this key.
get => (bool) $this->get('restore-exclude-previews', false);
@@ -200,43 +204,6 @@ class ConfigurationManager
set { $this->set('turn_domain', $value); }
}
public string $desecEmail {
get => $this->get('desec_email', '');
set { $this->set('desec_email', $value); }
}
public string $desecToken {
get {
$s = $this->get('secrets', []);
return isset($s['DESEC_TOKEN']) && is_string($s['DESEC_TOKEN']) ? $s['DESEC_TOKEN'] : '';
}
set {
$s = $this->get('secrets', []);
$s['DESEC_TOKEN'] = $value;
$this->set('secrets', $s);
}
}
public string $desecPassword {
get {
$s = $this->get('secrets', []);
return isset($s['DESEC_PASSWORD']) && is_string($s['DESEC_PASSWORD']) ? $s['DESEC_PASSWORD'] : '';
}
set {
$s = $this->get('secrets', []);
$s['DESEC_PASSWORD'] = $value;
$this->set('secrets', $s);
}
}
public function isDesecDomain(): bool {
return str_ends_with($this->domain, self::DEDYN_SUFFIX) && $this->desecToken !== '';
}
public function isDesecAccountRegistered(): bool {
return $this->desecToken !== '' && $this->desecEmail !== '' && $this->domain === '';
}
public string $apachePort {
get => $this->getEnvironmentalVariableOrConfig('APACHE_PORT', 'apache_port', '443');
set { $this->set('apache_port', $value); }
@@ -1147,8 +1114,8 @@ class ConfigurationManager
// Allow to get local ip-address of caddy container and add it to trusted proxies automatically
'CADDY_IP_ADDRESS' => in_array('caddy', $this->aioCommunityContainers, true) ? gethostbyname('nextcloud-aio-caddy') : '',
'WHITEBOARD_ENABLED' => $this->isWhiteboardEnabled ? 'yes' : '',
'WINDMILL_ENABLED' => $this->isWindmillEnabled ? 'yes' : '',
'AIO_VERSION' => $this->getAioVersion(),
'DESEC_TOKEN' => $this->desecToken,
default => $this->getRegisteredSecret($placeholder),
};
}

View File

@@ -35,12 +35,6 @@ class DependencyInjection
$container->get(GitHubContainerRegistryManager::class)
)
);
$container->set(
\AIO\Desec\DesecManager::class,
new \AIO\Desec\DesecManager(
$container->get(\AIO\Data\ConfigurationManager::class),
)
);
$container->set(
\AIO\Auth\PasswordGenerator::class,
new \AIO\Auth\PasswordGenerator()

View File

@@ -1,310 +0,0 @@
<?php
declare(strict_types=1);
namespace AIO\Desec;
use AIO\Data\ConfigurationManager;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
class DesecManager {
private const string DESEC_API_BASE = 'https://desec.io/api/v1';
private const int MAX_SLUG_ATTEMPTS = 5;
private const int SLUG_BYTES = 5; // bin2hex → 10-char slug
private const string SLUG_PATTERN = '/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/';
private Client $guzzleClient;
public function __construct(
private readonly ConfigurationManager $configurationManager,
) {
$this->guzzleClient = new Client([
'timeout' => 15,
'connect_timeout' => 10,
'http_errors' => false,
]);
}
/**
* Full registration flow: validates inputs, creates an account if needed,
* registers the domain, enables required containers, and updates the DNS record.
*
* When $password is non-empty the user is logging into an existing deSEC account
* rather than creating a new one. When $password is empty a new account is created
* with a randomly generated password (unless an account was already registered in a
* previous attempt).
*
* @throws \Exception on any validation or API error
*/
public function register(string $email, string $slug, string $password = ''): void {
if ($this->configurationManager->domain !== '') {
throw new \Exception('A domain is already configured. Reset the AIO instance first to register a new domain.');
}
$accountAlreadyRegistered = $this->configurationManager->isDesecAccountRegistered();
$token = $accountAlreadyRegistered
? $this->configurationManager->desecToken
: null;
$validatedSlug = $this->validateSlug($slug);
if (!$accountAlreadyRegistered) {
$validatedEmail = $this->validateEmail($email);
$validatedPassword = trim($password);
if ($validatedPassword !== '') {
// The user supplied their existing deSEC password — log in instead of registering.
// Store an empty password: the token is all we need; the user's password must not be persisted.
$token = $this->loginAccount($validatedEmail, $validatedPassword);
$this->saveAccountCredentials($token, '', $validatedEmail);
} else {
// 24 random bytes → 48-char hex password; satisfies deSEC's minimum length
// and lets the user log in at desec.io if they ever need to.
$generatedPassword = bin2hex(random_bytes(24));
$token = $this->registerAccount($validatedEmail, $generatedPassword);
$this->saveAccountCredentials($token, $generatedPassword, $validatedEmail);
}
}
$isNewAccount = !$accountAlreadyRegistered && trim($password) === '';
$domain = $this->registerDomain($token, $validatedSlug);
if ($isNewAccount) {
$this->createWildcardCname($token, $domain);
}
$this->enableDesecContainers();
$this->configurationManager->setDomain($domain, true);
$this->updateIpIfDesecDomain();
}
/**
* Validates an email address string.
*
* @throws \Exception if the email is empty or syntactically invalid
*/
private function validateEmail(string $email): string {
$email = trim($email);
if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
throw new \Exception('Please provide a valid email address.');
}
return $email;
}
/**
* Validates an optional subdomain slug.
* Returns an empty string when the caller wants a randomly generated slug.
*
* @throws \Exception if the slug is non-empty but does not match the allowed pattern
*/
private function validateSlug(string $slug): string {
$slug = trim($slug);
if ($slug !== '' && !preg_match(self::SLUG_PATTERN, $slug)) {
throw new \Exception(
'The desired subdomain must contain only lowercase letters, digits and hyphens, '
. 'be between 1 and 63 characters long, and must not start or end with a hyphen.'
);
}
return $slug;
}
/**
* Creates a new deSEC account and returns the API token issued for it.
*
* @throws \Exception on network failure or an unexpected HTTP response
*/
public function registerAccount(string $email, string $password): string {
try {
$res = $this->guzzleClient->post(self::DESEC_API_BASE . '/auth/', [
'json' => ['email' => $email, 'password' => $password],
]);
} catch (TransferException $e) {
throw new \Exception('Could not reach the deSEC API: ' . $e->getMessage());
}
$code = $res->getStatusCode();
$body = $res->getBody()->getContents();
if ($code === 400) {
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (is_array($data) && isset($data['email'])) {
throw new \Exception(
'This email address is already registered at deSEC. '
. 'If this is your account, please enter your deSEC password in the password field and try again.',
);
}
throw new \Exception('Registration at deSEC failed (HTTP 400): ' . $body);
}
if ($code !== 201) {
throw new \Exception('Unexpected response from deSEC during account registration (HTTP ' . $code . '): ' . $body);
}
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($data) || !isset($data['token']['token']) || !is_string($data['token']['token'])) {
throw new \Exception('Could not extract the API token from the deSEC response. Please try again.');
}
return $data['token']['token'];
}
/**
* Authenticates with an existing deSEC account and returns the API token issued for it.
*
* @throws \Exception on invalid credentials, network failure, or an unexpected HTTP response
*/
public function loginAccount(string $email, string $password): string {
try {
$res = $this->guzzleClient->post(self::DESEC_API_BASE . '/auth/login/', [
'json' => ['email' => $email, 'password' => $password],
]);
} catch (TransferException $e) {
throw new \Exception('Could not reach the deSEC API: ' . $e->getMessage());
}
$code = $res->getStatusCode();
$body = $res->getBody()->getContents();
if ($code === 400 || $code === 403) {
throw new \Exception('Could not log in to deSEC: invalid email address or password.');
}
if ($code !== 200 && $code !== 201) {
throw new \Exception('Unexpected response from deSEC during login (HTTP ' . $code . '): ' . $body);
}
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($data) || !isset($data['token']) || !is_string($data['token'])) {
throw new \Exception('Could not extract the API token from the deSEC login response. Please try again.');
}
return $data['token'];
}
/**
* Registers a dedyn.io domain for the authenticated account.
* When $slug is empty a random 10-character slug is tried up to MAX_SLUG_ATTEMPTS times.
*
* @return string the fully-qualified domain name that was registered
* @throws \Exception if the slug is taken, on network failure, or after exhausting random attempts
*/
public function registerDomain(string $token, string $slug): string {
$random = $slug === '';
$attempts = $random ? self::MAX_SLUG_ATTEMPTS : 1;
for ($i = 0; $i < $attempts; $i++) {
$domain = ($random ? bin2hex(random_bytes(self::SLUG_BYTES)) : $slug) . ConfigurationManager::DEDYN_SUFFIX;
try {
$res = $this->guzzleClient->post(self::DESEC_API_BASE . '/domains/', [
'headers' => ['Authorization' => 'Token ' . $token],
'json' => ['name' => $domain],
]);
} catch (TransferException $e) {
throw new \Exception('Could not reach the deSEC API: ' . $e->getMessage());
}
$code = $res->getStatusCode();
if ($code === 201) {
return $domain;
}
if ($code === 409) {
if (!$random) {
throw new \Exception('"' . $domain . '" is already taken. Please choose a different subdomain and try again.');
}
continue;
}
throw new \Exception('Unexpected response from deSEC during domain registration (HTTP ' . $code . '): ' . $res->getBody()->getContents());
}
throw new \Exception('Could not register a free dedyn.io domain after ' . self::MAX_SLUG_ATTEMPTS . ' attempts. Please try again.');
}
/**
* Creates a wildcard CNAME rrset (*.domain → domain.) for a newly registered domain.
* Errors are logged but do not abort the overall registration.
*/
private function createWildcardCname(string $token, string $domain): void {
try {
$res = $this->guzzleClient->post(self::DESEC_API_BASE . '/domains/' . $domain . '/rrsets/', [
'headers' => ['Authorization' => 'Token ' . $token],
'json' => [
'subname' => '*',
'type' => 'CNAME',
'ttl' => 3600,
'records' => [$domain . '.'],
],
]);
} catch (TransferException $e) {
error_log('Could not create wildcard CNAME for ' . $domain . ': ' . $e->getMessage());
return;
}
$code = $res->getStatusCode();
if ($code !== 201) {
error_log('Unexpected response when creating wildcard CNAME for ' . $domain . ' (HTTP ' . $code . '): ' . $res->getBody()->getContents());
}
}
/**
* Persists deSEC account credentials to the AIO configuration atomically.
*/
public function saveAccountCredentials(string $token, string $password, string $email): void {
$this->configurationManager->startTransaction();
$this->configurationManager->desecToken = $token;
$this->configurationManager->desecPassword = $password;
$this->configurationManager->desecEmail = $email;
$this->configurationManager->commitTransaction();
}
/**
* Ensures the caddy and dnsmasq community containers are enabled.
*/
public function enableDesecContainers(): void {
$this->configurationManager->startTransaction();
$enabled = array_values(array_filter(
$this->configurationManager->aioCommunityContainers,
fn(string $cc): bool => $cc !== '',
));
if (!in_array('caddy', $enabled, true)) {
$enabled[] = 'caddy';
}
if (!in_array('dnsmasq', $enabled, true)) {
$enabled[] = 'dnsmasq';
}
$this->configurationManager->aioCommunityContainers = $enabled;
$this->configurationManager->commitTransaction();
}
/**
* Updates the deSEC dynamic-DNS record with the current public IP.
* Does nothing when the configured domain is not a deSEC-managed dedyn.io domain.
*/
public function updateIpIfDesecDomain(): void {
if (!$this->configurationManager->isDesecDomain()) {
return;
}
$domain = $this->configurationManager->domain;
$token = $this->configurationManager->desecToken;
try {
$res = $this->guzzleClient->get('https://update.dedyn.io/', [
'query' => ['hostname' => $domain],
'headers' => ['Authorization' => 'Token ' . $token],
]);
$status = trim($res->getBody()->getContents());
if (str_starts_with($status, 'good') || str_starts_with($status, 'nochg')) {
error_log('deSEC IP update for ' . $domain . ': ' . $status);
} else {
error_log('deSEC IP update for ' . $domain . ' returned unexpected response: ' . $status);
}
} catch (\Exception $e) {
error_log('Could not update deSEC DNS record for ' . $domain . ': ' . $e->getMessage());
}
}
}

View File

@@ -867,15 +867,7 @@ readonly class DockerActionManager {
// 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.
if ($container->identifier === 'nextcloud-aio-domaincheck') {
$alias = 'nextcloud-aio-apache';
}
// Add NC_DOMAIN as a Docker network alias so that intra-network traffic for the Nextcloud
// domain is forwarded directly to the aio-caddy container without leaving the Docker network.
if ($container->identifier === 'nextcloud-aio-caddy') {
$alias = $this->configurationManager->domain;
}
$alias = ($container->identifier === 'nextcloud-aio-domaincheck') ? 'nextcloud-aio-apache' : '';
$this->ConnectContainerIdToNetwork($container->identifier, $container->internalPorts, alias: $alias);

View File

@@ -123,7 +123,7 @@
<p>Make sure that this server is reachable on port 443 (port 443/tcp is open/forwarded in your firewall/router and 443/udp as well if you want to enable http3) and that you've correctly set up the DNS config for the domain that you enter (set the A record to your public ipv4-address and if you need ipv6, set the AAAA record to your public ipv6-address. A CNAME record is, of course, also possible). You should see hints on what went wrong in the top right corner if your domain is not accepted.</p>
<details>
<summary>Click here for further hints</summary>
<p>If you do not have a domain yet, the easiest option is to use the <strong>deSEC free domain</strong> registration below. You can also get a free domain from duckdns.org or others, or use <a target="_blank" href="https://github.com/nextcloud/all-in-one/discussions/6817">Tailscale</a>.</p>
<p>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 <a target="_blank" href="https://github.com/nextcloud/all-in-one/discussions/6817">Tailscale</a></p>
<p>If you have a dynamic public IP-address, you can use e.g. <a target="_blank" href="https://ddclient.net/">DDclient</a> with a compatible domain provider for DNS updates.</p>
<p>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 <a target="_blank" href="https://github.com/nextcloud/all-in-one/blob/main/local-instance.md">this documentation</a>.</p>
<p>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.</p>
@@ -132,7 +132,6 @@
{% endif %}
<p><strong>Hint:</strong> If the domain validation fails but you are completely sure that you've configured everything correctly, you may skip the domain validation by following <a target="_blank" href="https://github.com/nextcloud/all-in-one#how-to-skip-the-domain-validation">this documentation</a>.</p>
</details>
{% include 'includes/desec-register.twig' %}
{% endif %}
<h2>Restore former AIO instance from backup</h2>
@@ -376,14 +375,6 @@
{% if was_start_button_clicked == true %}
{% if is_desec_domain %}
<h2>deSEC account credentials</h2>
<p>Your domain <strong>{{ domain }}</strong> is managed via <a target="_blank" href="https://desec.io">deSEC</a>. Below are your deSEC account credentials. You can use them to log in at <a target="_blank" href="https://desec.io">desec.io</a> to manage your domain directly.</p>
<p>Email: <strong>{{ desec_email }}</strong></p>
<p>Password: <details style="display:inline"><summary>Reveal deSEC password</summary><strong>{{ desec_password }}</strong></details></p>
<p>Please save these credentials in a safe place.</p>
{% endif %}
{% if is_backup_section_enabled == false %}
<h2>Backup and restore</h2>
<p>The backup section is disabled via environmental variable.</p>

View File

@@ -1,9 +1,6 @@
<h2>Community Containers</h2>
<p>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.</p>
<p><strong>⚠️ Caution: </strong>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!</p>
{% if is_desec_domain == true %}
<p> Your Nextcloud domain (<strong>{{ domain }}</strong>) was registered via deSEC. The <strong>caddy</strong> community container has been automatically enabled as a reverse proxy and the <strong>dnsmasq</strong> container has been automatically enabled so that LAN devices can resolve your Nextcloud domain to the server's local IP address. Please <a target="_blank" href="https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq"><strong>read the dnsmasq documentation</strong></a> for the required router change.</p>
{% endif %}
{% if isAnyRunning == true %}
<p><strong>Please note:</strong> You can enable or disable the options below only when your containers are stopped.</p>
{% else %}

View File

@@ -1,26 +0,0 @@
<details{% if desec_account_registered %} open{% endif %}>
<summary>Don't have a domain? Get a free one from deSEC</summary>
<p><a target="_blank" href="https://desec.io">deSEC</a> offers free dynamic DNS subdomains under <strong>dedyn.io</strong>. AIO can register an account and a subdomain for you automatically. The <strong>caddy</strong> community container will be enabled as a reverse proxy, the <strong>dnsmasq</strong> container will be enabled for local DNS resolution, and the mastercontainer will keep your DNS record up to date automatically.</p>
{% if desec_account_registered %}
<p>Your deSEC account (<strong>{{ desec_email }}</strong>) was registered successfully but the domain could not be registered. Please enter a desired subdomain slug (the part before <code>.dedyn.io</code>) and try again, or leave it blank for a random one.</p>
<p>Your deSEC login credentials (for <a target="_blank" href="https://desec.io">desec.io</a>): Email: <strong>{{ desec_email }}</strong>. <details style="display:inline"><summary>Reveal deSEC password</summary><strong>{{ desec_password }}</strong></details>. Please save these in a safe place.</p>
<form method="POST" action="api/desec/register" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input type="text" name="desec_slug" placeholder="my-nextcloud (optional)" pattern="[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?" title="Only lowercase letters, digits and hyphens (163 characters). No leading or trailing hyphen." />
<input type="submit" value="Register free domain via deSEC" />
</form>
{% else %}
<p>Please enter your email address. You can also enter a desired subdomain slug (the part before <code>.dedyn.io</code>); leave it blank for a random one.</p>
<p>If you already have a deSEC account for this email address, enter your deSEC password in the optional password field below to log in with it instead of creating a new account.</p>
<form method="POST" action="api/desec/register" class="xhr">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input type="email" name="desec_email" placeholder="your@email.com" required />
<input type="password" name="desec_password" placeholder="deSEC password (only if already registered)" autocomplete="current-password" />
<input type="text" name="desec_slug" placeholder="my-nextcloud (optional)" pattern="[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?" title="Only lowercase letters, digits and hyphens (163 characters). No leading or trailing hyphen." />
<input type="submit" value="Register free domain via deSEC" />
</form>
<p><strong>Note:</strong> By submitting this form you agree to the <a target="_blank" href="https://desec.io/terms">deSEC terms of service</a>. 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 <a target="_blank" href="https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq">dnsmasq documentation</a>). Alternatively adjust the hosts files on your clients so that they can reach the server using the local ip-address.</p>
{% endif %}
</details>

View File

@@ -220,6 +220,20 @@
>
<label for="whiteboard">Whiteboard</label>
</p>
<p>
<input
type="checkbox"
id="windmill"
name="windmill"
{% if is_windmill_enabled == true %}
checked="checked"
data-initial-state="true"
{% else %}
data-initial-state="false"
{% endif %}
>
<label for="windmill">Windmill (workflow automation. Access the Windmill UI at <code>https://{{ domain }}/windmill</code>)</label>
</p>
<input class="options-form-submit" type="submit" value="Save changes" />
</form>
<p><strong>Minimal system requirements:</strong> When any optional container is enabled, at least 2GB RAM, a dual-core CPU and 40GB system storage are required. When enabling ClamAV, Nextcloud Talk Recording-server or Fulltextsearch, at least 3GB RAM are required. For Talk Recording-server additional 2 vCPUs are required. When enabling everything, at least 5GB RAM and a quad-core CPU are required. Recommended are at least 1GB more RAM than the minimal requirement. For further advice and recommendations see <strong><a target="_blank" href="https://github.com/nextcloud/all-in-one/discussions/1335">this documentation</a></strong></p>

View File

@@ -61,7 +61,6 @@ Included are:
- [Mail server can be added](https://github.com/nextcloud/all-in-one#mail-server)
- Nextcloud can be [accessed locally via the domain](https://github.com/nextcloud/all-in-one#how-can-i-access-nextcloud-locally)
- Can [be installed locally](https://github.com/nextcloud/all-in-one/blob/main/local-instance.md) (if you don't want or cannot make the instance publicly reachable)
- Free [deSEC](https://desec.io) dynamic-DNS domain (`*.dedyn.io`) can be registered directly from the AIO interface — no external domain needed
- [IPv6-ready](https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md)
- Can be used with [Docker rootless](https://github.com/nextcloud/all-in-one/blob/main/docker-rootless.md) (good for additional security)
- Runs on all platforms Docker supports (e.g. also on Windows and Macos)
@@ -249,9 +248,6 @@ https://your-domain-that-points-to-this-server.tld:8443
5. If you enable Nextcloud Talk, open port `3478/TCP` and `3478/UDP` in your firewall/router for the Talk (TURN) container.
> [!TIP]
> Don't have a domain yet? AIO can register a free dynamic-DNS subdomain under `dedyn.io` for you via [deSEC](https://desec.io) — no external setup needed. Look for the **"Don't have a domain? Get a free one from deSEC"** section on the AIO interface when you are asked to enter your domain. AIO will automatically register the domain, keep the DNS record up to date, and enable the [Caddy](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy) community container as a reverse proxy as well as the [dnsmasq](https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq) container for local DNS resolution.
# FAQ
- [TOC](#faq)
- [Where can I find additional documentation?](#where-can-i-find-additional-documentation)
@@ -265,7 +261,6 @@ https://your-domain-that-points-to-this-server.tld:8443
- [Notes on Cloudflare (proxy/tunnel)](#notes-on-cloudflare-proxytunnel)
- [How to run Nextcloud behind a Cloudflare Tunnel?](#how-to-run-nextcloud-behind-a-cloudflare-tunnel)
- [How to run Nextcloud via Tailscale?](#how-to-run-nextcloud-via-tailscale)
- [How to get a free domain via deSEC?](#how-to-get-a-free-domain-via-desec)
- [How to get Nextcloud running using the ACME DNS-challenge?](#how-to-get-nextcloud-running-using-the-acme-dns-challenge)
- [How to run Nextcloud locally? No domain wanted, or wanting intranet access within your LAN.](#how-to-run-nextcloud-locally-no-domain-wanted-or-wanting-intranet-access-within-your-lan)
- [Can I use an ip-address for Nextcloud instead of a domain?](#can-i-use-an-ip-address-for-nextcloud-instead-of-a-domain)
@@ -395,7 +390,7 @@ Only those (if you access the Mastercontainer Interface internally via port 8080
- `3478/TCP` and `3478/UDP`: will be used by the Turnserver inside the Talk container and needs to be open/forwarded in your firewall/router
### Notes on Cloudflare (proxy/tunnel)
Since Cloudflare Proxy/Tunnel comes with a lot of limitations which are listed below, it is rather recommended to use the [built-in deSEC domain registration](#how-to-get-a-free-domain-via-desec) or switch to [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817) if possible.
Since Cloudflare Proxy/Tunnel comes with a lot of limitations which are listed below, it is rather recommended to switch to [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817) if possible.
- Cloudflare Proxy and Cloudflare Tunnel both require Cloudflare to perform TLS termination on their side and thus decrypt all the traffic on their infrastructure. This is a privacy concern and you will need to look for other solutions if it's unacceptable for you.
- Using Cloudflare Tunnel might potentially slow down Nextcloud since local access via the configured domain is not possible because TLS termination is in that case offloaded to Cloudflare's infrastructure. There is no way to disable this behavior in Cloudflare Tunnel.
- It is known that the domain validation may not work correctly behind Cloudflare since Cloudflare might block the validation attempt. You can simply skip it in that case by following: https://github.com/nextcloud/all-in-one#how-to-skip-the-domain-validation
@@ -415,19 +410,6 @@ Although it does not seems like it is the case but from AIO perspective a Cloudf
### How to run Nextcloud via Tailscale?
For a reverse proxy example guide for Tailscale, see this guide by [@Perseus333](https://github.com/Perseus333): https://github.com/nextcloud/all-in-one/discussions/6817
### How to get a free domain via deSEC?
[deSEC](https://desec.io) offers free dynamic-DNS subdomains under `dedyn.io`. AIO integrates the registration process directly:
1. Open the AIO interface and expand the **"Don't have a domain? Get a free one from deSEC"** section on the domain-entry page.
2. Enter your email address and, optionally, a desired subdomain slug (the part before `.dedyn.io`). Leave the slug blank for a random one.
3. Click **Register free domain via deSEC**. AIO will create a deSEC account, register the subdomain and store the credentials in its configuration.
After successful registration:
- AIO sets the registered `*.dedyn.io` domain as the Nextcloud domain automatically.
- The [Caddy](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy) community container is enabled as a reverse proxy.
- The [dnsmasq](https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq) community container is enabled so that LAN devices can resolve the domain to the server's local IP. Follow the [dnsmasq documentation](https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq) for the required router change.
- The mastercontainer keeps the DNS record up to date automatically when your public IP changes.
### How to get Nextcloud running using the ACME DNS-challenge?
You can install AIO behind an external reverse proxy where is also documented how to get it running using the ACME DNS-challenge for getting a valid certificate for AIO. See the [reverse proxy documentation](./reverse-proxy.md). (Meant is the `Caddy with ACME DNS-challenge` section). Also see https://github.com/dani-garcia/vaultwarden/wiki/Running-a-private-vaultwarden-instance-with-Let%27s-Encrypt-certs#getting-a-custom-caddy-build for additional docs on this topic.
@@ -435,22 +417,22 @@ You can install AIO behind an external reverse proxy where is also documented ho
If you do not want to open Nextcloud to the public internet, you may have a look at the following documentation on how to set it up locally: [local-instance.md](./local-instance.md), but keep in mind you're still required to have https working properly.
### Can I use an ip-address for Nextcloud instead of a domain?
No and it will not be added. If you only want to run it locally, you may have a look at the following documentation: [local-instance.md](./local-instance.md). Recommended is to use the [built-in deSEC domain registration](#how-to-get-a-free-domain-via-desec) to get a free domain automatically, or alternatively [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817).
No and it will not be added. If you only want to run it locally, you may have a look at the following documentation: [local-instance.md](./local-instance.md). Recommended is to use [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817).
### Can I run AIO offline or in an airgapped system?
No. This is not possible and will not be added due to multiple reasons: update checks, app installs via app-store, downloading additional docker images on demand and more.
### Are self-signed certificates supported for Nextcloud?
No and they will not be. If you want to run it locally, without opening Nextcloud to the public internet, please have a look at the [local instance documentation](./local-instance.md). Recommended is to use the [built-in deSEC domain registration](#how-to-get-a-free-domain-via-desec) to obtain a free domain with a valid certificate automatically, or alternatively [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817).
No and they will not be. If you want to run it locally, without opening Nextcloud to the public internet, please have a look at the [local instance documentation](./local-instance.md). Recommended is to use [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817).
### Can I use AIO with multiple domains?
No and it will not be added. However you can use [this feature](https://github.com/nextcloud/all-in-one/blob/main/multiple-instances.md) in order to create multiple AIO instances, one for each domain.
### Are other ports than the default 443 for Nextcloud supported?
No and they will not be. If port 443 and/or 80 is blocked for you, you may use the [built-in deSEC domain registration](#how-to-get-a-free-domain-via-desec) (which uses the Caddy community container on port 443) or [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817) if you want to publish it online. If you already run a different service on port 443, please use a dedicated domain for Nextcloud and set it up correctly by following the [reverse proxy documentation](./reverse-proxy.md). However in all cases the Nextcloud interface will redirect you to port 443.
No and they will not be. If port 443 and/or 80 is blocked for you, you may use [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817) if you want to publish it online. If you already run a different service on port 443, please use a dedicated domain for Nextcloud and set it up correctly by following the [reverse proxy documentation](./reverse-proxy.md). However in all cases the Nextcloud interface will redirect you to port 443.
### Can I run Nextcloud in a subdirectory on my domain?
No and it will not be added. Please use a dedicated (sub-)domain for Nextcloud and set it up correctly by following the [reverse proxy documentation](./reverse-proxy.md). If you don't have a domain yet, use the [built-in deSEC domain registration](#how-to-get-a-free-domain-via-desec) to get one for free, or alternatively [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817).
No and it will not be added. Please use a dedicated (sub-)domain for Nextcloud and set it up correctly by following the [reverse proxy documentation](./reverse-proxy.md). Alternatively, you may use [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817) if you want to publish it online.
### How can I access Nextcloud locally?
Please note that local access is not possible if you are running AIO behind Cloudflare Tunnel since TLS proxying is in that case offloaded to Cloudflares infrastructure. You can fix this by setting up your own reverse proxy that handles TLS proxying locally and will make the steps below work.
@@ -464,8 +446,6 @@ Now that this is out of the way, the recommended way how to access Nextcloud loc
- https://dockerlabs.collabnix.com/intermediate/networking/Configuring_DNS.html
Apart from that there is now a community container that can be added to the AIO stack: https://github.com/nextcloud/all-in-one/tree/main/community-containers/pi-hole
If you registered your domain via the built-in [deSEC integration](#how-to-get-a-free-domain-via-desec), the [dnsmasq](https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq) community container is automatically enabled and resolves your Nextcloud domain to the server's local IP for LAN clients. You only need to point your router's DHCP DNS server to the AIO host — see the [dnsmasq documentation](https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq) for details.
### How to overwrite the local DNS resolution for some domains or add extra hosts to the containers?
For some use cases, you might need to overwrite the local DNS resolution of some domains inside the containers. On Linux, you can do so either by using a local DNS server as described in the section above and add a local DNS entry into the dns server and make your containers use that DNS server.

View File

@@ -6,7 +6,7 @@ This guide explains how to connect to Nextcloud AIO securely via HTTPS (TLS) usi
- **Integrated**: AIO's built-in reverse proxy with automatic HTTPS
- **External**: An external reverse proxy (such as Caddy or Nginx or Cloudflare Proxy)
- **Secure tunnel**: Tunneling services for private network access or public access without port forwarding (such as Tailscale Serve or Cloudflare Tunnel)
- **Secure tunnel**: Tunneling services for private network access or public access without port forwarding (such as Tailscale Serve, Cloudflare Tunnel, or Pangolin)
## Choosing Your Approach
@@ -23,6 +23,7 @@ This guide explains how to connect to Nextcloud AIO securely via HTTPS (TLS) usi
| **Integrated** | Simple setups, single service on port 443 | Public IP, dedicated port 443 | Yes (443) |
| **External Reverse Proxy** (including Cloudflare Proxy) | Multiple services, existing web server, or users wanting DDoS protection | Existing reverse proxy, willingness to set one up, or Cloudflare account | Yes (443) |
| **Cloudflare Tunnel** | No port forwarding possible/desired, public access | Cloudflare account | No |
| **Pangolin** | No port forwarding possible/desired, public or private access, self-hostable tunnel | Pangolin Cloud account or self-hosted Pangolin server | No |
| **Tailscale Serve** | Private access (tailnet only) | Tailscale account | No |
| **Tailscale Funnel** | Public access via Tailscale | Tailscale account | No |
@@ -68,29 +69,32 @@ Using an existing external reverse proxy is required in particular if port `443/
> [!NOTE]
> An external reverse proxy can also facilitate other routing approaches, but Nextcloud AIO only supports having its own dedicated hostname (e.g., `cloud.example.com`). You cannot run it in a subfolder like `example.com/nextcloud/`.[^shared]
### Secure tunnel: Using AIO with a secure tunneling service (*Tailscale, Cloudflare*)
### Secure tunnel: Using AIO with a secure tunneling service (*Tailscale, Cloudflare, Pangolin*)
Cloudflare and Tailscale offer secure tunneling services that let you access your Nextcloud without opening ports on your firewall.
Cloudflare, Tailscale, and Pangolin offer secure tunneling services that let you access your Nextcloud without opening ports on your firewall.
#### Private network access
For Nextcloud AIO, you can use:
- **Cloudflare Tunnel (`cloudflared`)** - Secure outbound-only tunnels that don't require exposing ports
- **Tailscale Serve** - Expose services privately on your Tailscale network (tailnet only)
- **Pangolin** - WireGuard-based outbound tunnels using the Newt connector; can be used for both private and public access
Both options provide private network access to your Nextcloud AIO instance.
All three options provide private network access to your Nextcloud AIO instance.
#### Public Internet access (without port forwarding)
To make your Nextcloud AIO instance accessible from the public Internet (not just your private network), you can use:
- **Cloudflare Tunnel** with public routes enabled (which combines Cloudflare Tunnel with Cloudflare's proxy features)
- **Tailscale Funnel** - Expose services to the public Internet via Tailscale's infrastructure
- **Pangolin** - Expose services publicly via Pangolin Cloud or a self-hosted Pangolin server
**Comparison of Cloudflare and Tailscale options:**
**Comparison of Cloudflare, Tailscale, and Pangolin options:**
| Feature | Access Scope | Inbound Ports Required | Use Case |
|---------|--------------|----------------|----------|
| **Cloudflare Tunnel** | Public Internet | None | Public access without port forwarding |
| **Pangolin** | Public Internet or private | None | Public or private access; self-hostable |
| **Tailscale Serve** | Your Tailscale network only | None | Private access for you and invited users |
| **Tailscale Funnel** | Public Internet | None | Public access through Tailscale |
@@ -104,7 +108,7 @@ To make your Nextcloud AIO instance accessible from the public Internet (not jus
## Configuration and Deployment
> [!NOTE]
> These instructions assume you already have a domain name pointing to your server's public IP address. If you don't have a domain yet, AIO can register a free dynamic-DNS subdomain under `dedyn.io` for you via the built-in [deSEC integration](https://github.com/nextcloud/all-in-one#how-to-get-a-free-domain-via-desec) — no external setup needed. Alternatively, see the recommendations below.
> These instructions assume you already have a domain name pointing to your server's public IP address. If you don't have a domain yet, see the recommendations below.
### Quick overview
@@ -117,7 +121,7 @@ To run Nextcloud AIO behind an external reverse proxy or secure tunneling/proxyi
The sections below provide detailed instructions for each step.
> [!TIP]
> If you don't have a domain yet, AIO can register a free `*.dedyn.io` subdomain for you via [deSEC](https://desec.io) directly from the AIO interface — see [How to get a free domain via deSEC](https://github.com/nextcloud/all-in-one#how-to-get-a-free-domain-via-desec). This is the recommended option. Alternatively, you can use [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817). If you don't have an external reverse proxy yet, we recommend [Caddy](https://github.com/nextcloud/all-in-one/discussions/575).
> If you don't have a domain yet, we recommend using [an approach using Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817). If you don't have an external reverse proxy yet, we recommend [Caddy](https://github.com/nextcloud/all-in-one/discussions/575).
### Step-by-Step Instructions
@@ -737,6 +741,48 @@ httpServer.on('upgrade', (req, socket, head) => {
</details>
##### Pangolin
<details>
<summary>click here to expand</summary>
[Pangolin](https://pangolin.net/) is an open-source, identity-based remote access platform built on WireGuard that enables secure connectivity to private resources without opening inbound firewall ports. It uses the **Newt** connector as an outbound-only WireGuard tunnel client that runs alongside AIO on your server and connects to a Pangolin server (either Pangolin Cloud or your own self-hosted instance). From AIO's perspective, Pangolin works like a reverse proxy: Pangolin handles TLS and routes public traffic through the tunnel to AIO.
**Requirements:**
- A [Pangolin Cloud](https://app.pangolin.net/) account **or** a [self-hosted Pangolin server](https://docs.pangolin.net/self-host/quick-install)
- No open inbound ports required
**Setup:**
1. **Set up Pangolin**: Either sign up for a free [Pangolin Cloud](https://app.pangolin.net/) account, or [self-host Pangolin](https://docs.pangolin.net/self-host/quick-install) on a VPS with a public IP address.
1. **Create a site**: In the Pangolin dashboard, create a new site for the machine that will run AIO. Follow the instructions to generate a **Newt ID** and **Newt secret** for the site.
1. **Deploy Newt on the AIO host**: Run the Newt connector on the same machine as AIO. You can deploy it as a Docker container alongside AIO:
```yaml
services:
newt:
image: fosrl/newt
restart: unless-stopped
environment:
- PANGOLIN_ENDPOINT=https://app.pangolin.net # For Pangolin Cloud; replace with your self-hosted server URL if self-hosting
- NEWT_ID=<your-newt-id>
- NEWT_SECRET=<your-newt-secret>
```
Or as a standalone binary — see the [Newt documentation](https://github.com/fosrl/newt) for details.
1. **Create a resource**: In the Pangolin dashboard, create a new resource for the site. Set the target to `http://private.ip.address.of.server:11000`.<br>
⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the port and address to match your `APACHE_PORT` and `APACHE_IP_BINDING` settings.
1. **Configure AIO**: When starting AIO, add `--env SKIP_DOMAIN_VALIDATION=true` to disable domain validation (domain validation is known to not work through Pangolin tunnels). Use the domain assigned by Pangolin as your Nextcloud domain when opening the AIO interface.
1. Now continue with [point 2](#2-use-this-startup-command).
</details>
##### Synology Reverse Proxy
<details>

View File

@@ -9,13 +9,6 @@ For the below to work, it is important that you have a domain that you point ont
- [ ] Entering `10.0.0.1` should report that ip-addresses are not supported
- [ ] Entering `nextcloud.com` should report that the domain does not point to this server
- [ ] Entering the domain that does point to your server e.g. `yourdomain.com` should finally redirect you to the next screen (if you did not configure your domain yet or did not open port 443, it should report that to you)
- [ ] Below the domain input box there should be a collapsed `<details>` element labelled **"Don't have a domain? Get a free one from deSEC"**
- [ ] Expanding it should show a form with fields for email address and an optional subdomain slug, plus a **Register free domain via deSEC** submit button
- [ ] Submitting with an empty email should show an error asking for a valid email address
- [ ] Submitting with an invalid slug (e.g. `-bad-`) should show a validation error about the allowed slug format
- [ ] Submitting with a valid email and no slug should register a random `*.dedyn.io` domain, set it as the Nextcloud domain and redirect to the next screen
- [ ] After successful registration, the `caddy` and `dnsmasq` community containers should be listed as enabled in the Community Containers section
- [ ] After successful registration, there should be a **deSEC account credentials** section in the AIO interface that shows the registered email and allows revealing the password
- [ ] Now you should see a button `Start containers` and an explanation which points out that clicking on the button will start the containers and that this can take a long time.
- [ ] Below that you should see a section `Optional addons` which shows a checkbox list with addons that can be enabled or disabled.
- [ ] Collabora, Imaginary, Talk and Whiteboard should be enabled, the rest disabled