diff --git a/arch_install.sh b/arch_install.sh index 40fb4a4..00686df 100644 --- a/arch_install.sh +++ b/arch_install.sh @@ -1,28 +1,16 @@ #!/usr/bin/env bash -# Nextcloud + Apache + PHP-FPM on Arch Linux +# Nextcloud + Apache + PHP 8.3 (php-legacy) + MariaDB + Redis on Arch Linux # Production-oriented, reverse-proxy aware # -# What this does: -# - Installs Apache, MariaDB, Redis, PHP-FPM and needed PHP extensions -# - Downloads official Nextcloud release tarball -# - Configures Apache -> PHP-FPM via mod_proxy_fcgi -# - Configures MariaDB database/user -# - Configures Redis over unix socket for locking/cache -# - Installs Nextcloud non-interactively -# - Sets trusted proxy / HTTPS overwrite options -# - Adds cron job for background jobs -# -# Assumptions: -# - Arch server with systemd -# - You want Apache listening on :80 and TLS terminated upstream -# - Apache runs as user/group "http" on Arch -# -# Run as root. +# Run as root: +# sudo ./arch_install.sh set -euo pipefail IFS=$'\n\t' -# ====== EDIT ME ====== +# ========================= +# EDIT THESE VARIABLES +# ========================= DOMAIN="domain.com" NC_DIR="/srv/http/nextcloud" @@ -35,49 +23,115 @@ DB_PASS="changeMe" ADMIN_USER="admin" ADMIN_PASS="changeMe" -# If behind Cloudflare Tunnel or another local reverse proxy: +# If you're behind Cloudflare Tunnel or another reverse proxy: TRUST_LOCAL_PROXY="true" -REAL_IP_HEADER="CF-Connecting-IP" # or X-Forwarded-For if not Cloudflare +REAL_IP_HEADER="CF-Connecting-IP" # use X-Forwarded-For if not Cloudflare -# Download URL: pin a version for production stability -# Check https://download.nextcloud.com/server/releases/ +# Pin a specific release for production stability NC_VERSION="33.0.0" NC_TARBALL_URL="https://download.nextcloud.com/server/releases/nextcloud-${NC_VERSION}.tar.bz2" NC_SHA512_URL="https://download.nextcloud.com/server/releases/nextcloud-${NC_VERSION}.tar.bz2.sha512" -# ====== Sanity checks ====== -if [[ $EUID -ne 0 ]]; then - echo "Run this script as root." - exit 1 -fi +# PHP legacy paths/services on Arch +PHP_BIN="/usr/bin/php-legacy" +PHP_INI="/etc/php-legacy/php.ini" +PHP_FPM_WWW_CONF="/etc/php-legacy/php-fpm.d/www.conf" +PHP_FPM_SERVICE="php-fpm-legacy" -for v in DOMAIN NC_DIR NC_DATA_DIR DB_NAME DB_USER DB_PASS ADMIN_USER ADMIN_PASS NC_VERSION; do - if [[ -z "${!v}" ]]; then - echo "Variable $v is empty. Edit the script first." +# Redis socket +REDIS_SOCK="/run/redis/redis.sock" + +# ========================= +# HELPER FUNCTIONS +# ========================= +log() { + printf '\n==== %s ====\n' "$1" +} + +require_root() { + if [[ $EUID -ne 0 ]]; then + echo "Run this script as root." exit 1 fi -done +} -# ====== Packages ====== +check_vars() { + local vars=( + DOMAIN NC_DIR NC_DATA_DIR + DB_NAME DB_USER DB_PASS + ADMIN_USER ADMIN_PASS + NC_VERSION NC_TARBALL_URL NC_SHA512_URL + ) + for v in "${vars[@]}"; do + if [[ -z "${!v}" ]]; then + echo "Variable $v is empty. Edit the script first." + exit 1 + fi + done +} + +enable_php_ext() { + local ext="$1" + if ! grep -Eq "^[[:space:]]*extension=${ext}\.so" "$PHP_INI"; then + sed -i "/^;extension=${ext}\.so/s/^;//" "$PHP_INI" || true + if ! grep -Eq "^[[:space:]]*extension=${ext}\.so" "$PHP_INI"; then + printf "\nextension=%s.so\n" "$ext" >> "$PHP_INI" + fi + fi +} + +set_ini_value() { + local key="$1" + local value="$2" + if grep -Eq "^[;[:space:]]*${key}[[:space:]]*=" "$PHP_INI"; then + sed -ri "s|^[;[:space:]]*${key}[[:space:]]*=.*|${key} = ${value}|" "$PHP_INI" + else + printf "\n%s = %s\n" "$key" "$value" >> "$PHP_INI" + fi +} + +set_fpm_value() { + local key="$1" + local value="$2" + if grep -Eq "^[;[:space:]]*${key}[[:space:]]*=" "$PHP_FPM_WWW_CONF"; then + sed -ri "s|^[;[:space:]]*${key}[[:space:]]*=.*|${key} = ${value}|" "$PHP_FPM_WWW_CONF" + else + printf "\n%s = %s\n" "$key" "$value" >> "$PHP_FPM_WWW_CONF" + fi +} + +# ========================= +# PRECHECKS +# ========================= +require_root +check_vars + +log "Updating system and installing packages" pacman -Syu --noconfirm pacman -S --needed --noconfirm \ apache mariadb redis cronie \ - php php-fpm php-gd php-intl php-sodium php-zip php-apcu php-redis php-imagick \ + php-legacy php-legacy-fpm php-legacy-gd php-legacy-intl php-legacy-sodium \ + php-legacy-apcu php-legacy-redis php-legacy-imagick \ curl wget tar bzip2 unzip sudo -# ====== Directories ====== +# ========================= +# DIRECTORIES +# ========================= +log "Creating base directories" install -d -m 0755 /srv/http install -d -m 0750 "${NC_DATA_DIR}" -# ====== MariaDB ====== +# ========================= +# MARIADB +# ========================= +log "Initializing and configuring MariaDB" if [[ ! -d /var/lib/mysql/mysql ]]; then mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql fi systemctl enable --now mariadb -# Headless mysql_secure_installation-ish cleanup mysql <<'SQL' DELETE FROM mysql.user WHERE User=''; DROP DATABASE IF EXISTS test; @@ -93,140 +147,104 @@ GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost'; FLUSH PRIVILEGES; SQL -# ====== PHP ====== -# Enable required/recommended extensions in /etc/php/php.ini -PHP_INI="/etc/php/php.ini" +# ========================= +# PHP +# ========================= +log "Configuring PHP 8.3 legacy stack" -enable_php_ext() { - local ext="$1" - if ! grep -Eq "^[[:space:]]*extension=${ext}\.so" "$PHP_INI"; then - sed -i "/^;extension=${ext}\.so/s/^;//" "$PHP_INI" || true - if ! grep -Eq "^[[:space:]]*extension=${ext}\.so" "$PHP_INI"; then - printf "\nextension=%s.so\n" "$ext" >> "$PHP_INI" - fi - fi -} - -# Core modules shipped as split packages / shared modules on Arch -for ext in gd intl mysqli pdo_mysql sodium zip; do +# Required/recommended extensions for Nextcloud +for ext in gd intl mysqli pdo_mysql sodium zip apcu redis imagick; do enable_php_ext "$ext" done -# Recommended php.ini tuning for Nextcloud -sed -ri 's/^;?\s*memory_limit\s*=.*/memory_limit = 512M/' "$PHP_INI" -sed -ri 's/^;?\s*upload_max_filesize\s*=.*/upload_max_filesize = 1024M/' "$PHP_INI" -sed -ri 's/^;?\s*post_max_size\s*=.*/post_max_size = 1024M/' "$PHP_INI" -sed -ri 's/^;?\s*max_execution_time\s*=.*/max_execution_time = 360/' "$PHP_INI" -sed -ri 's/^;?\s*output_buffering\s*=.*/output_buffering = Off/' "$PHP_INI" -sed -ri 's/^;?\s*date\.timezone\s*=.*/date.timezone = UTC/' "$PHP_INI" +# Nextcloud-oriented php.ini tuning +set_ini_value "memory_limit" "512M" +set_ini_value "upload_max_filesize" "1024M" +set_ini_value "post_max_size" "1024M" +set_ini_value "max_execution_time" "360" +set_ini_value "max_input_time" "360" +set_ini_value "output_buffering" "Off" +set_ini_value "date.timezone" "UTC" -# OPcache tuning -if grep -Eq '^\s*;?\s*opcache.enable\s*=' "$PHP_INI"; then - sed -ri 's/^;?\s*opcache.enable\s*=.*/opcache.enable=1/' "$PHP_INI" -else - printf "\nopcache.enable=1\n" >> "$PHP_INI" -fi +# OPcache +set_ini_value "zend_extension" "opcache" +set_ini_value "opcache.enable" "1" +set_ini_value "opcache.enable_cli" "0" +set_ini_value "opcache.interned_strings_buffer" "16" +set_ini_value "opcache.max_accelerated_files" "10000" +set_ini_value "opcache.memory_consumption" "256" +set_ini_value "opcache.save_comments" "1" +set_ini_value "opcache.revalidate_freq" "60" -if grep -Eq '^\s*;?\s*opcache.enable_cli\s*=' "$PHP_INI"; then - sed -ri 's/^;?\s*opcache.enable_cli\s*=.*/opcache.enable_cli=0/' "$PHP_INI" -else - printf "opcache.enable_cli=0\n" >> "$PHP_INI" -fi +# PHP-FPM pool config for Apache's http user +set_fpm_value "user" "http" +set_fpm_value "group" "http" +set_fpm_value "listen" "/run/php-fpm-legacy/php-fpm.sock" +set_fpm_value "listen.owner" "http" +set_fpm_value "listen.group" "http" +set_fpm_value "listen.mode" "0660" +set_fpm_value "pm" "dynamic" +set_fpm_value "pm.max_children" "64" +set_fpm_value "pm.start_servers" "8" +set_fpm_value "pm.min_spare_servers" "4" +set_fpm_value "pm.max_spare_servers" "16" -if grep -Eq '^\s*;?\s*opcache.interned_strings_buffer\s*=' "$PHP_INI"; then - sed -ri 's/^;?\s*opcache.interned_strings_buffer\s*=.*/opcache.interned_strings_buffer=16/' "$PHP_INI" -else - printf "opcache.interned_strings_buffer=16\n" >> "$PHP_INI" -fi +systemctl enable --now "${PHP_FPM_SERVICE}" +systemctl restart "${PHP_FPM_SERVICE}" -if grep -Eq '^\s*;?\s*opcache.max_accelerated_files\s*=' "$PHP_INI"; then - sed -ri 's/^;?\s*opcache.max_accelerated_files\s*=.*/opcache.max_accelerated_files=10000/' "$PHP_INI" -else - printf "opcache.max_accelerated_files=10000\n" >> "$PHP_INI" -fi - -if grep -Eq '^\s*;?\s*opcache.memory_consumption\s*=' "$PHP_INI"; then - sed -ri 's/^;?\s*opcache.memory_consumption\s*=.*/opcache.memory_consumption=256/' "$PHP_INI" -else - printf "opcache.memory_consumption=256\n" >> "$PHP_INI" -fi - -if grep -Eq '^\s*;?\s*opcache.save_comments\s*=' "$PHP_INI"; then - sed -ri 's/^;?\s*opcache.save_comments\s*=.*/opcache.save_comments=1/' "$PHP_INI" -else - printf "opcache.save_comments=1\n" >> "$PHP_INI" -fi - -# php-fpm: use unix socket and Apache-compatible user/group -PHP_FPM_WWW_CONF="/etc/php/php-fpm.d/www.conf" -sed -ri 's|^user\s*=.*|user = http|' "$PHP_FPM_WWW_CONF" -sed -ri 's|^group\s*=.*|group = http|' "$PHP_FPM_WWW_CONF" -sed -ri 's|^listen\s*=.*|listen = /run/php-fpm/php-fpm.sock|' "$PHP_FPM_WWW_CONF" -sed -ri 's|^;?listen.owner\s*=.*|listen.owner = http|' "$PHP_FPM_WWW_CONF" -sed -ri 's|^;?listen.group\s*=.*|listen.group = http|' "$PHP_FPM_WWW_CONF" -sed -ri 's|^;?listen.mode\s*=.*|listen.mode = 0660|' "$PHP_FPM_WWW_CONF" -sed -ri 's|^;?env\[HOSTNAME\].*|env[HOSTNAME] = $HOSTNAME|' "$PHP_FPM_WWW_CONF" || true -sed -ri 's|^;?env\[PATH\].*|env[PATH] = /usr/local/bin:/usr/bin:/bin|' "$PHP_FPM_WWW_CONF" || true -sed -ri 's|^;?env\[TMP\].*|env[TMP] = /tmp|' "$PHP_FPM_WWW_CONF" || true -sed -ri 's|^;?env\[TMPDIR\].*|env[TMPDIR] = /tmp|' "$PHP_FPM_WWW_CONF" || true -sed -ri 's|^;?env\[TEMP\].*|env[TEMP] = /tmp|' "$PHP_FPM_WWW_CONF" || true - -systemctl enable --now php-fpm -systemctl restart php-fpm - -# ====== Redis ====== +# ========================= +# REDIS +# ========================= +log "Configuring Redis" REDIS_CONF="/etc/redis/redis.conf" sed -ri 's|^port .*|port 0|' "$REDIS_CONF" -if grep -Eq '^\s*unixsocket\s+' "$REDIS_CONF"; then - sed -ri 's|^unixsocket\s+.*|unixsocket /run/redis/redis.sock|' "$REDIS_CONF" + +if grep -Eq '^[[:space:]]*unixsocket[[:space:]]+' "$REDIS_CONF"; then + sed -ri "s|^[[:space:]]*unixsocket[[:space:]]+.*|unixsocket ${REDIS_SOCK}|" "$REDIS_CONF" else - printf "\nunixsocket /run/redis/redis.sock\n" >> "$REDIS_CONF" + printf "\nunixsocket %s\n" "$REDIS_SOCK" >> "$REDIS_CONF" fi -if grep -Eq '^\s*unixsocketperm\s+' "$REDIS_CONF"; then - sed -ri 's|^unixsocketperm\s+.*|unixsocketperm 770|' "$REDIS_CONF" +if grep -Eq '^[[:space:]]*unixsocketperm[[:space:]]+' "$REDIS_CONF"; then + sed -ri 's|^[[:space:]]*unixsocketperm[[:space:]]+.*|unixsocketperm 770|' "$REDIS_CONF" else printf "unixsocketperm 770\n" >> "$REDIS_CONF" fi -install -d -m 0755 /run/redis usermod -aG redis http || true systemctl enable --now redis systemctl restart redis -# ====== Apache ====== +# ========================= +# APACHE +# ========================= +log "Configuring Apache" HTTPD_CONF="/etc/httpd/conf/httpd.conf" -# Enable required modules if commented sed -ri 's|^#(LoadModule proxy_module modules/mod_proxy.so)|\1|' "$HTTPD_CONF" sed -ri 's|^#(LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so)|\1|' "$HTTPD_CONF" sed -ri 's|^#(LoadModule rewrite_module modules/mod_rewrite.so)|\1|' "$HTTPD_CONF" sed -ri 's|^#(LoadModule headers_module modules/mod_headers.so)|\1|' "$HTTPD_CONF" sed -ri 's|^#(LoadModule remoteip_module modules/mod_remoteip.so)|\1|' "$HTTPD_CONF" || true -sed -ri 's|^#(LoadModule ssl_module modules/mod_ssl.so)|\1|' "$HTTPD_CONF" || true -sed -ri 's|^#(LoadModule socache_shmcb_module modules/mod_socache_shmcb.so)|\1|' "$HTTPD_CONF" || true -# ServerName to silence warning -if ! grep -Eq '^\s*ServerName\s+' "$HTTPD_CONF"; then +if ! grep -Eq '^[[:space:]]*ServerName[[:space:]]+' "$HTTPD_CONF"; then printf "\nServerName %s\n" "$DOMAIN" >> "$HTTPD_CONF" fi -# Include extra vhosts dir install -d -m 0755 /etc/httpd/conf/extra + if ! grep -Fq "IncludeOptional conf/extra/*.conf" "$HTTPD_CONF"; then printf "\nIncludeOptional conf/extra/*.conf\n" >> "$HTTPD_CONF" fi -# RemoteIP config for local tunnel/proxy cat > /etc/httpd/conf/extra/remoteip-nextcloud.conf < /etc/httpd/conf/extra/nextcloud.conf < ServerName ${DOMAIN} @@ -245,7 +263,6 @@ cat > /etc/httpd/conf/extra/nextcloud.conf < @@ -254,7 +271,7 @@ cat > /etc/httpd/conf/extra/nextcloud.conf < - SetHandler "proxy:unix:/run/php-fpm/php-fpm.sock|fcgi://localhost/" + SetHandler "proxy:unix:/run/php-fpm-legacy/php-fpm.sock|fcgi://localhost/" ErrorLog "/var/log/httpd/nextcloud_error.log" @@ -262,11 +279,14 @@ cat > /etc/httpd/conf/extra/nextcloud.conf < EOF -systemctl enable --now httpd httpd -t +systemctl enable --now httpd systemctl reload httpd -# ====== Download Nextcloud (official upstream tarball) ====== +# ========================= +# DOWNLOAD NEXTCLOUD +# ========================= +log "Downloading official Nextcloud release" TMPDIR="$(mktemp -d)" trap 'rm -rf "$TMPDIR"' EXIT @@ -274,28 +294,27 @@ cd "$TMPDIR" curl -fsSLo nextcloud.tar.bz2 "${NC_TARBALL_URL}" curl -fsSLo nextcloud.tar.bz2.sha512 "${NC_SHA512_URL}" -# Verify sha512 sha512sum -c nextcloud.tar.bz2.sha512 tar -xjf nextcloud.tar.bz2 -# Deploy +log "Deploying Nextcloud" rm -rf "${NC_DIR}" mv nextcloud "${NC_DIR}" -# Ownership & permissions chown -R http:http "${NC_DIR}" "${NC_DATA_DIR}" find "${NC_DIR}" -type d -exec chmod 0750 {} \; find "${NC_DIR}" -type f -exec chmod 0640 {} \; chmod 0750 "${NC_DATA_DIR}" -# Required writable app dirs -install -d -o http -g http -m 0750 "${NC_DIR}/data" install -d -o http -g http -m 0750 "${NC_DIR}/config" install -d -o http -g http -m 0750 "${NC_DIR}/apps" -# ====== Install Nextcloud ====== -sudo -u http php "${NC_DIR}/occ" maintenance:install \ +# ========================= +# INSTALL NEXTCLOUD +# ========================= +log "Running Nextcloud installer" +sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" maintenance:install \ --database "mysql" \ --database-name "${DB_NAME}" \ --database-user "${DB_USER}" \ @@ -304,51 +323,67 @@ sudo -u http php "${NC_DIR}/occ" maintenance:install \ --admin-pass "${ADMIN_PASS}" \ --data-dir "${NC_DATA_DIR}" -# ====== Reverse proxy / HTTPS awareness ====== -sudo -u http php "${NC_DIR}/occ" config:system:set overwrite.cli.url --value="https://${DOMAIN}" -sudo -u http php "${NC_DIR}/occ" config:system:set overwriteprotocol --value="https" +# ========================= +# REVERSE PROXY / HTTPS +# ========================= +log "Applying reverse-proxy and HTTPS settings" +sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set overwrite.cli.url --value="https://${DOMAIN}" +sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set overwriteprotocol --value="https" if [[ "${TRUST_LOCAL_PROXY}" == "true" ]]; then - sudo -u http php "${NC_DIR}/occ" config:system:set trusted_proxies 0 --value="127.0.0.1" - sudo -u http php "${NC_DIR}/occ" config:system:set trusted_proxies 1 --value="::1" + sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set trusted_proxies 0 --value="127.0.0.1" + sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set trusted_proxies 1 --value="::1" if [[ "${REAL_IP_HEADER}" == "CF-Connecting-IP" ]]; then - sudo -u http php "${NC_DIR}/occ" config:system:set forwarded_for_headers 0 --value="HTTP_CF_CONNECTING_IP" + sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set forwarded_for_headers 0 --value="HTTP_CF_CONNECTING_IP" else - sudo -u http php "${NC_DIR}/occ" config:system:set forwarded_for_headers 0 --value="HTTP_X_FORWARDED_FOR" + sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set forwarded_for_headers 0 --value="HTTP_X_FORWARDED_FOR" fi fi -sudo -u http php "${NC_DIR}/occ" config:system:set trusted_domains 1 --value="${DOMAIN}" +sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set trusted_domains 1 --value="${DOMAIN}" -# ====== Memory cache / locking ====== -sudo -u http php "${NC_DIR}/occ" config:system:set memcache.local --value='\OC\Memcache\APCu' -sudo -u http php "${NC_DIR}/occ" config:system:set memcache.locking --value='\OC\Memcache\Redis' -sudo -u http php "${NC_DIR}/occ" config:system:set redis --type=json --value='{"host":"\/run\/redis\/redis.sock","port":0,"timeout":1.5}' +# ========================= +# CACHE / LOCKING +# ========================= +log "Configuring APCu and Redis" +sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set memcache.local --value='\OC\Memcache\APCu' +sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set memcache.locking --value='\OC\Memcache\Redis' +sudo -u http "${PHP_BIN}" "${NC_DIR}/occ" config:system:set redis --type=json --value="{\"host\":\"${REDIS_SOCK}\",\"port\":0,\"timeout\":1.5}" -# ====== Background jobs (recommended) ====== +# ========================= +# CRON +# ========================= +log "Configuring cron background jobs" systemctl enable --now cronie cat > /etc/cron.d/nextcloud <