diff --git a/.github/workflows/build-and-publish-matomo.yml b/.github/workflows/build-and-publish-matomo.yml new file mode 100644 index 0000000..a497272 --- /dev/null +++ b/.github/workflows/build-and-publish-matomo.yml @@ -0,0 +1,22 @@ +--- +name: Build & Publish Matomo + +on: # yamllint disable-line rule:truthy + schedule: + - cron: "0 0 1,15 * *" # Every 2 weeks + push: + branches: + - main + paths: + - .github/workflows/build-and-publish-matomo.yml + - .github/workflows/internal-build-and-publish.yml + - 'common/**' + - 'matomo/**' + workflow_dispatch: + +jobs: + build-and-publish: + uses: ./.github/workflows/internal-build-and-publish.yml + with: + image: matomo + version: latest diff --git a/matomo/Dockerfile b/matomo/Dockerfile new file mode 100644 index 0000000..d8399b7 --- /dev/null +++ b/matomo/Dockerfile @@ -0,0 +1,26 @@ +FROM matomo:fpm-alpine as final + +# Install packages +# hadolint ignore=DL3018 +RUN apk add --no-cache \ + nginx \ + supervisor + +# Copy configuration files +# - nginx +COPY common/config/nginx/ /etc/nginx/ +COPY matomo/config/nginx/http.d/ /etc/nginx/http.d/ +# - supervisor +COPY common/config/supervisor/supervisord.conf /etc/supervisor/ +COPY common/config/supervisor/conf.d/nginx.conf /etc/supervisor/conf.d/ +COPY matomo/config/supervisor/conf.d/ /etc/supervisor/conf.d/ +# - init +COPY matomo/scripts/ / + +# Clean Up +WORKDIR /var/www/html +EXPOSE 80 + +# Start supervisord by default +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["serve"] diff --git a/matomo/README.md b/matomo/README.md new file mode 100644 index 0000000..07efee0 --- /dev/null +++ b/matomo/README.md @@ -0,0 +1,48 @@ +# Matomo Docker + +šŸ³ Variant of the official Matomo docker image, tweaked to match our preferences. + +Available images: +- `latest`: normal version. + +## Commands + +Docker will default to the `serve` command. + +- `serve`: starts all services, such as `nginx`. +- `scheduler`: process scheduled tasks (cron jobs). +- `init`: initialize the project, such as executing migrations. +- fallback: execute the provided command. + +# Initialization + +Using an init container (that invokes `init` command above), the container will be prepared for use. This entails: + +- Running through the Matomo onboarding wizard. +- Installing some plugins. +- Tweaking some base Matomo options. +- Applying the environment variables to the Matomo config. + +# Configuration + +This image can be configured using the following environment variables: + +| Environment Key | Applied | Description | +------------------|---------|-------------- +| MATOMO_USERNAME | Bootstrap only | Admin user username | +| MATOMO_PASSWORD | Bootstrap only | Admin user password | +| MATOMO_EMAIL | Bootstrap only | Admin user email address | +| MATOMO_HOST | Bootstrap only | Hostname of the first website that'll be created | +| MATOMO_WEBSITE_NAME | Bootstrap only | Name of the first website that'll be created | +| MATOMO_SMTP_HOST | Every run | SMTP server hostname | +| MATOMO_SMTP_PORT_NUMBER | Every run | SMTP server port number | +| MATOMO_SMTP_USER | Every run | SMTP server username | +| MATOMO_SMTP_PASSWORD | Every run | SMTP server password | +| MATOMO_SMTP_ENCRYPTION | Every run | SMTP server encryption | +| MATOMO_NOREPLY_NAME | Every run | Sender name for sent emails | +| MATOMO_NOREPLY_ADDRESS | Every run | Sender email address for sent emails | +| MATOMO_DATABASE_HOST | Every run | Database hostname | +| MATOMO_DATABASE_PORT_NUMBER | Every run | Database port number | +| MATOMO_DATABASE_NAME | Every run | Database name | +| MATOMO_DATABASE_USER | Every run | Database user | +| MATOMO_DATABASE_PASSWORD | Every run | Database password | diff --git a/matomo/config/nginx/http.d/default.conf b/matomo/config/nginx/http.d/default.conf new file mode 100644 index 0000000..3aa097d --- /dev/null +++ b/matomo/config/nginx/http.d/default.conf @@ -0,0 +1,23 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + # Redirect root folder + location = /metrics { + access_log off; + log_not_found off; + return 302 /metrics/; + } + + # Pass along to matomo server block + location ^~/metrics/ { + access_log off; + log_not_found off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-NginX-Proxy true; + proxy_set_header X-Forwarded-Uri /metrics; + proxy_pass http://127.0.0.1:26059/; + } +} diff --git a/matomo/config/nginx/http.d/matomo.conf b/matomo/config/nginx/http.d/matomo.conf new file mode 100644 index 0000000..548653a --- /dev/null +++ b/matomo/config/nginx/http.d/matomo.conf @@ -0,0 +1,64 @@ +server { + listen 127.0.0.1:26059; + + # Paths + root /var/www/html; + index index.php; + + # Security + add_header Referrer-Policy origin always; # make sure outgoing links don't show the URL to the Matomo instance + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # serve PHP files + location ~ ^/(index|matomo|piwik|js/index|plugins/HeatmapSessionRecording/configs)\.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass 127.0.0.1:9000; + } + + ## deny access to all other .php files + location ~* ^.+\.php$ { + deny all; + return 403; + } + + ## serve all other files normally + location / { + try_files $uri $uri/ =404; + } + + ## disable all access to the following directories + location ~ ^/(config|tmp|core|lang) { + deny all; + return 403; + } + + location ~ /\.ht { + deny all; + return 403; + } + + location ~ js/container_.*_preview\.js$ { + expires off; + add_header Cache-Control 'private, no-cache, no-store'; + } + + location ~ \.(gif|ico|jpg|png|svg|js|css|htm|html|mp3|mp4|wav|ogg|avi|ttf|eot|woff|woff2|json)$ { + allow all; + ## Cache images,CSS,JS and webfonts for an hour + ## Increasing the duration may improve the load-time, but may cause old files to show after an Matomo upgrade + expires 1h; + add_header Pragma public; + add_header Cache-Control "public"; + } + + location ~ ^/(libs|vendor|plugins|misc|node_modules) { + deny all; + return 403; + } + + ## properly display textfiles in root directory + location ~/(.*\.md|LEGALNOTICE|LICENSE) { + default_type text/plain; + } +} diff --git a/matomo/config/supervisor/conf.d/php-fpm.conf b/matomo/config/supervisor/conf.d/php-fpm.conf new file mode 100644 index 0000000..81ed7fe --- /dev/null +++ b/matomo/config/supervisor/conf.d/php-fpm.conf @@ -0,0 +1,10 @@ +[program:php-fpm] +command=php-fpm -F + +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +autorestart=false +startretries=0 diff --git a/matomo/scripts/bootstrap.sh b/matomo/scripts/bootstrap.sh new file mode 100755 index 0000000..3fa8752 --- /dev/null +++ b/matomo/scripts/bootstrap.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env sh + +set -euo pipefail + +config_file="/var/www/html/config/config.ini.php" + +start_server() { + echo "Booting up temporary serverā€¦" + + # Start services + /entrypoint.sh php-fpm & + nginx + sleep 2 +} + +exec_sql_query() { + php -r " + \$conn = new PDO(\"mysql:host=${MATOMO_DATABASE_HOST}:${MATOMO_DATABASE_PORT_NUMBER};dbname=${MATOMO_DATABASE_NAME}\", \"${MATOMO_DATABASE_USER}\", \"${MATOMO_DATABASE_PASSWORD}\"); + \$conn->exec(\"$1\");" +} + +# Run through the installation wizard +matomo_pass_wizard() { + local wizard_url cookie_file curl_output curl_opts curl_data_opts + + # Start wizard + wizard_url="http://127.0.0.1:26059/" + cookie_file="/tmp/cookie$RANDOM" + curl_opts="--location --silent --cookie $cookie_file --cookie-jar $cookie_file -o /dev/null" + curl $curl_opts $wizard_url + + echo "Step 1: System check" + curl_data_opts="--data-urlencode action=systemCheck" + curl $curl_opts $curl_data_opts $wizard_url + + echo "Step 2: Database setup" + curl_data_opts="--data-urlencode action=databaseSetup\ + --data-urlencode host=${MATOMO_DATABASE_HOST}:${MATOMO_DATABASE_PORT_NUMBER}\ + --data-urlencode username=${MATOMO_DATABASE_USER}\ + --data-urlencode password=${MATOMO_DATABASE_PASSWORD}\ + --data-urlencode dbname=${MATOMO_DATABASE_NAME}\ + --data-urlencode tables_prefix=matomo_\ + --data-urlencode adapter=PDO\\MYSQL" + curl $curl_opts $curl_data_opts $wizard_url + + echo "Step 3: Create tables (SKIP)" + + echo "Step 4: Setup super-user" + curl_data_opts="--data-urlencode action=setupSuperUser\ + --data-urlencode module=Installation\ + --data-urlencode login=${MATOMO_USERNAME}\ + --data-urlencode password=${MATOMO_PASSWORD}\ + --data-urlencode password_bis=${MATOMO_PASSWORD}\ + --data-urlencode email=${MATOMO_EMAIL}" + curl $curl_opts $curl_data_opts $wizard_url + + echo "Step 5: Setup first tracking website" + curl_data_opts="--data-urlencode action=firstWebsiteSetup\ + --data-urlencode module=Installation\ + --data-urlencode siteName=${MATOMO_WEBSITE_NAME}\ + --data-urlencode url=${MATOMO_HOST}\ + --data-urlencode timezone=UTC" + curl $curl_opts $curl_data_opts $wizard_url + + echo "Step 6: Tracking code" + curl_data_opts="--data-urlencode action=trackingCode\ + --data-urlencode module=Installation" + curl $curl_opts $curl_data_opts $wizard_url + + echo "Step 7: Finish installation" + curl_data_opts="--data-urlencode action=finished\ + --data-urlencode module=Installation" + curl $curl_opts $curl_data_opts $wizard_url +} + +# Install & activate the given plugin +matomo_install_plugin() { + echo "Installing plugin '$1'ā€¦" + + # Download plugin + curl --location --silent -o "/tmp/$1.zip" "https://plugins.matomo.org/api/2.0/plugins/$1/download/latest" + unzip -q "/tmp/$1.zip" -d /var/www/html/plugins + chown -R www-data:www-data "/var/www/html/plugins/$1" + rm "/tmp/$1.zip" + + # Activate plugin + ./console plugin:activate $1 +} + +# Adapt some general settings (stored in the DB) to match our preferences +matomo_tweak_options() { + echo "Tweak some optionsā€¦" + + # General settings + exec_sql_query "REPLACE INTO matomo_option VALUES + ('enableBrowserTriggerArchiving', '0', 1), + ('PrivacyManager.ipAddressMaskLength', '2', 0), + ('PrivacyManager.ipAnonymizerEnabled', '1', 0), + ('PrivacyManager.useAnonymizedIpForVisitEnrichment', '1', 0), + ('PrivacyManager.anonymizeOrderId', '1', 0), + ('PrivacyManager.anonymizeUserId', '1', 0), + ('PrivacyManager.doNotTrackEnabled', '1', 0), + ('delete_logs_enable', '1', 0), + ('delete_logs_older_than', '180', 0), + ('delete_reports_enable', '1', 0), + ('delete_reports_keep_basic_metrics', '1', 0), + ('delete_reports_keep_month_reports', '1', 0), + ('delete_reports_keep_year_reports', '1', 0), + ('delete_reports_older_than', '12', 0);" + + # Configure ProtectTrackID + exec_sql_query "INSERT INTO matomo_plugin_setting VALUES + ('ProtectTrackID', 'baseSetting', 'ABCDEFGHIJKLMNOPijklmnopqrstuvxwyz12345', 0, '', DEFAULT), + ('ProtectTrackID', 'saltSetting', '$(openssl rand -hex 20)', 0, '', DEFAULT), + ('ProtectTrackID', 'lengthSetting', '20', 0, '', DEFAULT);" + + # Configure TrackingSpamPrevention + exec_sql_query "INSERT INTO matomo_plugin_setting VALUES + ('TrackingSpamPrevention', 'block_clouds', '1', 0, '', DEFAULT), + ('TrackingSpamPrevention', 'block_headless', '1', 0, '', DEFAULT);" +} + +# Update configuration settings stored in the config.ini.php file +# Will always be run, to reflect changes to the ENV +matomo_update_config() { + # Database settings + ./console config:set \ + "database.host=\"$MATOMO_DATABASE_HOST:$MATOMO_DATABASE_PORT_NUMBER\"" \ + "database.username=\"$MATOMO_DATABASE_USER\"" \ + "database.password=\"$MATOMO_DATABASE_PASSWORD\"" \ + "database.dbname=\"$MATOMO_DATABASE_NAME\"" \ + "General.enable_load_data_infile=0" + + # Handle proxy correctly + ./console config:set \ + 'General.force_ssl=1' \ + 'General.assume_secure_protocol=1' \ + 'General.proxy_uri_header=1' \ + 'General.enable_trusted_host_check=0' \ + 'General.trusted_hosts=[]' \ + 'General.proxy_client_headers=[]' \ + 'General.proxy_client_headers[]="HTTP_X_FORWARDED_FOR"' \ + 'General.proxy_host_headers=[]' \ + 'General.proxy_host_headers[]="HTTP_X_FORWARDED_HOST"' + + # Email settings + ./console config:set \ + "General.noreply_email_address=\"$MATOMO_NOREPLY_ADDRESS\"" \ + "General.noreply_email_name=\"$MATOMO_NOREPLY_NAME\"" \ + 'mail.transport="smtp"' \ + "mail.port=\"$MATOMO_SMTP_PORT_NUMBER\"" \ + "mail.host=\"$MATOMO_SMTP_HOST\"" \ + 'mail.type="Login"' \ + "mail.username=\"$MATOMO_SMTP_USER\"" \ + "mail.password=\"$MATOMO_SMTP_PASSWORD\"" \ + "mail.encryption=\"$MATOMO_SMTP_ENCRYPTION\"" +} + +# Only initialize if we do NOT have a config file +if [ ! -f "$config_file" ]; then + start_server + matomo_pass_wizard + matomo_install_plugin BotTracker + matomo_install_plugin JsTrackerForceAsync + matomo_install_plugin Provider + matomo_install_plugin ProtectTrackID + matomo_install_plugin TrackingSpamPrevention + matomo_install_plugin TreemapVisualization + matomo_tweak_options +fi + +# Always update general config +matomo_update_config diff --git a/matomo/scripts/docker-entrypoint.sh b/matomo/scripts/docker-entrypoint.sh new file mode 100755 index 0000000..c94c242 --- /dev/null +++ b/matomo/scripts/docker-entrypoint.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env sh + +set -euo pipefail + + +## Bootstrap + + +if [ ! -e matomo.php ]; then + tar cf - --one-file-system -C /usr/src/matomo . | tar xf - + chown -R www-data:www-data . +fi + + +## Commands ## + + +# Helper to serve all services +if [ "$1" = 'serve' ]; then + exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf + +# Helper to run cron jobs +elif [ "$1" = 'scheduler' ]; then + exec php console core:archive --verbose --no-interaction + +# Init script, for use in initContainers (for example) +elif [ "$1" = 'init' ]; then + exec /bootstrap.sh + +# Fallback: just execute given command +else + exec "$@" +fi