Tools: I built a Drupal installer that tells you if your site is safe to ship (2026)

Tools: I built a Drupal installer that tells you if your site is safe to ship (2026)

The problem nobody talks about

What I built

The line that started it

What it checks

The honest score

What I learned building it

The stack

Try it Most Drupal installers stop at "it's running." You follow the tutorial. You provision a server. You install Drupal. The site loads. You think you're done. Your trusted_host_patterns might be unconfigured — meaning any Host header gets accepted, opening you to cache poisoning. Your private file path might be unset — meaning files uploaded to private:// are publicly accessible via direct URL. Your Redis might be running but not actually wired as Drupal's cache backend. Your TLS cert might have 12 days left and you have no alert for it. The site looks fine. The vulnerabilities are invisible. Every Drupal developer I know has shipped something that looked fine and wasn't. Including me. Actools is a Drupal 11 installer for Hetzner VPS. One command, complete stack — Caddy 2, PHP 8.3-FPM, MariaDB 11.4, Redis 7, XeLaTeX PDF worker, automated backups. That part exists. There are other installers. What's different is this: 25 checks. Four categories. A score out of 10. Fix commands attached to every finding. Not a dashboard. Not a Drupal module. A CLI tool that tells you the truth about your own server. At the top of audit.sh there's a comment: That's the whole product philosophy in three lines. Written before a single check existed. Fresh install scores 6/10. Not 10. Not "everything is perfect." Six. Because on a brand new server there's no backup yet, Redis isn't wired as the cache backend by default, and a few medium-priority items need operator attention. That's honest. A tool that gives you 10/10 on a fresh install is lying to you. The score goes up as you fix things. Run actools backup. Wire Redis. Pin your Docker images. Each fix moves the needle. Shell escaping across Docker layers is genuinely hard. Injecting $settings['trusted_host_patterns'] into a PHP file through bash → docker exec → bash → heredoc — every layer eats escape characters differently. I went through printf, echo -e, inline quoting, and eventually landed on the only solution that actually works: Quoted heredoc inside the container. Write to temp file. Append. Delete. No escaping war. Three AI systems independently converged on this exact pattern when I described the problem. That's usually a sign it's right. Idempotency checks need to be precise. My first idempotency check used grep -q file_private_path settings.php. It matched the commented-out default line # $settings['file_private_path'] = ''; and skipped the injection every time. The installer said "set" — nothing was actually written. Fix: grep -q "^$settings\['file_private_path'\]" — anchor to the start of line, require the actual PHP assignment. Cache matters more than you think. Settings written to settings.php aren't visible to Drupal until the cache is rebuilt. The installer now runs drush cr immediately after both injections. Obvious in hindsight. Invisible until you're staring at an audit that says CRITICAL on something you just fixed. MIT license. No lock-in. The installer is free. What you install today stays yours. Future modules are optional. You need a Hetzner VPS running Ubuntu 24.04 and a domain pointed at it. That's it. First tester feedback welcome at GitHub Issues. Built with Claude. For Claude. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ actools audit $ actools audit $ actools audit === ACTOOLS DRUPAL AUDIT === [DRUPAL] PASS Security advisories: none found PASS trusted_host_patterns: configured PASS Error display: hidden PASS Private file path: configured and writable [INTEGRATION] PASS Redis: write/read/TTL confirmed PASS Queue worker: enqueue test passed PASS HTTP: Cache-Control header present [STACK] PASS Containers: all 5/5 running PASS Site response: HTTP 200 PASS TLS: valid, 90 days remaining PASS MariaDB: reachable PASS Worker container: healthy [SECURITY] PASS HTTPS: HTTP redirects to HTTPS PASS HSTS header: present PASS X-Frame-Options header: present PASS Server header: hidden ───────────────────────────────────────── PASS: 22 WARN: 5 FAIL: 1 Audit score: 6/10 Fix FAIL items before next deploy. === ACTOOLS DRUPAL AUDIT === [DRUPAL] PASS Security advisories: none found PASS trusted_host_patterns: configured PASS Error display: hidden PASS Private file path: configured and writable [INTEGRATION] PASS Redis: write/read/TTL confirmed PASS Queue worker: enqueue test passed PASS HTTP: Cache-Control header present [STACK] PASS Containers: all 5/5 running PASS Site response: HTTP 200 PASS TLS: valid, 90 days remaining PASS MariaDB: reachable PASS Worker container: healthy [SECURITY] PASS HTTPS: HTTP redirects to HTTPS PASS HSTS header: present PASS X-Frame-Options header: present PASS Server header: hidden ───────────────────────────────────────── PASS: 22 WARN: 5 FAIL: 1 Audit score: 6/10 Fix FAIL items before next deploy. === ACTOOLS DRUPAL AUDIT === [DRUPAL] PASS Security advisories: none found PASS trusted_host_patterns: configured PASS Error display: hidden PASS Private file path: configured and writable [INTEGRATION] PASS Redis: write/read/TTL confirmed PASS Queue worker: enqueue test passed PASS HTTP: Cache-Control header present [STACK] PASS Containers: all 5/5 running PASS Site response: HTTP 200 PASS TLS: valid, 90 days remaining PASS MariaDB: reachable PASS Worker container: healthy [SECURITY] PASS HTTPS: HTTP redirects to HTTPS PASS HSTS header: present PASS X-Frame-Options header: present PASS Server header: hidden ───────────────────────────────────────── PASS: 22 WARN: 5 FAIL: 1 Audit score: 6/10 Fix FAIL items before next deploy. # The Drupal community has enough Report modules. # What it lacks is a CLI tool that says: # I found a problem. I won't let you deploy until you run this specific command to fix it. # The Drupal community has enough Report modules. # What it lacks is a CLI tool that says: # I found a problem. I won't let you deploy until you run this specific command to fix it. # The Drupal community has enough Report modules. # What it lacks is a CLI tool that says: # I found a problem. I won't let you deploy until you run this specific command to fix it. -weight: 500;">docker compose exec -T "$php_svc" bash -c "cat > /tmp/php_inject.php << 'EOF' \$settings['trusted_host_patterns'] = array('^${domain_escaped}\$', '^.*\\.${domain_escaped}\$'); // trusted_host_patterns_active EOF cat /tmp/php_inject.php >> /path/to/settings.php rm -f /tmp/php_inject.php" -weight: 500;">docker compose exec -T "$php_svc" bash -c "cat > /tmp/php_inject.php << 'EOF' \$settings['trusted_host_patterns'] = array('^${domain_escaped}\$', '^.*\\.${domain_escaped}\$'); // trusted_host_patterns_active EOF cat /tmp/php_inject.php >> /path/to/settings.php rm -f /tmp/php_inject.php" -weight: 500;">docker compose exec -T "$php_svc" bash -c "cat > /tmp/php_inject.php << 'EOF' \$settings['trusted_host_patterns'] = array('^${domain_escaped}\$', '^.*\\.${domain_escaped}\$'); // trusted_host_patterns_active EOF cat /tmp/php_inject.php >> /path/to/settings.php rm -f /tmp/php_inject.php" -weight: 500;">git clone https://github.com/actools-pl/actoolsDrupal.-weight: 500;">git cd actoolsDrupal cp actools.env.example actools.env && nano actools.env -weight: 600;">sudo ./actools.sh actools audit -weight: 500;">git clone https://github.com/actools-pl/actoolsDrupal.-weight: 500;">git cd actoolsDrupal cp actools.env.example actools.env && nano actools.env -weight: 600;">sudo ./actools.sh actools audit -weight: 500;">git clone https://github.com/actools-pl/actoolsDrupal.-weight: 500;">git cd actoolsDrupal cp actools.env.example actools.env && nano actools.env -weight: 600;">sudo ./actools.sh actools audit - Security advisories via drush pm:security - trusted_host_patterns — reads settings.php and verifies it's active - Config drift — but only if the sync directory has a baseline (fresh installs get INFO, not false WARNING) - Error display mode - Session cookie security flags - Queue backlog - Redis behavioral test — not just "is it running" but write/read/TTL cycle - Redis as actual Drupal cache backend - HTTP cache headers - Queue worker — enqueues a test job and verifies processing - Private file path — verifiable and writable - All containers running - HTTP 200 response - TLS validity and days remaining - Memory available - Backup existence and age - MariaDB reachability - Worker container health - HTTPS redirect - HSTS header - X-Frame-Options - X-Content-Type-Options - Server header hidden - Referrer-Policy - Docker image pinning - Drupal 11 + PHP 8.3-FPM - Caddy 2 (automatic HTTPS, security headers, rate limiting) - MariaDB 11.4 - XeLaTeX worker (PDF generation, self-contained) - GitHub Actions CI (bats + shellcheck + Trivy + CodeQL) - Hetzner CX22 — €10/month