Files
proxmox-lxc-shell-commands/proxmox-lxc-matomo.sh

565 lines
19 KiB
Bash

#!/usr/bin/env bash
# Matomo LXC Installation Script
# Description: Automated installation of Matomo (Web Analytics) with PHP 8.3, MariaDB, and Nginx
# OS: Debian 12 (Bookworm) - Auto-detected latest version
# Ports: Web: 80
# Repository: https://gitea.ranode.net/proxmox-infra/proxmox-lxc-shell-commands
# Last Updated: 2026-01-13
set -euo pipefail
#################################################################
# Configuration Variables
#################################################################
# Container Configuration
CT_ID=${CT_ID:-27001} # Container ID
CT_HOSTNAME=${CT_HOSTNAME:-"matomo"} # Container hostname
CT_CORES=${CT_CORES:-2} # CPU cores
CT_MEMORY=${CT_MEMORY:-2048} # RAM in MB (increased for better performance)
CT_SWAP=${CT_SWAP:-512} # Swap in MB
CT_DISK_SIZE=${CT_DISK_SIZE:-10} # Root disk size in GB
# Network Configuration
CT_IP=${CT_IP:-"dhcp"} # IP address (dhcp or static like 192.168.2.65/24)
CT_GATEWAY=${CT_GATEWAY:-"192.168.2.1"} # Gateway (required for static IP)
CT_BRIDGE=${CT_BRIDGE:-"vmbr0"} # Network bridge
CT_NAMESERVER=${CT_NAMESERVER:-"8.8.8.8"} # DNS server
# Storage Configuration
CT_STORAGE=${CT_STORAGE:-"local-lvm"} # Storage pool for container
TEMPLATE_STORAGE=${TEMPLATE_STORAGE:-"local"} # Storage pool for templates
# Debian Template (will be auto-detected)
DEBIAN_VERSION="12" # Debian version
TEMPLATE_NAME="" # Auto-detected
# Matomo Configuration
MATOMO_DOMAIN=${MATOMO_DOMAIN:-"matomo.ranode.net"}
MATOMO_DB_NAME="matomo"
MATOMO_DB_USER="matomo"
MATOMO_DB_PASS=${MATOMO_DB_PASS:-"$(openssl rand -base64 16 | tr -d '/+=' | head -c 16)"}
MARIADB_ROOT_PASS=${MARIADB_ROOT_PASS:-"$(openssl rand -base64 16 | tr -d '/+=' | head -c 16)"}
# PHP Configuration
PHP_VERSION="8.3"
# Container Options
CT_ONBOOT=${CT_ONBOOT:-1} # Start on boot (1=yes, 0=no)
CT_UNPRIVILEGED=${CT_UNPRIVILEGED:-1} # Unprivileged container (1=yes, 0=no)
CT_FEATURES=${CT_FEATURES:-"keyctl=1,nesting=1"} # Container features
#################################################################
# Color Output Functions
#################################################################
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
NC='\033[0m' # No Color
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
progress() { echo -e "${MAGENTA}[...]${NC} $1"; }
#################################################################
# Validation Functions
#################################################################
check_root() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
exit 1
fi
}
check_proxmox() {
if ! command -v pct &> /dev/null; then
error "This script must be run on a Proxmox VE host"
exit 1
fi
}
check_container_exists() {
if pct status "$CT_ID" &> /dev/null; then
error "Container ID $CT_ID already exists"
exit 1
fi
}
detect_and_download_template() {
info "Updating template database..."
pveam update 2>&1 || warn "Template update encountered issues"
info "Detecting available Debian ${DEBIAN_VERSION} template..."
local available_template
available_template=$(pveam available --section system 2>/dev/null | grep "debian-${DEBIAN_VERSION}" | grep "standard" | tail -1 | awk '{print $2}')
if [[ -z "$available_template" ]]; then
error "No Debian ${DEBIAN_VERSION} template found"
exit 1
fi
TEMPLATE_NAME="$available_template"
info "Found template: $TEMPLATE_NAME"
if pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE_NAME"; then
success "Template already downloaded"
return 0
fi
progress "Downloading Debian template..."
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE_NAME" || {
error "Failed to download template"
exit 1
}
success "Template downloaded"
}
#################################################################
# Container Creation Functions
#################################################################
create_container() {
info "Creating LXC container $CT_ID ($CT_HOSTNAME)..."
local net_config="name=eth0,bridge=${CT_BRIDGE}"
if [[ "$CT_IP" == "dhcp" ]]; then
net_config="${net_config},ip=dhcp"
else
net_config="${net_config},ip=${CT_IP},gw=${CT_GATEWAY}"
fi
pct create "$CT_ID" \
"${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE_NAME}" \
--hostname "$CT_HOSTNAME" \
--cores "$CT_CORES" \
--memory "$CT_MEMORY" \
--swap "$CT_SWAP" \
--rootfs "${CT_STORAGE}:${CT_DISK_SIZE}" \
--net0 "$net_config" \
--nameserver "$CT_NAMESERVER" \
--onboot "$CT_ONBOOT" \
--unprivileged "$CT_UNPRIVILEGED" \
--features "$CT_FEATURES" \
--ostype debian || {
error "Failed to create container"
exit 1
}
success "Container $CT_ID created"
}
start_container() {
info "Starting container $CT_ID..."
pct start "$CT_ID" || { error "Failed to start container"; exit 1; }
sleep 5
success "Container started"
}
configure_autologin() {
info "Configuring automatic console login..."
pct exec "$CT_ID" -- bash -c "mkdir -p /etc/systemd/system/container-getty@1.service.d"
pct exec "$CT_ID" -- bash -c 'cat > /etc/systemd/system/container-getty@1.service.d/override.conf << EOF
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM
EOF'
pct exec "$CT_ID" -- bash -c "systemctl daemon-reload"
success "Autologin configured"
}
#################################################################
# Application Installation Functions
#################################################################
install_sury_php_repo() {
info "Adding Sury PHP repository for PHP ${PHP_VERSION}..."
# Install prerequisites
pct exec "$CT_ID" -- bash -c "apt-get update -qq"
pct exec "$CT_ID" -- bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq apt-transport-https lsb-release ca-certificates curl gnupg"
# Add Sury GPG key and repository
pct exec "$CT_ID" -- bash -c "curl -sSL https://packages.sury.org/php/apt.gpg | gpg --dearmor -o /usr/share/keyrings/sury-php.gpg"
pct exec "$CT_ID" -- bash -c "echo 'deb [signed-by=/usr/share/keyrings/sury-php.gpg] https://packages.sury.org/php/ bookworm main' > /etc/apt/sources.list.d/sury-php.list"
# Update package lists
pct exec "$CT_ID" -- bash -c "apt-get update -qq"
success "Sury PHP repository added"
}
install_packages() {
info "Installing required packages..."
# Install MariaDB
pct exec "$CT_ID" -- bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq mariadb-server mariadb-client"
# Install Nginx
pct exec "$CT_ID" -- bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nginx"
# Install PHP and extensions for Matomo
pct exec "$CT_ID" -- bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
php${PHP_VERSION}-fpm \
php${PHP_VERSION}-mysql \
php${PHP_VERSION}-curl \
php${PHP_VERSION}-gd \
php${PHP_VERSION}-cli \
php${PHP_VERSION}-xml \
php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-intl \
php${PHP_VERSION}-zip \
php${PHP_VERSION}-ldap \
php${PHP_VERSION}-gmp \
php${PHP_VERSION}-opcache \
unzip curl wget ca-certificates"
success "Packages installed"
}
configure_mariadb() {
info "Configuring MariaDB..."
# Start MariaDB
pct exec "$CT_ID" -- bash -c "systemctl start mariadb"
pct exec "$CT_ID" -- bash -c "systemctl enable mariadb"
# Secure MariaDB and create database
pct exec "$CT_ID" -- bash -c "mysql -u root <<MYSQL
ALTER USER 'root'@'localhost' IDENTIFIED BY '${MARIADB_ROOT_PASS}';
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
CREATE DATABASE ${MATOMO_DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER '${MATOMO_DB_USER}'@'localhost' IDENTIFIED BY '${MATOMO_DB_PASS}';
GRANT ALL PRIVILEGES ON ${MATOMO_DB_NAME}.* TO '${MATOMO_DB_USER}'@'localhost';
FLUSH PRIVILEGES;
MYSQL"
success "MariaDB configured"
}
install_matomo() {
info "Downloading and installing Matomo..."
# Download latest Matomo
pct exec "$CT_ID" -- bash -c "cd /tmp && wget -q https://builds.matomo.org/matomo-latest.zip"
# Extract to web directory
pct exec "$CT_ID" -- bash -c "unzip -q /tmp/matomo-latest.zip -d /var/www/"
pct exec "$CT_ID" -- bash -c "chown -R www-data:www-data /var/www/matomo"
pct exec "$CT_ID" -- bash -c "chmod -R 755 /var/www/matomo"
# Create tmp directory with proper permissions
pct exec "$CT_ID" -- bash -c "mkdir -p /var/www/matomo/tmp"
pct exec "$CT_ID" -- bash -c "chown -R www-data:www-data /var/www/matomo/tmp"
pct exec "$CT_ID" -- bash -c "chmod -R 777 /var/www/matomo/tmp"
# Cleanup
pct exec "$CT_ID" -- bash -c "rm /tmp/matomo-latest.zip"
success "Matomo installed"
}
configure_php() {
info "Configuring PHP-FPM with optimized settings..."
# Create optimized PHP-FPM pool configuration for Matomo
pct exec "$CT_ID" -- bash -c "cat > /etc/php/${PHP_VERSION}/fpm/pool.d/matomo.conf <<'EOF'
[matomo]
user = www-data
group = www-data
listen = /run/php/php-fpm-matomo.sock
listen.owner = www-data
listen.group = www-data
; Process Manager Settings
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
; Slow log for debugging
slowlog = /var/log/php-fpm-matomo-slow.log
request_slowlog_timeout = 3s
; Logging
php_admin_value[error_log] = /var/log/php-matomo-error.log
php_admin_flag[log_errors] = on
; Matomo recommended settings
php_value[memory_limit] = 256M
php_value[max_execution_time] = 300
php_value[upload_max_filesize] = 64M
php_value[post_max_size] = 64M
; OPcache settings for performance
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.memory_consumption] = 256
php_admin_value[opcache.interned_strings_buffer] = 16
php_admin_value[opcache.max_accelerated_files] = 10000
php_admin_value[opcache.revalidate_freq] = 2
php_admin_value[opcache.fast_shutdown] = 1
; Realpath cache for performance
php_admin_value[realpath_cache_size] = 4096k
php_admin_value[realpath_cache_ttl] = 600
EOF"
# Restart PHP-FPM
pct exec "$CT_ID" -- bash -c "systemctl restart php${PHP_VERSION}-fpm"
success "PHP-FPM configured with optimizations"
}
configure_nginx() {
info "Configuring Nginx..."
pct exec "$CT_ID" -- bash -c "cat > /etc/nginx/sites-available/matomo <<'NGINX'
server {
listen 80;
server_name ${MATOMO_DOMAIN} _;
root /var/www/matomo;
index index.php index.html;
# Logs
access_log /var/log/nginx/matomo-access.log;
error_log /var/log/nginx/matomo-error.log;
# Security headers
add_header X-Frame-Options \"SAMEORIGIN\" always;
add_header X-Content-Type-Options \"nosniff\" always;
add_header X-XSS-Protection \"1; mode=block\" always;
add_header X-Robots-Tag \"noindex\" always;
location / {
try_files \$uri \$uri/ =404;
}
# PHP handling for Matomo
location ~ ^/(index|matomo|piwik|js/index|plugins/HeatmapSessionRecording/configs)\\.php\$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm-matomo.sock;
fastcgi_read_timeout 300;
}
# Deny access to other PHP files
location ~* ^.+\\.php\$ {
deny all;
return 403;
}
# Static files caching
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)\$ {
expires 1y;
add_header Cache-Control \"public, immutable\";
}
# Deny access to sensitive files
location ~ /\\. {
deny all;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
log_not_found off;
access_log off;
}
}
NGINX"
# Enable site
pct exec "$CT_ID" -- bash -c "ln -sf /etc/nginx/sites-available/matomo /etc/nginx/sites-enabled/"
pct exec "$CT_ID" -- bash -c "rm -f /etc/nginx/sites-enabled/default"
# Test and reload
pct exec "$CT_ID" -- bash -c "nginx -t"
pct exec "$CT_ID" -- bash -c "systemctl reload nginx"
success "Nginx configured"
}
setup_cron() {
info "Setting up Matomo cron job..."
pct exec "$CT_ID" -- bash -c "cat > /etc/cron.d/matomo <<'CRON'
# Matomo archive processing
5 * * * * www-data /usr/bin/php /var/www/matomo/console core:archive --url=http://localhost > /var/log/matomo-archive.log 2>&1
CRON"
success "Cron job configured"
}
add_container_notes() {
info "Adding container notes..."
local container_ip
if [[ "$CT_IP" == "dhcp" ]]; then
sleep 3
container_ip=$(pct exec "$CT_ID" -- hostname -I 2>/dev/null | awk '{print $1}')
else
container_ip="${CT_IP%/*}"
fi
local notes="Matomo (Web Analytics)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CONTAINER DETAILS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Container ID: ${CT_ID}
Hostname: ${CT_HOSTNAME}
IP Address: ${container_ip}
CPU Cores: ${CT_CORES}
Memory: ${CT_MEMORY}MB
Disk Size: ${CT_DISK_SIZE}GB
APPLICATION ACCESS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Web UI: http://${container_ip}
Domain: https://${MATOMO_DOMAIN}
Setup Wizard: http://${container_ip}/index.php
DATABASE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Host: localhost
Database: ${MATOMO_DB_NAME}
User: ${MATOMO_DB_USER}
Password: ${MATOMO_DB_PASS}
Root Password: ${MARIADB_ROOT_PASS}
SERVICE MANAGEMENT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Nginx: systemctl status nginx
PHP-FPM: systemctl status php${PHP_VERSION}-fpm
MariaDB: systemctl status mariadb
PATHS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Web Root: /var/www/matomo
Config: /var/www/matomo/config/config.ini.php
Logs: /var/log/nginx/matomo-*.log
PHP Slow Log: /var/log/php-fpm-matomo-slow.log
CONTAINER MANAGEMENT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Enter: pct enter ${CT_ID}
Start: pct start ${CT_ID}
Stop: pct stop ${CT_ID}
SETUP INSTRUCTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Access http://${container_ip} in browser
2. Follow the setup wizard
3. Use database credentials above
4. Configure Traefik for HTTPS access
PERFORMANCE NOTES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- OPcache enabled (256MB)
- PHP-FPM optimized pool
- Slow request logging enabled"
pct set "$CT_ID" -description "$notes" 2>/dev/null || warn "Failed to add notes"
success "Container notes added"
}
#################################################################
# Information Display Functions
#################################################################
display_info() {
local container_ip
if [[ "$CT_IP" == "dhcp" ]]; then
container_ip=$(pct exec "$CT_ID" -- hostname -I | awk '{print $1}')
else
container_ip="${CT_IP%/*}"
fi
echo ""
echo "================================================================="
success "Matomo LXC Container Setup Complete!"
echo "================================================================="
echo ""
echo "Container Details:"
echo " Container ID: $CT_ID"
echo " Hostname: $CT_HOSTNAME"
echo " IP Address: $container_ip"
echo ""
echo "Application Access:"
echo " Web UI: http://${container_ip}"
echo " Setup Wizard: http://${container_ip}/index.php"
echo ""
echo "Database Credentials (for setup wizard):"
echo " Host: localhost"
echo " Database: $MATOMO_DB_NAME"
echo " User: $MATOMO_DB_USER"
echo " Password: $MATOMO_DB_PASS"
echo ""
echo "MariaDB Root Password: $MARIADB_ROOT_PASS"
echo ""
echo "Next Steps:"
echo " 1. Open http://${container_ip} in your browser"
echo " 2. Complete the Matomo setup wizard"
echo " 3. Configure Traefik reverse proxy for HTTPS"
echo ""
echo "Performance Features:"
echo " - OPcache enabled with 256MB memory"
echo " - PHP-FPM pool optimized for Matomo"
echo " - Slow request logging enabled"
echo ""
echo "================================================================="
}
#################################################################
# Main Execution
#################################################################
main() {
info "Starting Matomo LXC container creation..."
echo ""
# Pre-flight checks
check_root
check_proxmox
check_container_exists
detect_and_download_template
# Create and configure container
create_container
start_container
configure_autologin
# Install and configure application
install_sury_php_repo # NEW: Add Sury repo first
install_packages
configure_mariadb
install_matomo
configure_php
configure_nginx
setup_cron
# Add container notes
add_container_notes
# Display information
display_info
}
# Run main function
main "$@"