# Drupal best practices

This page collects the settings and habits that keep a Drupal site fast and stable on TurboStack. Drupal is a configuration-only application on the platform: you bring your own codebase and database and deploy with [Git deployment](../../platform/hosts/applications/git-deployment.md), while TurboStack provisions and tunes the surrounding stack.

> [!NOTE]
> Because the code and database are yours, application-level tuning (caching modules, compiled assets, scheduled tasks) is your responsibility. TurboStack handles the runtime, web server, cache, and database layers.

## What TurboStack configures for you

When you set an app's type to `drupal`, the platform provisions and pre-tunes several pieces so you do not have to:

- **Nginx vhost tuned for Drupal.** A Drupal-aware server config is written to your app's `nginx/50main.conf`. It sets the document root to `public_html`, routes clean URLs through `index.php`, passes image-style and asset generation back to Drupal, and serves static files with long `expires` headers.
- **PHP-FPM backend.** Requests for `.php` (and `update.php`) are passed to a dedicated PHP-FPM pool for your user/app, with a generous `fastcgi_read_timeout` for long operations like updates and imports.
- **Security hardening in the vhost.** Direct access is denied to PHP in `sites/*/files`, scripts under `vendor/`, and private file paths. Dotfiles (except `.well-known`) and source files such as `.module`, `.inc`, `.twig`, `.yml`, `composer.json`, and `*.sql` are also blocked.
- **Varnish full-page cache (optional).** When you enable Varnish, a Drupal-specific VCL is installed. It is built for the Advanced Varnish module: cache-tag and wildcard `BAN` invalidation, per-role cache bins via the `ADVBIN` cookie, ESI/BigPipe support, gzip compression, and cookie stripping for static assets. Authenticated, `/user`, `/admin`, and cron/install/update paths bypass the cache.
- **Redis service.** Redis can be enabled as a service for object and session caching (you must point Drupal at it - see below).

> [!TIP]
> The vhost is written with `force: no`, so it is not overwritten on later runs. If you hand-edit `nginx/50main.conf`, your changes persist - but you then own keeping it current.

## Recommended optimizations

Combine Drupal's own production guidance with the platform options. Enable services on the host's [Services](../../platform/hosts/services.md) tab.

- **Use Redis for cache and sessions.** Enable the Redis service, install the Drupal `redis` contrib module, and configure the cache backends in `settings.php`. This removes cache and session load from MySQL. See [Redis object and session cache](#redis-object-and-session-cache) for the config block and TTL tuning.
- **Enable Varnish for anonymous traffic.** Turn on Varnish and install the Advanced Varnish module so cache-tag invalidation works with the supplied VCL. Anonymous page views are then served without hitting PHP.
- **Turn on Drupal's own caches.** In production, enable page and dynamic page caching and the internal CSS/JS aggregation; never run with the Twig debug cache disabled.
- **Aggregate and compress assets.** Enable Cascading Style Sheets (CSS) and JavaScript (JS) aggregation; the Nginx config already serves the compiled files with far-future expiry.
- **Keep OPcache warm.** PHP OPcache caches compiled code; keep it enabled and sized for your module count to cut per-request overhead.
- **Optimize images.** Use responsive image styles and modern formats (WebP/AVIF), which the vhost serves with caching.
- **Run cron via Drush, not the web cron.** Disable Drupal's automated web cron and schedule `drush cron` so cron is decoupled from page requests. Use a [Scheduled task / cron service](../../platform/hosts/services.md) on the host.
- **Use a Content Delivery Network (CDN)** in front of the site for static assets and global reach where appropriate.

## Redis object and session cache

Enable the Redis service, install the Drupal `redis` contrib module, then point Drupal at the cache instance over its socket. TurboStack runs the cache instance on port 6379 (socket `/var/run/redis/redis.sock`) with no password. Add this to `settings.php`:

```php
$settings['redis.connection']['interface'] = 'PhpRedis';
$settings['redis.connection']['host'] = '/var/run/redis/redis.sock';
$settings['redis.connection']['port'] = 6379;
$settings['redis.connection']['password'] = '';
$settings['redis.connection']['prefix'] = 'example:';
```

Use a unique `prefix` per site so several sites on one host do not overwrite each other's keys. Clear all caches after the change (`drush cr`).

### Tune the cache Time to Live (TTL)

The Drupal Redis module gives permanent cache items a long default lifetime. The constant `LIFETIME_PERM_DEFAULT` in `modules/contrib/redis/src/Cache/CacheBase.php` is `31536000` (one year): any key without an explicit TTL falls back to that. A one-year fallback lets stale bins build up and fill Redis.

You do not need to edit the module. Set a shorter TTL per cache bin in `settings.php` with the `perm_ttl_<bin>` setting:

```php
$settings['redis.settings']['perm_ttl_cache_menu'] = 21600; // 6 hours
```

This gives the `menu` bin a six-hour lifetime while other bins keep the default. Add one line per bin you want to cap. To decide which bins matter, inspect the keys (for example with Redis Insight) and sort by TTL, then purge the old keys:

```bash
drush cr
tscli redis clear
```

> [!WARNING]
> `tscli redis clear` empties the cache Redis instance. Run it only when you intend to clear the cache. The persistent instance on port 6378 (sessions) is not touched.

## Sizing and scaling

TurboStack auto-tunes service sizes to the host. Change them only when you have measured evidence (slow queries, cache evictions, memory pressure):

| Variable | Tune when |
| --- | --- |
| `mysql_innodb_size` | Working set larger than the buffer pool; frequent disk reads |
| `redis_memory` | Cache evictions / low hit rate in Redis |
| `varnish_cache_size` | Anonymous hit rate dropping as content grows |

See [Performance tuning](../../concepts/performance-tuning.md) for how to measure before changing defaults, and increase PHP `memory_limit` only for genuinely heavy operations (config import, large migrations).

## Stability

- **Back up before changes.** Confirm scheduled [Backups](../../platform/hosts/backups.md) cover both the database and `sites/default/files`, and take one before deploys or `drush updb`/config imports.
- **Watch [Health](../../platform/hosts/health.md).** Monitor CPU, memory, and service status, especially after a deploy.
- **Keep versions current.** Track security releases for Drupal core, contrib modules, and the PHP runtime.
- **Manage config in Git.** Export with `drush cex` and import with `drush cim` so configuration moves predictably between environments.
- **Test on a staging clone** before applying updates or module changes to production.

## Related

- [Deploy Drupal](deploy.md)
- [Drupal reference](reference.md)
- [Troubleshooting Drupal](troubleshooting.md)
- [Services](../../platform/hosts/services.md)
- [Performance tuning](../../concepts/performance-tuning.md)
