Markus Michalski FAQ

Server Tools: Technical Details

Technical details of Server Tools: layers and modules, program flow, webserver dispatch, generated Apache and Nginx configurations, logrotate, version detection, error handling, tests and CI.

Deutsche Version

Back to overview


Technical Details

Structure

Server Tools consists of an entry script and libraries following the "building blocks" pattern: small functions each do one task, higher-level functions combine them into flows with validation, prompt, rollback and audit log.

graph TD
    A["bin/server-tools"] --> B["core.sh, config.sh, security.sh"]
    A --> C["backup.sh, install.sh"]
    A --> D["database.sh, vhost.sh, ssl.sh, cron.sh, firewall.sh, fail2ban.sh, log.sh, status.sh, user.sh"]
    A --> E["cli.sh, menu.sh"]
    D --> F["_ws_dispatch"]
    F --> G["webserver/apache.sh"]
    F --> H["webserver/nginx.sh"]
File Purpose
bin/server-tools Entry: determine library path, evaluate flags, dispatch the command
lib/core.sh Logging, die, confirm, dependency check, version detection
lib/config.sh Defaults, load and validate config, --config output
lib/security.sh Input validation, escaping, audit log, password generation, safe writing
lib/backup.sh Backups before deletion, backup overview
lib/install.sh install and uninstall
lib/database.sh MySQL operations
lib/vhost.sh VHost flows, logrotate, audit
lib/webserver/apache.sh, nginx.sh Configuration generation and site mechanics per webserver
lib/ssl.sh, cron.sh, firewall.sh, fail2ban.sh, log.sh, status.sh, user.sh Feature modules
lib/cli.sh Argument handling and dispatch of the CLI commands
lib/menu.sh Interactive menu
conf/server-tools.conf.example Config template

Each library starts with a guard (_<NAME>_SOURCED) that prevents loading it twice.

Program flow

  1. bin/server-tools sets set -euo pipefail.
  2. It looks for the libraries in ../lib relative to the binary, then in /usr/local/lib/server-tools.
  3. It loads all libraries in a fixed order: core, config, security, backup, database, vhost, ssl, cron, status, log, firewall, fail2ban, user, install, cli, menu.
  4. The global flags --yes and --quiet are removed from the argument list. --yes sets ST_AUTO_CONFIRM=true.
  5. The first parameter selects the command. Most branches run check_root, load_config, check_dependencies and the CLI function in turn.
  6. Without an argument the menu starts.

core.sh determines the version from the file .version next to the libraries, otherwise via git describe --tags --always in the library directory, otherwise dev.

Error handling

The entry script runs with set -e. A function that ends with status 1 therefore ends the whole call with exit code 1. The menu catches this: every action runs there with || true, an error leads back to the menu.

log_info, log_warn and log_error write messages to stderr. die writes an error message and exits with 1.

Webserver dispatch

vhost.sh calls webserver-dependent functions via _ws_dispatch <name> <arguments>. Depending on ST_WEBSERVER this becomes apache_<name> or nginx_<name>:

Name Purpose
vhost_config_path Path of the configuration file
vhost_exists, vhost_enabled Existence, activation
generate_vhost_config, generate_proxy_config, generate_redirect_config Generate configuration
generate_www_redirect_snippet, generate_https_redirect_snippet Redirect blocks
get_vhost_docroot, get_vhost_php_version, get_vhost_type, get_vhost_backend, websocket_enabled Read values from a configuration
change_php_version_in_config Replace the socket path
enable_site, disable_site, reload Enable, disable, test and reload
enable_modules_php, enable_modules_proxy, rewrite_module_ensure Apache modules, no effect on Nginx

Type, DocumentRoot, PHP version and backend of a VHost are read by the tool from the configuration file, there is no separate database.

Generated configurations

The examples show the templates with example.com, an alias and PHP 8.3.

Apache, PHP VHost

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/example.com/html

    <Directory /var/www/example.com/html>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    <FilesMatch \.php$>
        SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
    </FilesMatch>

    # Security Headers
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"

    # Forwarded Headers
    RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
    RequestHeader set X-Forwarded-Port expr=%{SERVER_PORT}

    ServerSignature Off

    ErrorLog /var/www/example.com/logs/error.log
    CustomLog /var/www/example.com/logs/access.log combined
</VirtualHost>

Apache, reverse proxy

Instead of DocumentRoot, Directory and FilesMatch the block contains:

    ProxyPreserveHost On
    ProxyPass / http://localhost:3000/
    ProxyPassReverse / http://localhost:3000/

With --websocket a block RewriteEngine On follows with two RewriteCond (Upgrade: websocket, Connection: upgrade) and RewriteRule ^/?(.*) ws://localhost:3000/$1 [P,L]. Security and forwarded headers as well as the log paths correspond to the PHP VHost.

Nginx, PHP VHost

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example.com/html;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.ht {
        deny all;
    }

    # Security Headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    access_log /var/www/example.com/logs/access.log;
    error_log /var/www/example.com/logs/error.log;
}

The Nginx proxy block contains a location / with proxy_pass, proxy_http_version 1.1, proxy_set_header Host $host (except with --preserve-host false), X-Real-IP, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Port, with --websocket additionally Upgrade and Connection "upgrade". The block structure must end with a single line }, because redirect blocks are inserted before this line.

Logrotate

/etc/logrotate.d/vhost-<domain> contains for /var/www/<domain>/logs/*.log: weekly, missingok, rotate <ST_LOGROTATE_ROTATE>, maxage <ST_LOGROTATE_DAYS>, compress, delaycompress, notifempty, create 640 www-data www-data, sharedscripts and a postrotate that reloads the webserver if its PID file exists. Which webserver that is, is fixed in the text when it is generated.

Files the tool creates

Path Purpose
/etc/server-tools/config Configuration
<ST_CREDENTIAL_DIR>/<db>.txt Credentials per database
<ST_BACKUP_DIR>/*.tar.gz Backups before deletion
<ST_DB_BACKUP_DIR>/*.sql.gz Database dumps
<ST_AUDIT_LOG> Audit log
/etc/logrotate.d/vhost-<domain> Logrotate per VHost
/var/www/<domain>/logs/, html/ Directories of a domain
/etc/cron.d/<name> Cron jobs
/etc/cron.d/ssl-renewal, /usr/local/bin/ssl-renewal.sh, /var/log/ssl-renewal.log Certificate renewal
/var/www/<domain>/.ssh/authorized_keys Keys of the domain user

Development and tests

make setup-tests   # clone BATS and helper libraries to tests/libs
make test          # all tests
make lint          # ShellCheck
make format-check  # shfmt -i 4 -bn -ci
make check         # lint, format-check and test

The tests are located in tests/unit/*.bats (more than 500 tests in 17 files). Each test runs in its own temporary directory, system commands are replaced by mocks (mock_command in tests/test_helper.bash). No test touches the real system.

On push and pull request to main, the CI runs three jobs: ShellCheck and shfmt, the BATS tests and an install smoke test that runs make install, make uninstall, ./bin/server-tools install and uninstall against the real /usr/local paths.

Code style: Bash 4.4 or newer, four spaces of indentation, set -euo pipefail only in the entry script, commits following Conventional Commits. The guidelines are in CONTRIBUTING.md of the repository.