Installing NextCloud and Enabling Calendar App: Trisquel 12

#!/bin/bash
# =============================================================================
# Nextcloud Installer
# Targets: Ubuntu 22.04/24.04/26.04, Trisquel 11/12/13
# Requires: PowerDNS running, domain zone created
# NOTE: This script switches PHP from mod_php to FPM for all vhosts
# Uses DNS-01 (RFC2136/TSIG) for TLS — no propagation wait needed
# =============================================================================

set -euo pipefail

# --- Configuration ---
NEXTCLOUD_DOWNLOAD_URL="https://download.nextcloud.com/server/releases/latest.tar.bz2"
ROOT_MYSQL_PASSWORD="penguin"
NEXCLOUD_MYSQL_PASSWORD="penguin"
NEXTCLOUD_MYSQL_ADMIN_PASSWORD="penguin"
domain_name="tuxmail.io"
wireguard_static_ip="94.158.244.150"
user1_email_address="user1"

# --- Debugging helpers ---
LOG_FILE="/var/log/nextcloud-install.log"
PASS_COUNT=0
FAIL_COUNT=0

log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

die() {
log "FATAL: $*" >&2
exit 1
}

check() {
local desc="$1"
local cmd="$2"
if eval "$cmd" &>/dev/null; then
log " [PASS] $desc"
((PASS_COUNT++)) || true
else
log " [FAIL] $desc"
((FAIL_COUNT++)) || true
fi
}

verify() {
log "--- VERIFICATION ---"
}

get_php_ver() {
if [ -z "${php_ver:-}" ]; then
php_ver=$(ls /etc/php/ 2>/dev/null | grep -E '^[0-9]+\.[0-9]+$' | head -1)
if [ -z "$php_ver" ]; then
php_ver="8.3"
fi
fi
}

# =============================================================================
# Section 0: OS Detection
# =============================================================================
detect_os() {
log "=== SECTION: OS Detection ==="
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
else
die "Cannot detect OS"
fi
log "Detected OS: $OS_ID $OS_VERSION"

if [ "$OS_VERSION" = "22.04" ] || [ "$OS_VERSION" = "11.0" ]; then
php_ver="8.1"
elif [ "$OS_VERSION" = "24.04" ] || [ "$OS_VERSION" = "12.0" ]; then
php_ver="8.3"
elif [ "$OS_VERSION" = "26.04" ] || [ "$OS_VERSION" = "13.0" ]; then
php_ver="8.5"
else
php_ver=$(ls /etc/php/ 2>/dev/null | grep -E '^[0-9]+\.[0-9]+$' | head -1)
if [ -z "$php_ver" ]; then
php_ver="8.3"
fi
fi
log "Using PHP version: $php_ver"

verify
check "OS detected ($OS_ID $OS_VERSION)" "test -n '$OS_ID'"
check "PHP version resolved ($php_ver)" "test -n '$php_ver'"
}

# =============================================================================
# Section 1: Package Installation
# =============================================================================
section_packages() {
log "=== SECTION: Packages ==="
get_php_ver
apt-get update
apt-get install -y \
"php${php_ver}-common" \
"php${php_ver}-fpm" \
"php${php_ver}-gd" \
"php${php_ver}-mysql" \
"php${php_ver}-curl" \
"php${php_ver}-zip" \
"php${php_ver}-mbstring" \
"php${php_ver}-imagick" \
"php${php_ver}-bcmath" \
"php${php_ver}-xml" \
"php${php_ver}-intl" \
"php${php_ver}-gmp" \
"php${php_ver}-bz2" \
"php${php_ver}-apcu" \
php-redis \
apache2 \
mariadb-server \
libarchive-tools \
zip unzip wget bzip2 \
redis-server \
python3-certbot-apache \
python3-certbot-dns-rfc2136

if apt-get install -y libapache2-mod-http2 2>/dev/null; then
a2enmod http2
log "HTTP/2 module installed and enabled"
else
log "WARNING: libapache2-mod-http2 not available, skipping HTTP/2"
fi

a2enmod rewrite dir mime env headers
log "Packages installed"

verify
check "PHP FPM installed" "test -f /etc/php/${php_ver}/fpm/php-fpm.conf"
check "Redis server installed" "test -f /usr/bin/redis-server"
check "certbot dns-rfc2136 plugin installed" "certbot --help plugins 2>/dev/null | grep -q 'dns-rfc2136'"
}

# =============================================================================
# Section 2: Database
# =============================================================================
section_database() {
log "=== SECTION: Database ==="

mysql --user="root" --password="$ROOT_MYSQL_PASSWORD" <> /etc/powerdns/pdns.conf
fi
systemctl restart pdns
log "dnsupdate=yes enabled in PowerDNS"
fi

# Generate TSIG key and configure DNS updates
if [ ! -f /root/certbot-dns-credentials.ini ]; then
log "Generating TSIG key for certbot..."
local tsig_output
tsig_output=$(pdnsutil generate-tsig-key certbot hmac-sha256)
log "$tsig_output"

local tsig_key
tsig_key=$(echo "$tsig_output" | grep -oP 'hmac-sha256\s+\K\S+')

if [ -z "$tsig_key" ]; then
die "Failed to extract TSIG key from pdnsutil output: $tsig_output"
fi

pdnsutil import-tsig-key certbot hmac-sha256 "$tsig_key"
log "TSIG key imported into PowerDNS"

pdnsutil set-meta "$domain_name" TSIG-ALLOW-DNSUPDATE certbot
pdnsutil set-meta "$domain_name" ALLOW-DNSUPDATE-FROM 127.0.0.1
log "DNS update permissions set for zone $domain_name"

cat > /root/certbot-dns-credentials.ini << EOF
dns_rfc2136_server = 127.0.0.1
dns_rfc2136_port = 53
dns_rfc2136_name = certbot
dns_rfc2136_secret = ${tsig_key}
dns_rfc2136_algorithm = HMAC-SHA256
EOF
chmod 600 /root/certbot-dns-credentials.ini
log "Certbot DNS credentials written to /root/certbot-dns-credentials.ini"
else
log "TSIG credentials already exist, skipping key generation"
fi

# Always ensure PowerDNS-side config is in sync (idempotent)
local tsig_key_existing
tsig_key_existing=$(grep 'dns_rfc2136_secret' /root/certbot-dns-credentials.ini | awk '{print $3}')
pdnsutil import-tsig-key certbot hmac-sha256 "$tsig_key_existing" 2>/dev/null || true
pdnsutil set-meta "$domain_name" TSIG-ALLOW-DNSUPDATE certbot
pdnsutil set-meta "$domain_name" ALLOW-DNSUPDATE-FROM 127.0.0.1

# Add DNS A record for nextcloud
if ! pdnsutil list-zone "$domain_name" 2>/dev/null | grep -q "^nextcloud.${domain_name}"; then
pdnsutil add-record "$domain_name" nextcloud A "$wireguard_static_ip"
log "DNS A record added for nextcloud.${domain_name}"
else
log "DNS A record already exists for nextcloud.${domain_name}"
fi

# Get TLS cert via DNS-01
if [ ! -f "/etc/letsencrypt/live/nextcloud.${domain_name}/fullchain.pem" ]; then
log "Requesting TLS certificate via DNS-01 (RFC2136)..."
certbot certonly --dns-rfc2136 \
--dns-rfc2136-credentials /root/certbot-dns-credentials.ini \
--agree-tos --non-interactive \
--email "${user1_email_address}@${domain_name}" \
-d "nextcloud.${domain_name}"
log "TLS certificate obtained for nextcloud.${domain_name}"
else
log "TLS certificate already exists for nextcloud.${domain_name}"
fi

setfacl -R -m u:www-data:rx /etc/letsencrypt/live/ /etc/letsencrypt/archive/ 2>/dev/null || true

verify
check "TSIG credentials file exists" "test -f /root/certbot-dns-credentials.ini"
check "Cert exists for nextcloud" "test -f /etc/letsencrypt/live/nextcloud.${domain_name}/fullchain.pem"
check "Cert is for correct domain" "openssl x509 -noout -subject -in /etc/letsencrypt/live/nextcloud.${domain_name}/fullchain.pem | grep -q 'nextcloud.${domain_name}'"
check "Cert is valid (not expired)" "openssl x509 -checkend 86400 -noout -in /etc/letsencrypt/live/nextcloud.${domain_name}/fullchain.pem"
}

# =============================================================================
# Section 6: Switch to PHP-FPM + Update All VHosts
# =============================================================================
section_fpm_switch() {
log "=== SECTION: PHP-FPM Switch ==="
get_php_ver

a2dismod "php${php_ver}" 2>/dev/null || true
a2dismod mpm_prefork 2>/dev/null || true
a2enmod mpm_event proxy_fcgi setenvif
a2enconf "php${php_ver}-fpm"

local fpm_handler="SetHandler \"proxy:unix:/var/run/php/php${php_ver}-fpm.sock|fcgi://localhost/\""

for vhost in /etc/apache2/sites-available/*.conf; do
local vhost_name
vhost_name=$(basename "$vhost")

if echo "$vhost_name" | grep -qE "^nextcloud\."; then
continue
fi

if ! grep -q "DocumentRoot" "$vhost"; then
continue
fi

if grep -q "FilesMatch" "$vhost"; then
continue
fi

sed -i "s## \n ${fpm_handler}\n \n#g" "$vhost"
log "FPM handler added to $vhost_name"
done

# Create Nextcloud HTTP vhost (redirect to HTTPS)
cat > "/etc/apache2/sites-available/nextcloud.${domain_name}.conf" << EOF

ServerName nextcloud.${domain_name}
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)\$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

EOF

# Create Nextcloud HTTPS vhost
local http2_line=""
if [ -f /usr/lib/apache2/modules/mod_http2.so ]; then
http2_line=" Protocols h2 h2c http/1.1"
fi

cat > "/etc/apache2/sites-available/nextcloud.${domain_name}-ssl.conf" << EOF

ServerName nextcloud.${domain_name}
DocumentRoot /var/www/nextcloud

SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/nextcloud.${domain_name}/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/nextcloud.${domain_name}/privkey.pem
${http2_line}

Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted

${fpm_handler}

Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"

ErrorLog \${APACHE_LOG_DIR}/nextcloud_error.log
CustomLog \${APACHE_LOG_DIR}/nextcloud_access.log combined

EOF

a2ensite "nextcloud.${domain_name}.conf"
a2ensite "nextcloud.${domain_name}-ssl.conf"
a2dissite "nextcloud.${domain_name}-le-ssl.conf" 2>/dev/null || true

if ! apache2ctl -t 2>&1; then
die "Apache config test failed. Run 'apache2ctl -t' to see the error."
fi

systemctl restart apache2
sleep 2

verify
check "PHP-FPM is running" "systemctl is-active php${php_ver}-fpm"
check "Apache is running" "systemctl is-active apache2"
check "Nextcloud HTTPS vhost enabled" "test -f /etc/apache2/sites-enabled/nextcloud.${domain_name}-ssl.conf"
check "Nextcloud responds locally" "for i in 1 2 3 4 5; do curl -sk -o /dev/null -w '%{http_code}' --resolve 'nextcloud.${domain_name}:443:127.0.0.1' https://nextcloud.${domain_name}/ 2>/dev/null | grep -qE '^(200|301|302)\$' && break; sleep 1; done"
check "HTTP redirects to HTTPS" "curl -s -o /dev/null -w '%{http_code}' --resolve 'nextcloud.${domain_name}:80:127.0.0.1' http://nextcloud.${domain_name}/ | grep -q '301'"
}

# =============================================================================
# Section 7: Nextcloud Configuration (via occ)
# =============================================================================
section_config() {
log "=== SECTION: Nextcloud Configuration ==="
get_php_ver

# Ensure ownership is correct before running occ
chown -R www-data:www-data /var/www/nextcloud/

local occ="php /var/www/nextcloud/occ"

# Set tempdirectory first to avoid /tmp Snowflake FileSequence ownership issues
$occ config:system:set tempdirectory --value="/var/www/nextcloud/data/tmp" 2>/dev/null || true

$occ config:system:set trusted_domains 1 --value="nextcloud.${domain_name}" 2>/dev/null || true
log "Trusted domains set"

$occ config:system:set htaccess.RewriteBase --value="/" 2>/dev/null || true
log "RewriteBase set"

$occ config:system:set defaultapp --value="calendar" 2>/dev/null || true
log "Default app set to calendar"

$occ config:system:set filelocking.enabled --value="true" 2>/dev/null || true
log "File locking enabled"

php /var/www/nextcloud/occ maintenance:update:htaccess
log "htaccess updated"

# Fix ownership after occ runs as root
chown -R www-data:www-data /var/www/nextcloud/

verify
check "config.php is valid" "php -l /var/www/nextcloud/config/config.php"
check "Trusted domain is set" "grep -q 'nextcloud.${domain_name}' /var/www/nextcloud/config/config.php"
check "File locking is enabled" "grep -q 'filelocking.enabled' /var/www/nextcloud/config/config.php"
}

# =============================================================================
# Section 8: Redis Configuration
# =============================================================================
section_redis() {
log "=== SECTION: Redis ==="
get_php_ver

# Replace entire lines to avoid leftover values
sed -i 's|^port .*|port 0|' /etc/redis/redis.conf
sed -i 's|^#\? *unixsocket.*|unixsocket /var/run/redis/redis.sock|' /etc/redis/redis.conf
sed -i 's|^#\? *unixsocketperm.*|unixsocketperm 770|' /etc/redis/redis.conf

# Remove duplicate unixsocket/unixsocketperm lines (keep last occurrence)
local tmp_conf="/tmp/redis.conf.tmp"
tac /etc/redis/redis.conf | awk '
/^unixsocket / { if (seen_us++) next }
/^unixsocketperm / { if (seen_up++) next }
{ print }
' | tac > "$tmp_conf"
mv "$tmp_conf" /etc/redis/redis.conf
chmod 644 /etc/redis/redis.conf

log "Redis configured for Unix socket"

if ! id -nG www-data | tr ' ' '\n' | grep -q '^redis$'; then
usermod -a -G redis www-data
log "www-data added to redis group"
fi

# Ensure socket directory exists
mkdir -p /var/run/redis
chown redis:redis /var/run/redis
chmod 770 /var/run/redis

systemctl reset-failed redis-server 2>/dev/null || true
systemctl restart redis-server
systemctl enable redis-server

# Ensure ownership before running occ
chown -R www-data:www-data /var/www/nextcloud/

local occ="php /var/www/nextcloud/occ"
$occ config:system:set memcache.locking --value="\OC\Memcache\Redis" 2>/dev/null || true
$occ config:system:set redis.host --value="/var/run/redis/redis.sock" 2>/dev/null || true
$occ config:system:set redis.port --value="0" 2>/dev/null || true
$occ config:system:set redis.dbindex --value="0" 2>/dev/null || true
$occ config:system:set redis.timeout --value="1.5" 2>/dev/null || true
log "Nextcloud configured to use Redis for locking"

# Fix ownership after occ runs as root
chown -R www-data:www-data /var/www/nextcloud/

verify
check "Redis is running" "systemctl is-active redis-server"
check "Redis socket exists" "test -S /var/run/redis/redis.sock"
check "Redis locking configured in Nextcloud" "grep -q 'memcache.locking' /var/www/nextcloud/config/config.php"
}

# =============================================================================
# Section 9: APCu Configuration
# =============================================================================
section_apcu() {
log "=== SECTION: APCu ==="
get_php_ver

# Ensure ownership before running occ
chown -R www-data:www-data /var/www/nextcloud/

local occ="php /var/www/nextcloud/occ"
$occ config:system:set memcache.local --value="\OC\Memcache\APCu" 2>/dev/null || true
log "APCu configured for local memory cache"

# Fix ownership after occ runs as root
chown -R www-data:www-data /var/www/nextcloud/

verify
check "APCu extension loaded" "php -m | grep -qi 'apcu'"
check "APCu configured in Nextcloud" "grep -q 'memcache.local' /var/www/nextcloud/config/config.php"
}

# =============================================================================
# Section 10: PHP Tuning
# =============================================================================
section_php_tuning() {
log "=== SECTION: PHP Tuning ==="
get_php_ver

local php_ini="/etc/php/${php_ver}/fpm/php.ini"
local pool_conf="/etc/php/${php_ver}/fpm/pool.d/www.conf"

sed -i "s/^upload_max_filesize = .*/upload_max_filesize = 64M/" "$php_ini"
sed -i "s/^post_max_size = .*/post_max_size = 96M/" "$php_ini"
sed -i "s/^memory_limit = .*/memory_limit = 512M/" "$php_ini"
sed -i "s/^max_execution_time = .*/max_execution_time = 600/" "$php_ini"
sed -i "s/^max_input_vars = .*/max_input_vars = 3000/" "$php_ini"
sed -i "s/^max_input_time = .*/max_input_time = 1000/" "$php_ini"
log "PHP.ini tuned"

sed -i "s/^pm.max_children = .*/pm.max_children = 64/" "$pool_conf"
sed -i "s/^pm.start_servers = .*/pm.start_servers = 16/" "$pool_conf"
sed -i "s/^pm.min_spare_servers = .*/pm.min_spare_servers = 16/" "$pool_conf"
sed -i "s/^pm.max_spare_servers = .*/pm.max_spare_servers = 32/" "$pool_conf"
log "FPM pool tuned"

if ! grep -q "^opcache.enable=1" "$php_ini"; then
cat >> "$php_ini" << 'OPCACHE'

; OPcache settings
opcache.enable=1
opcache.enable_cli=1
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.memory_consumption=128
opcache.save_comments=1
opcache.revalidate_freq=60
OPCACHE
log "OPcache enabled"
else
log "OPcache already configured"
fi

if [ -f "/etc/php/${php_ver}/mods-available/apcu.ini" ]; then
if ! grep -q "apc.enable_cli=1" "/etc/php/${php_ver}/mods-available/apcu.ini"; then
echo "apc.enable_cli=1" >> "/etc/php/${php_ver}/mods-available/apcu.ini"
log "apc.enable_cli=1 set"
fi
fi

if ! grep -q "redis.session.locking_enabled" "$php_ini"; then
cat >> "$php_ini" << 'REDIS'

; Redis session locking
redis.session.locking_enabled=1
redis.session.lock_retries=-1
redis.session.lock_wait_time=10000
REDIS
log "Redis session locking enabled in PHP"
fi

systemctl restart "php${php_ver}-fpm"
systemctl restart apache2
sleep 2

verify
check "upload_max_filesize is 64M" "grep -q '^upload_max_filesize = 64M' $php_ini"
check "memory_limit is 512M" "grep -q '^memory_limit = 512M' $php_ini"
check "OPcache enabled" "grep -q '^opcache.enable=1' $php_ini"
check "FPM max_children is 64" "grep -q '^pm.max_children = 64' $pool_conf"
check "PHP-FPM is running" "systemctl is-active php${php_ver}-fpm"
check "Apache is running" "systemctl is-active apache2"
}

# =============================================================================
# Section 11: Enable Apps
# =============================================================================
section_apps() {
log "=== SECTION: Apps ==="
get_php_ver

# Ensure ownership before running occ
chown -R www-data:www-data /var/www/nextcloud/

local occ="php /var/www/nextcloud/occ"

$occ app:enable calendar 2>/dev/null || true
log "Calendar app enabled"

# Fix ownership after occ runs as root
chown -R www-data:www-data /var/www/nextcloud/

verify
check "Calendar app enabled" "php /var/www/nextcloud/occ app:list 2>/dev/null | grep -qi calendar"
}

# =============================================================================
# Section 12: Final Verification
# =============================================================================
section_final_test() {
log "=== SECTION: Final Verification ==="
get_php_ver

verify
check "Nextcloud reachable over HTTPS" "for i in 1 2 3 4 5; do curl -sk -o /dev/null -w '%{http_code}' --resolve 'nextcloud.${domain_name}:443:127.0.0.1' https://nextcloud.${domain_name}/ 2>/dev/null | grep -qE '^(200|301|302)\$' && break; sleep 1; done"
check "Cert is for correct domain" "echo | openssl s_client -connect 127.0.0.1:443 -servername nextcloud.${domain_name} 2>/dev/null | openssl x509 -noout -subject | grep -q 'nextcloud.${domain_name}'"
check "HTTP redirects to HTTPS" "curl -s -o /dev/null -w '%{http_code}' --resolve 'nextcloud.${domain_name}:80:127.0.0.1' http://nextcloud.${domain_name}/ | grep -q '301'"
check "PHP-FPM running" "systemctl is-active php${php_ver}-fpm"
check "Apache running" "systemctl is-active apache2"
check "Redis running" "systemctl is-active redis-server"
check "MariaDB running" "systemctl is-active mariadb"
check "PowerDNS still answering" "dig @127.0.0.1 $domain_name NS +short | grep -q 'ns1'"
check "Nextcloud A record in zone" "pdnsutil list-zone '$domain_name' | grep -q 'nextcloud'"
check "Dovecot still running" "systemctl is-active dovecot"
check "Postfix still running" "systemctl is-active postfix"
check "config dir writable by www-data" "test -w /var/www/nextcloud/config/"
check "data dir writable by www-data" "test -w /var/www/nextcloud/data/"
}

# =============================================================================
# Main
# =============================================================================
ALL_SECTIONS=(
detect_os
section_packages
section_database
section_download
section_install_nextcloud
section_certbot_dns
section_fpm_switch
section_config
section_redis
section_apcu
section_php_tuning
section_apps
section_final_test
)

if [ $# -gt 0 ]; then
for section in "$@"; do
case "$section" in
detect_os|section_*)
log "Running: $section"
"$section"
;;
all)
for s in "${ALL_SECTIONS[@]}"; do
log "Running: $s"
"$s"
done
;;
*)
die "Unknown section: $section"
;;
esac
done
else
for s in "${ALL_SECTIONS[@]}"; do
log "Running: $s"
"$s"
done
fi

# --- Summary ---
log "=============================================="
log " PASSED: $PASS_COUNT"
log " FAILED: $FAIL_COUNT"
log "=============================================="

if [ "$FAIL_COUNT" -gt 0 ]; then
log "WARNING: Some checks failed. Review [FAIL] entries above."
log "Full log: $LOG_FILE"
exit 1
else
log "All checks passed."
fi
log "=== Nextcloud installation complete ==="