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.
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
bin/server-toolssetsset -euo pipefail.- It looks for the libraries in
../librelative to the binary, then in/usr/local/lib/server-tools. - It loads all libraries in a fixed order:
core,config,security,backup,database,vhost,ssl,cron,status,log,firewall,fail2ban,user,install,cli,menu. - The global flags
--yesand--quietare removed from the argument list.--yessetsST_AUTO_CONFIRM=true. - The first parameter selects the command. Most branches run
check_root,load_config,check_dependenciesand the CLI function in turn. - 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.