diff --git a/.docker/Dockerfile.nginx-port-selector b/.docker/Dockerfile.nginx-port-selector new file mode 100644 index 0000000..d309795 --- /dev/null +++ b/.docker/Dockerfile.nginx-port-selector @@ -0,0 +1,9 @@ +FROM docker:27-cli + +RUN apk add --no-cache docker-cli-compose + +COPY scripts/nginx-port-selector.sh /usr/local/bin/nginx-port-selector.sh + +RUN chmod +x /usr/local/bin/nginx-port-selector.sh + +ENTRYPOINT ["/usr/local/bin/nginx-port-selector.sh"] diff --git a/.docker/scripts/nextcloud-entrypoint.sh b/.docker/scripts/nextcloud-entrypoint.sh index 3fbccb5..80d34c3 100644 --- a/.docker/scripts/nextcloud-entrypoint.sh +++ b/.docker/scripts/nextcloud-entrypoint.sh @@ -146,5 +146,5 @@ exec busybox crond -f -l 0 -L /dev/stdout > /dev/null 2>&1 & runuser -u www-data -- php -f /var/www/html/cron.php # Start PHP-FPM -echo "💙 Nextcloud is up!" +echo "Starting PHP-FPM..." exec "$@" diff --git a/.docker/scripts/nginx-port-selector.sh b/.docker/scripts/nginx-port-selector.sh new file mode 100644 index 0000000..d9b99c1 --- /dev/null +++ b/.docker/scripts/nginx-port-selector.sh @@ -0,0 +1,125 @@ +#!/bin/sh + +set -eu + +compose_project() { + docker inspect --format '{{ index .Config.Labels "com.docker.compose.project" }}' "$(hostname)" +} + +project="$(compose_project)" +if [ -z "$project" ]; then + echo 'Could not determine the Compose project from the selector container.' >&2 + exit 1 +fi + +if [ -z "${PROJECT_DIR:-}" ]; then + echo 'The host project directory was not provided to the selector.' >&2 + exit 1 +fi + +compose() { + docker compose \ + --project-name "$project" \ + --project-directory "$PROJECT_DIR" \ + --file "$PROJECT_DIR/docker-compose.yml" \ + "$@" +} + +echo "Validating Compose project ${project} at ${PROJECT_DIR}." +compose config --quiet + +cleanup_nginx() { + compose rm --force --stop nginx || true +} + +trap cleanup_nginx INT TERM + +start_nginx() { + HTTP_PORT="$1" HTTPS_PORT="$2" IP_BIND="${IP_BIND:-127.0.0.1}" \ + compose up --detach --no-deps --scale nginx=1 nginx +} + +published_port() { + mapping="$(compose port "$1" "$2" 2>/dev/null || true)" + [ -n "$mapping" ] || return 0 + printf '%s\n' "${mapping##*:}" +} + +report_environment_ready() { + selector_http_port="$1" + selector_https_port="$2" + mailpit_port="$(published_port mailpit 8025)" + eurooffice_port="$(published_port eurooffice 80)" + playwright_port="$(published_port playwright 9323)" + signal_port="$(published_port signal-gateway 8080)" + + compose exec -T \ + -e ENV_HTTP_PORT="$selector_http_port" \ + -e ENV_HTTPS_PORT="$selector_https_port" \ + -e ENV_MAILPIT_PORT="$mailpit_port" \ + -e ENV_EUROOFFICE_PORT="$eurooffice_port" \ + -e ENV_PLAYWRIGHT_PORT="$playwright_port" \ + -e ENV_SIGNAL_PORT="$signal_port" \ + -e ENV_ADMIN_USER="$NEXTCLOUD_ADMIN_USER" \ + -e ENV_ADMIN_PASSWORD="$NEXTCLOUD_ADMIN_PASSWORD" \ + -e ENV_NEXTCLOUD_BRANCH="$VERSION_NEXTCLOUD" \ + nextcloud sh /var/www/scripts/report-environment-ready +} + +exit_successfully() { + echo '✅ Port selection complete. Exiting normally.' + exit 0 +} + +if [ "${HTTP_PORT+x}" = x ] || [ "${HTTPS_PORT+x}" = x ]; then + echo "Explicit ports requested: HTTP ${HTTP_PORT:-80}, HTTPS ${HTTPS_PORT:-443}." + set +e + output="$(start_nginx "${HTTP_PORT:-80}" "${HTTPS_PORT:-443}" 2>&1)" + status=$? + set -e + printf '%s\n' "$output" + if [ "$status" -ne 0 ]; then + cleanup_nginx + exit "$status" + fi + echo 'nginx started with explicit ports.' + if ! report_environment_ready "${HTTP_PORT:-80}" "${HTTPS_PORT:-443}"; then + echo 'Could not print environment banner.' >&2 + fi + exit_successfully +fi + +offset=0 +while [ "$offset" -le 19 ]; do + http_port=$((80 + offset)) + https_port=$((443 + offset)) + echo "Trying nginx ports HTTP ${http_port} / HTTPS ${https_port}." + + set +e + output="$(start_nginx "$http_port" "$https_port" 2>&1)" + status=$? + set -e + printf '%s\n' "$output" + + if [ "$status" -eq 0 ]; then + echo "nginx started with HTTP ${http_port} / HTTPS ${https_port}." + if ! report_environment_ready "$http_port" "$https_port"; then + echo 'Could not print environment banner.' >&2 + fi + exit_successfully + fi + + if ! printf '%s\n' "$output" | grep -Eiq \ + 'address already in use|port is already allocated|port is already in use|failed to bind host port|cannot bind .* port'; then + echo 'nginx failed for a reason unrelated to a port conflict; stopping.' >&2 + cleanup_nginx + exit "$status" + fi + + echo "Ports ${http_port}/${https_port} are unavailable; trying the next pair." >&2 + cleanup_nginx + offset=$((offset + 1)) +done + +echo 'No available nginx port pair found in HTTP 80-99 / HTTPS 443-462.' >&2 +exit 1 diff --git a/.docker/scripts/report-environment-ready b/.docker/scripts/report-environment-ready new file mode 100644 index 0000000..2c5a402 --- /dev/null +++ b/.docker/scripts/report-environment-ready @@ -0,0 +1,58 @@ +#!/bin/sh + +set -eu + +wait_for_php_fpm() { + attempt=0 + while [ "$attempt" -lt 100 ]; do + if php -r ' + $socket = @fsockopen("127.0.0.1", 9000, $errno, $errstr, 0.1); + if ($socket === false) { + exit(1); + } + fclose($socket); + ' >/dev/null 2>&1; then + return 0 + fi + attempt=$((attempt + 1)) + sleep 0.1 + done + return 1 +} + +url() { + port="$1" + base="$2" + if [ "$port" = "80" ] || [ "$port" = "443" ]; then + printf '%s' "$base" + else + printf '%s:%s' "$base" "$port" + fi +} + +emit_banner() { + { + printf '\n' + printf '┌─ 💙 Environment ready ─────────────────────\n' + printf '│\n' + [ -z "${ENV_HTTP_PORT:-}" ] || printf '│ %-16s %s\n' 'Nextcloud HTTP' "$(url "$ENV_HTTP_PORT" http://localhost)" + [ -z "${ENV_HTTPS_PORT:-}" ] || printf '│ %-16s %s\n' 'Nextcloud HTTPS' "$(url "$ENV_HTTPS_PORT" https://localhost)" + [ -z "${ENV_MAILPIT_PORT:-}" ] || printf '│ %-16s %s\n' 'Mailpit' "http://localhost:$ENV_MAILPIT_PORT" + [ -z "${ENV_EUROOFFICE_PORT:-}" ] || printf '│ %-16s %s\n' 'EuroOffice' "http://localhost:$ENV_EUROOFFICE_PORT" + [ -z "${ENV_PLAYWRIGHT_PORT:-}" ] || printf '│ %-16s %s\n' 'Playwright' "http://localhost:$ENV_PLAYWRIGHT_PORT" + [ -z "${ENV_SIGNAL_PORT:-}" ] || printf '│ %-16s %s\n' 'Signal' "http://localhost:$ENV_SIGNAL_PORT" + printf '│\n' + [ -z "${ENV_NEXTCLOUD_BRANCH:-}" ] || printf '│ %-16s %s\n' 'Nextcloud branch' "$ENV_NEXTCLOUD_BRANCH" + [ -z "${ENV_ADMIN_USER:-}" ] || printf '│ %-16s %s\n' 'Admin user' "$ENV_ADMIN_USER" + [ -z "${ENV_ADMIN_PASSWORD:-}" ] || printf '│ %-16s %s\n' 'Admin password' "$ENV_ADMIN_PASSWORD" + printf '│\n' + printf '└────────────────────────────────────────────\n' + } > /proc/1/fd/2 +} + +if ! wait_for_php_fpm; then + echo 'Could not confirm PHP-FPM readiness; skipping environment banner.' >&2 + exit 0 +fi + +emit_banner diff --git a/docker-compose.yml b/docker-compose.yml index e6af1f3..29567fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,6 +48,7 @@ services: - host.docker.internal:host-gateway nginx: image: ghcr.io/librecodecoop/nextcloud-dev-nginx:latest + scale: 0 # build: # context: .docker/ # dockerfile: Dockerfile.nginx @@ -60,11 +61,30 @@ services: - ./volumes/nginx/certs:/certs ports: - target: 80 - published: "${HTTP_PORT:-80-99}" + published: "${HTTP_PORT:-80}" host_ip: ${IP_BIND:-127.0.0.1} - target: 443 - published: "${HTTPS_PORT:-443-462}" + published: "${HTTPS_PORT:-443}" host_ip: ${IP_BIND:-127.0.0.1} + nginx-port-selector: + build: + context: .docker/ + dockerfile: Dockerfile.nginx-port-selector + volumes: + - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock + - .:${PWD}:ro + working_dir: ${PWD} + environment: + - HTTP_PORT + - HTTPS_PORT + - IP_BIND + - PROJECT_DIR=${PWD} + - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin} + - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin} + - VERSION_NEXTCLOUD=${VERSION_NEXTCLOUD:-master} + restart: "no" + depends_on: + - nextcloud mailpit: image: axllent/mailpit ports: