Repository: roots/docs
Branch: docs
Commit: b9b2674e1a18
Files: 101
Total size: 352.4 KB
Directory structure:
gitextract_yjm39ipx/
├── .github/
│ └── workflows/
│ ├── lint-bash-blocks.yml
│ └── trigger-docs-sync.yml
├── .gitignore
├── CLAUDE.md
├── README.md
├── acorn/
│ ├── available-packages.md
│ ├── compatibility.md
│ ├── controllers-middleware-kernel.md
│ ├── creating-and-processing-laravel-queues.md
│ ├── creating-and-running-laravel-migrations.md
│ ├── creating-wp-cli-commands-with-artisan-console.md
│ ├── directory-structure.md
│ ├── eloquent-models.md
│ ├── error-handling.md
│ ├── installation.md
│ ├── laravel-cache-alternative-to-wordpress-transients.md
│ ├── laravel-redis-configuration.md
│ ├── logging.md
│ ├── package-development.md
│ ├── rendering-blade-views.md
│ ├── routing.md
│ ├── upgrading-acorn.md
│ ├── using-livewire-with-wordpress.md
│ └── wp-cli.md
├── bedrock/
│ ├── auditing-wordpress-vulnerabilities-with-composer.md
│ ├── bedrock-with-ddev.md
│ ├── bedrock-with-devkinsta.md
│ ├── bedrock-with-lando.md
│ ├── bedrock-with-local.md
│ ├── bedrock-with-valet.md
│ ├── compatibility.md
│ ├── composer.md
│ ├── configuration.md
│ ├── converting-wordpress-sites-to-bedrock.md
│ ├── deployment.md
│ ├── disable-plugins-based-on-environment.md
│ ├── environment-variables.md
│ ├── folder-structure.md
│ ├── installation.md
│ ├── local-development.md
│ ├── mu-plugin-autoloader.md
│ ├── patching-wordpress-plugins-with-composer.md
│ ├── patching-wordpress-with-composer.md
│ ├── private-or-commercial-wordpress-plugins-as-composer-dependencies.md
│ ├── server-configuration.md
│ ├── testing.md
│ └── wp-cron.md
├── netlify.toml
├── sage/
│ ├── adding-linting.md
│ ├── blade-templates.md
│ ├── bootstrap.md
│ ├── compatibility.md
│ ├── compiling-assets.md
│ ├── components.md
│ ├── composers.md
│ ├── configuration.md
│ ├── deployment.md
│ ├── fonts-setup.md
│ ├── functionality.md
│ ├── gutenberg.md
│ ├── installation.md
│ ├── localization.md
│ ├── sass.md
│ ├── structure.md
│ ├── tailwind-css.md
│ ├── theme-templates.md
│ ├── use-blade-icons.md
│ └── woocommerce.md
└── trellis/
├── ansible.md
├── cli.md
├── composer-authentication.md
├── configuring-php.md
├── cron-jobs.md
├── database-access.md
├── debugging-php.md
├── deploy-to-digitalocean.md
├── deploy-to-hetzner-cloud.md
├── deploy-with-github-actions.md
├── deployments.md
├── existing-projects.md
├── fastcgi-caching.md
├── install-wordpress-language-files.md
├── installation.md
├── local-development.md
├── mail.md
├── multiple-sites.md
├── multisite.md
├── nginx-includes.md
├── passwords.md
├── python.md
├── redis.md
├── remote-server-setup.md
├── sage-integration.md
├── security.md
├── server-logs.md
├── ssh-keys.md
├── ssl.md
├── troubleshooting.md
├── user-contributed-extensions.md
├── vault.md
└── wordpress-sites.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/lint-bash-blocks.yml
================================================
name: Lint bash code blocks
on:
pull_request:
paths:
- '**/*.md'
jobs:
check-bash-blocks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for multi-line bash code blocks
run: |
fail=0
while IFS= read -r file; do
# Strip leading ./ so GitHub annotations link to the correct file
clean="${file#./}"
awk -v file="$clean" '
/^```bash/ { in_block=1; lines=0; start=NR; next }
/^```/ && in_block {
if (lines > 1) {
printf "::error file=%s,line=%d::Bash code block has multiple commands. Each block must contain exactly one command.\n", file, start
found=1
}
in_block=0; next
}
in_block && /^[^#]/ && !/^[[:space:]]*$/ { lines++ }
END { if (found) exit 1 }
' "$file" || fail=1
done < <(find . -name '*.md' -not -path './.git/*' -not -name 'CLAUDE.md')
if [ "$fail" -eq 1 ]; then
echo ""
echo "Error: Found bash code blocks with multiple commands."
echo "Each bash code block must contain exactly one command."
exit 1
fi
echo "All bash code blocks contain a single command."
================================================
FILE: .github/workflows/trigger-docs-sync.yml
================================================
name: Trigger docs sync
on:
push:
branches: [docs]
paths:
- 'acorn/**'
- 'bedrock/**'
- 'sage/**'
- 'trellis/**'
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- name: Trigger docs sync on roots.dev
run: gh workflow run docs-sync.yml --repo roots/roots.dev
env:
GH_TOKEN: ${{ secrets.ROOTS_DEV_TOKEN }}
================================================
FILE: .gitignore
================================================
================================================
FILE: CLAUDE.md
================================================
# Docs Conventions
## Markdown bash code blocks
Each bash code block must contain exactly one command. Never combine multiple commands in a single block. On the roots.io front-end, bash code blocks have a clipboard copy button that only supports single lines.
Good:
````markdown
```bash
composer require example/package
```
```bash
wp plugin activate example
```
````
Bad:
````markdown
```bash
composer require example/package
wp plugin activate example
```
````
================================================
FILE: README.md
================================================
# Roots Documentation
This repository is synced with the Roots docs for our primary projects:
- [Acorn Docs](https://roots.io/acorn/docs/)
- [Bedrock Docs](https://roots.io/bedrock/docs/)
- [Sage Docs](https://roots.io/sage/docs/)
- [Trellis Docs](https://roots.io/trellis/docs/)
## Contributing
Please use [Roots Discourse](https://discourse.roots.io/) to open a discussion about bigger changes before sending a pull request.
If you have an account on Roots Discourse, add your username in the authors list (in alphabetical order) at the top of any docs that you contribute to.
================================================
FILE: acorn/available-packages.md
================================================
---
date_modified: 2026-03-03 12:00
date_published: 2021-11-19 11:58
description: Explore community-developed packages for Acorn and Sage. WooCommerce integration, additional Laravel features, and third-party extensions.
title: Community Packages for Acorn
authors:
- alwaysblank
- ben
- QWp6t
---
# Community Packages for Acorn
| Package | Description |
| ----------- | ----------- |
| [`roots/acorn-ai`](https://github.com/roots/acorn-ai) | WordPress Abilities API integration and AI support for Acorn |
| [`roots/acorn-fse-helper`](https://github.com/roots/acorn-fse-helper) | Bootstrap FSE support in Acorn-based WordPress themes |
| [`roots/acorn-mail`](https://github.com/roots/acorn-mail) | A simple package handling WordPress SMTP using Acorn's mail configuration |
| [`roots/acorn-post-types`](https://github.com/roots/acorn-post-types) | Simple post types and taxonomies using Extended CPTs |
| [`roots/acorn-prettify`](https://github.com/roots/acorn-prettify) | A collection of modules to apply theme-agnostic front-end modifications to your Acorn-powered WordPress sites |
| [`roots/acorn-user-roles`](https://github.com/roots/acorn-user-roles) | Simple user role management for Acorn |
## Community packages
| Package | Description |
| ----------- | ----------- |
| [`40q/40q-seo-assistant`](https://github.com/40Q/40q-seo-assistant) | Editor-side SEO metadata suggestions for WordPress powered by Acorn |
| [`blavetstudio/sage-woocommerce-subscriptions`](https://github.com/blavetstudio/sage-woocommerce-subscriptions) | Add WooCommerce Subscriptions support to Sage 10 |
| [`digitalnodecom/substrate`](https://github.com/digitalnodecom/substrate) | AI MCP for Development with Bedrock, Acorn, Sage |
| [`generoi/sage-cachetags`](https://github.com/generoi/sage-cachetags) | A sage package for tracking what data rendered pages rely on using Cache Tags |
| [`generoi/sage-woocommerce`](https://github.com/generoi/sage-woocommerce) | Add WooCommerce support to Sage 10 |
| [`istogram/wp-api-content-migration`](https://github.com/istogram/wp-api-content-migration) | Migrate WordPress content using the WP REST API |
| [`leocolomb/wp-acorn-cache`](https://github.com/LeoColomb/wp-acorn-cache) | A WordPress cache manager powered by Laravel through Acorn |
| [`millipress/acorn-millicache`](https://github.com/MilliPress/Acorn-MilliCache) | MilliCache integration for Roots Acorn and Bedrock projects |
| [`log1x/acf-composer`](https://github.com/log1x/acf-composer) | ACF Composer is the ultimate tool for creating fields, blocks, widgets, and option pages using ACF Builder alongside Sage 10 |
| [`log1x/acorn-disable-media-pages`](https://github.com/log1x/acorn-disable-media-pages) | Disable media attachment pages on WordPress sites using Acorn |
| [`log1x/pagi`](https://github.com/log1x/pagi) | A better WordPress pagination utilizing Laravel's Pagination |
| [`log1x/poet`](https://github.com/log1x/poet) | Poet provides simple configuration-based post type, taxonomy, editor color palette, block category, block pattern and block registration/modification |
| [`log1x/sage-directives`](https://github.com/log1x/sage-directives) | A variety of useful Blade directives for use with Sage 10 including directives for WordPress, ACF, and various miscellaneous helpers |
| [`log1x/sage-html-forms`](https://github.com/log1x/sage-html-forms) | This is a simple package for the HTML Forms plugin that allows you to easily render forms using a corresponding Blade view (if one is present) with Sage 10 |
| [`log1x/sage-svg`](https://github.com/log1x/sage-svg) | Sage SVG is a simple package for using inline SVGs in your Sage 10 projects |
| [`pixelcollective/acorn-db`](https://github.com/pixelcollective/acorn-db) | Provides Sage 10 and other Acorn projects with an eloquent Model layer straight from the heart of the Laravel framework |
| [`supermundano/sage-the-events-calendar`](https://github.com/supermundano/sage-the-events-calendar) | Add The Events Calendar support to Sage 10 |
| [`tombroucke/sage-html-forms-export-submissions`](https://github.com/tombroucke/sage-html-forms-export-submissions) | Export HTML Forms submissions to Excel and CSV |
================================================
FILE: acorn/compatibility.md
================================================
---
date_modified: 2026-05-05 16:35
date_published: 2024-04-26 10:35
description: Known compatibility issues between WordPress plugins and Acorn, including solutions and workarounds for common integration conflicts.
title: WordPress Plugin Compatibility with Acorn
authors:
- ben
- dalepgrant
- joshf
---
# WordPress Plugin Compatibility with Acorn
Acorn is installed via Composer and includes many dependencies, that also include their own dependencies. WordPress plugin authors often include their own dependencies in a way that can conflict with Acorn.
Compatibility issues that arise in Acorn with other WordPress plugins are most often related to a WordPress plugin that is including an older version of a dependency that exists in the Acorn dependency tree.
**Plugin developers need to wrap their dependencies with their own namespace** in order to prevent conflicts with other plugins and with Acorn. The following tools can be used to handle this:
* [PHP-Scoper](https://github.com/humbug/php-scoper)
* [Imposter Plugin](https://github.com/TypistTech/imposter-plugin)
* [Mozart](https://github.com/coenjacobs/mozart)
**Acorn has no responsibility to fix compatibility issues that are the result of plugins that don't wrap their dependencies with their own namespace.**
## Known issues with plugins
Composer patches can sometimes be used to work around issues with plugins.
* **Google for WooCommerce** includes older versions of `psr/log` and `monolog/monolog`. [Patch available](https://gist.github.com/retlehs/3dfd033e196c25e376acbeb89fa41dbd).
* **Gravity Forms** merge tags JS causes an error on the admin notifications page. [@tombroucke provided a workaround in roots/acorn#198](https://github.com/roots/acorn/issues/198#issuecomment-1365942893).
* **Gravity Forms: Entry Automation FTP Extension** includes `league/flysystem` v1.1.4 which is incompatible with Acorn.
* **WooCommerce PayPal Payments** includes an older version of `psr/log`.
* **WooCommerce UPS Shipping** includes an older version of `psr/log`. [Patch available](https://gist.github.com/retlehs/4e76aee9a30cc0d3228cf6146eec64e0).
* **WooCommerce USPS Shipping** includes an older version of `psr/log`. [Patch available](https://gist.github.com/retlehs/4e76aee9a30cc0d3228cf6146eec64e0).
For more information on how to use Composer patches to resolve plugin conflicts, see [Patching WordPress Plugins with Composer](/bedrock/docs/patching-wordpress-plugins-with-composer/).
================================================
FILE: acorn/controllers-middleware-kernel.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2025-10-01 00:00
description: Build robust APIs and handle requests with Laravel controllers, middleware, and custom HTTP kernels in WordPress using Acorn. Clean separation of concerns with validation, authentication, and response formatting.
title: Controllers, Middleware, and HTTP Kernel in WordPress
authors:
- ben
---
# Controllers, Middleware, and HTTP Kernel in WordPress
Acorn brings Laravel's controller and middleware system to WordPress, enabling you to build robust APIs, handle complex request logic, and implement clean separation of concerns. Controllers organize your route logic, while middleware provides a convenient mechanism for filtering HTTP requests.
We recommend referencing the [Laravel docs on Controllers](https://laravel.com/docs/13.x/controllers) and [Middleware](https://laravel.com/docs/13.x/middleware) for a complete understanding.
## Creating controllers
### Generate a controller
To create a new controller, use the `make:controller` Artisan command:
#### Create a basic controller
```bash
$ wp acorn make:controller PostController
```
#### Create an API resource controller
```bash
$ wp acorn make:controller PostController --api
```
#### Create a controller with all CRUD methods
```bash
$ wp acorn make:controller PostController --resource
```
This creates a new controller file in `app/Http/Controllers/`.
### Basic controller example
```php
latest('ID')
->take(10)
->get();
return response()->json($posts);
}
public function show(int $id): JsonResponse
{
$post = Post::findOrFail($id);
return response()->json($post);
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
'status' => 'in:draft,publish'
]);
$post = Post::create([
'post_title' => $validated['title'],
'post_content' => $validated['content'],
'post_status' => $validated['status'] ?? 'draft',
'post_type' => 'post',
'post_author' => get_current_user_id() ?: 1,
]);
return response()->json($post, 201);
}
}
```
### Using controllers in routes
Define your routes in `routes/web.php`:
```php
validate([
'title' => 'required|max:255',
'content' => 'required',
]);
$post_id = wp_insert_post([
'post_title' => $validated['title'],
'post_content' => $validated['content'],
'post_status' => 'publish',
'post_type' => 'post',
]);
return response()->json(['id' => $post_id], 201);
}
}
```
## Creating middleware
### Generate middleware
To create new middleware, use the `make:middleware` Artisan command:
```bash
$ wp acorn make:middleware AuthenticateAdmin
```
This creates a new middleware file in `app/Http/Middleware/`.
### Authentication middleware example
```php
json([
'message' => 'Authentication required'
], 401);
}
if (!current_user_can('manage_options')) {
return response()->json([
'message' => 'Admin access required'
], 403);
}
return $next($request);
}
}
```
## Applying middleware
Apply middleware to routes:
```php
middleware(AuthenticateAdmin::class);
```
## Customizing the HTTP kernel
For most middleware needs, use the `withMiddleware()` method when [booting Acorn](/acorn/docs/installation/#advanced-booting). If you need more control, you can override the HTTP kernel entirely.
### Creating a custom kernel
Create a custom kernel class that extends Acorn's HTTP kernel. When overriding properties like `$middleware`, make sure to include any defaults you still need — setting the property replaces the parent's values entirely:
```php
middleware[] = \Illuminate\Foundation\Http\Middleware\TrimStrings::class;
parent::__construct($app, $router);
}
}
```
### Registering the custom kernel
Override the kernel singleton by rebinding it before `boot()`. The kernel is resolved during boot, so the binding must be in place before that happens:
```php
use Roots\Acorn\Application;
add_action('after_setup_theme', function () {
$builder = Application::configure()
->withProviders()
->withRouting(
web: base_path('routes/web.php'),
wordpress: true,
);
app()->singleton(
\Illuminate\Contracts\Http\Kernel::class,
\App\Http\Kernel::class
);
$builder->boot();
}, 0);
```
================================================
FILE: acorn/creating-and-processing-laravel-queues.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2025-09-29 00:00
description: Use Laravel's queue system in WordPress through Acorn. Process background jobs, handle async tasks, and schedule recurring operations efficiently.
title: Creating and Processing Laravel Queues
authors:
- ben
---
# Creating and Processing Laravel Queues
Acorn brings Laravel's robust queue system to WordPress, enabling you to defer time-consuming tasks like image processing, email sending, or API calls to background jobs. This improves your application's response time and user experience by handling heavy operations asynchronously.
We recommend referencing the [Laravel docs on Queues](https://laravel.com/docs/13.x/queues) for a complete understanding of the queue system.
## Setting up the queue system
Before you can start using queues, you need to create the necessary database tables to store jobs and track their status.
### 1. Generate queue tables
Create the migration files for queue functionality:
#### Generate the jobs table migration
```bash
$ wp acorn queue:table
```
#### Generate the job batches table (optional, for batch processing)
```bash
$ wp acorn queue:batches-table
```
### 2. Run migrations
Apply the migrations to create the required tables:
```bash
$ wp acorn migrate
```
This will create:
- A `jobs` table to store queued jobs
- A `job_batches` table for batch job processing (if generated)
- A `failed_jobs` table to track failed job attempts
## Creating your first job
To create a new job class, use the `make:job` command:
```bash
$ wp acorn make:job ProcessImageOptimization
```
This creates a new job file in `app/Jobs/` with the basic structure needed for a queue job.
### Job file structure
A typical job class contains several key components:
```php
attachmentId = $attachmentId;
}
/**
* Execute the job
*/
public function handle(): void
{
Log::info("Processing image optimization for attachment: {$this->attachmentId}");
$attachment = get_post($this->attachmentId);
if (!$attachment || $attachment->post_type !== 'attachment') {
Log::error("Invalid attachment ID: {$this->attachmentId}");
return;
}
$file_path = get_attached_file($this->attachmentId);
// Your image optimization logic here
// For example, using an image optimization library
// Mark as processed using post meta
update_post_meta($this->attachmentId, '_processed', true);
update_post_meta($this->attachmentId, '_processed_at', current_time('timestamp'));
Log::info("Successfully optimized image: {$this->attachmentId}");
}
/**
* Handle a job failure
*/
public function failed(\Throwable $exception): void
{
Log::error("Failed to optimize image {$this->attachmentId}: {$exception->getMessage()}");
// Notify administrators or take other actions
}
}
```
## Dispatching jobs
Once you've created a job, you can dispatch it from anywhere in your application:
### Basic dispatching
```php
use App\Jobs\ProcessImageOptimization;
// Dispatch a job to the default queue
ProcessImageOptimization::dispatch($attachmentId);
// Dispatch with a delay
ProcessImageOptimization::dispatch($attachmentId)
->delay(now()->addMinutes(5));
// Dispatch to a specific queue
ProcessImageOptimization::dispatch($attachmentId)
->onQueue('images');
```
### WordPress hook integration
Integrate queue jobs with WordPress hooks for automatic processing:
```php
// In your theme's functions.php or a service provider
add_action('add_attachment', function ($attachmentId) {
\App\Jobs\ProcessImageOptimization::dispatch($attachmentId);
});
// Process form submissions asynchronously
add_action('gform_after_submission', function ($entry, $form) {
\App\Jobs\ProcessFormSubmission::dispatch($entry['id']);
}, 10, 2);
```
## Processing queued jobs
To process jobs in the queue, you need to run a queue worker.
### Running a queue worker
#### Process jobs continuously
```bash
$ wp acorn queue:work
```
#### Process jobs from a specific queue
```bash
$ wp acorn queue:work --queue=high,default
```
#### Process a single job and exit
```bash
$ wp acorn queue:work --once
```
#### Process jobs for a specific duration
```bash
$ wp acorn queue:work --stop-when-empty
```
## Managing failed jobs
When jobs fail after all retry attempts, they're moved to the `failed_jobs` table.
### View failed jobs
```bash
$ wp acorn queue:failed
```
### Retry all failed jobs
```bash
$ wp acorn queue:retry all
```
### Retry specific job
```bash
$ wp acorn queue:retry 5
```
### Retry multiple jobs
```bash
$ wp acorn queue:retry 5 6 7
```
### Remove all failed jobs
```bash
$ wp acorn queue:flush
```
### Remove a specific failed job
```bash
$ wp acorn queue:forget 5
```
## Dispatching jobs
```php
// In a controller or WordPress hook
use App\Jobs\ProcessImageOptimization;
// Dispatch immediately
ProcessImageOptimization::dispatch($attachmentId);
// Dispatch with delay
ProcessImageOptimization::dispatch($attachmentId)->delay(now()->addMinutes(5));
```
================================================
FILE: acorn/creating-and-running-laravel-migrations.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2025-08-06 14:00
description: Use Laravel's migration system in WordPress through Acorn. Create, modify, and manage custom database tables with Artisan migration commands.
title: Creating and Running Laravel Migrations
authors:
- ben
---
# Creating and Running Laravel Migrations
Acorn brings Laravel's powerful migration system to WordPress, allowing you to version control your database schema and manage custom tables with ease. Each migration contains instructions for creating or modifying database tables.
We recommend referencing the [Laravel docs on Database Migrations](https://laravel.com/docs/13.x/migrations) for a complete understanding of the migration system.
## Creating your first migration
To create a new migration, use the `make:migration` command:
```bash
$ wp acorn make:migration create_app_settings_table
```
This will create a new migration file in `database/migrations/` with a timestamp prefix, like `2025_08_06_140000_create_app_settings_table.php`.
### Migration file structure
A typical migration contains two methods:
- `up()` - Defines changes to apply
- `down()` - Defines how to reverse those changes
Here's an example migration for an app settings table:
```php
id();
$table->string('key')->unique();
$table->json('value')->nullable();
$table->string('group')->default('general');
$table->boolean('is_public')->default(false);
$table->text('description')->nullable();
$table->timestamps();
$table->index('group');
$table->index('is_public');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('app_settings');
}
};
```
## Running migrations
To run all pending migrations:
```bash
$ wp acorn migrate
```
To see the status of your migrations:
```bash
$ wp acorn migrate:status
```
To rollback the last batch of migrations:
```bash
$ wp acorn migrate:rollback
```
## Adding columns to existing tables
To add columns to an existing table, create a new migration:
```bash
$ wp acorn make:migration add_encrypted_to_app_settings_table
```
```php
public function up(): void
{
Schema::table('app_settings', function (Blueprint $table) {
$table->boolean('is_encrypted')->default(false)->after('is_public');
});
}
public function down(): void
{
Schema::table('app_settings', function (Blueprint $table) {
$table->dropColumn('is_encrypted');
});
}
```
## Deployment
You should run migrations as part of your deployment process. Add this to your deployment script after `wp acorn optimize`:
```bash
$ wp acorn optimize
```
```bash
$ wp acorn migrate --force
```
The `--force` flag runs migrations without confirmation prompts in production environments.
## Troubleshooting
If you get an error about the migrations table not existing, run:
```bash
$ wp acorn migrate:install
```
================================================
FILE: acorn/creating-wp-cli-commands-with-artisan-console.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2025-09-28 00:00
description: Create custom WP-CLI commands using Laravel's Artisan Console system with Acorn. Extend WordPress CLI with powerful Laravel functionality.
title: Creating WP-CLI Commands with Artisan Console
authors:
- ben
---
# Creating WP-CLI Commands with Artisan Console
Acorn brings Laravel's powerful Artisan Console system to WordPress, allowing you to create custom WP-CLI commands with the same elegance and functionality you'd expect from Laravel. This enables you to build sophisticated command-line tools that integrate seamlessly with both WordPress and Laravel features.
We recommend referencing the [Laravel docs on Artisan Console](https://laravel.com/docs/13.x/artisan) for a complete understanding of the console system.
## Creating your first command
To create a new WP-CLI command, use the `make:command` Artisan command:
```bash
$ wp acorn make:command SeoAuditCommand
```
This will create a new command file in `app/Console/Commands/` with the basic structure needed for a custom command.
### Command file structure
A typical Artisan command contains several key properties and methods:
- `$signature` - Defines the command name, arguments, and options
- `$description` - Provides a description for the command
- `handle()` - Contains the command logic
Here's a basic example for auditing SEO:
```php
option('post-type');
$limit = (int) $this->option('limit');
$this->components->info("Auditing {$postType} posts for SEO issues...");
$posts = get_posts([
'post_type' => $postType,
'post_status' => 'publish',
'numberposts' => $limit,
]);
if (empty($posts)) {
$this->components->warn('No posts found to audit.');
return 0;
}
$issues = [];
foreach ($posts as $post) {
$postIssues = $this->auditPost($post);
if (!empty($postIssues)) {
$issues[$post->ID] = [
'title' => $post->post_title,
'issues' => $postIssues,
];
}
}
if (empty($issues)) {
$this->components->info('No SEO issues found! 🎉');
return 0;
}
$this->displayIssues($issues);
return 0;
}
protected function auditPost($post)
{
$issues = [];
$seoTitle = get_post_meta($post->ID, '_genesis_title', true) ?: $post->post_title;
if (strlen($seoTitle) < 30) {
$issues[] = 'SEO title too short (< 30 chars)';
}
if (strlen($seoTitle) > 60) {
$issues[] = 'SEO title too long (> 60 chars)';
}
$description = get_post_meta($post->ID, '_genesis_description', true);
if (empty($description)) {
$issues[] = 'Missing SEO meta description';
} elseif (strlen($description) < 120) {
$issues[] = 'Meta description too short (< 120 chars)';
} elseif (strlen($description) > 160) {
$issues[] = 'Meta description too long (> 160 chars)';
}
return $issues;
}
protected function displayIssues($issues)
{
$this->components->error('Found ' . count($issues) . ' posts with SEO issues:');
$this->newLine();
foreach ($issues as $postId => $data) {
$this->components->twoColumnDetail(
"Post #{$postId}",
$data['title']
);
foreach ($data['issues'] as $issue) {
$this->line(" → {$issue}");
}
$this->newLine();
}
}
}
```
## Command signature syntax
```php
// Basic command
protected $signature = 'newsletter:send';
// With arguments
protected $signature = 'user:create {name} {email}';
// With options
protected $signature = 'seo:audit {--post-type=post}';
```
## Running your commands
Once created, your commands are automatically available through WP-CLI:
#### Run your SEO audit command
```bash
$ wp acorn seo:audit
```
#### Run with options
```bash
$ wp acorn seo:audit --post-type=page --limit=50
```
#### Get help for a command
```bash
$ wp acorn help seo:audit
```
## Console output
```php
public function handle()
{
$this->info('Success message');
$this->error('Error message');
// Ask for input
$name = $this->components->ask('What is your name?');
// Use WordPress functions
$posts = get_posts(['numberposts' => 10]);
return 0; // Success
}
```
================================================
FILE: acorn/directory-structure.md
================================================
---
date_modified: 2025-07-22 13:34
date_published: 2021-11-19 11:58
description: Acorn works with zero configuration by default. Optionally publish config files to use Laravel's familiar directory structure in WordPress.
title: Acorn Application Directory Structure
authors:
- alwaysblank
- ben
- rafaucau
- QWp6t
---
# Acorn Application Directory Structure
## Zero-config setup
Out of the box, Acorn will [use its own configs](https://github.com/roots/acorn/tree/main/config), and it will keep the application cache and logs in the standard WordPress cache directory:
```plaintext
[wp-content]/ # wp-content directory ("app" if you're using Bedrock)
├── cache/
│ └── /acorn/ # Private application storage ("storage" directory)
│ ├── app/ # Files generated or used by the application
│ ├── framework/ # Files generated or used by Acorn (never edit)
│ └── logs/ # Application logs
└── themes/
└── [theme]/ # Theme directory (e.g., "sage")
├── app/ # Core application code
├── public/ # Built application assets (never edit)
├── resources/ # Uncompiled source assets and views
│ └── views/ # Application views to be compiled by Blade
└── vendor/ # Composer packages (never edit)
```
## Traditional setup
Acorn also supports a more traditional Laravel-esque structure. We recommend this approach if you are adding Acorn/Laravel packages and want to have more control over your app.
::: tip
If you've installed Acorn from your Bedrock project root, Acorn's `config/` directory will conflict with Bedrock's. We recommend using [Radicle](/radicle/) to avoid this.
There are no conflicts with the `config/` directory if you've installed Acorn from your theme.
:::
```plaintext
root/ # Base directory for your Acorn application (e.g., "sage")
├── app/ # Core application code
├── config/ # Application configuration
├── public/ # Built application assets (never edit)
├── resources/ # Uncompiled source assets and views
│ └── views/ # Application views to be compiled by Blade
├── storage/ # Private application storage
│ ├── app/ # Files generated or used by the application
│ ├── framework/ # Files generated or used by Acorn (never edit)
│ └── logs/ # Application logs
└── vendor/ # Composer packages (never edit)
```
You can manually create a `config/` directory, or you can automatically set up the traditional structure with WP-CLI (see below).
If you have a `config/` directory, you can drop your desired config files in there. any that are missing (such as `app.php`) will just be pulled from [Acorn's config directory](https://github.com/roots/acorn/tree/main/config).
### WP-CLI commands for setting up the traditional structure
You can automatically set up the traditional structure via WP-CLI:
```shell
$ wp acorn acorn:init storage && wp acorn vendor:publish --tag=acorn
```
Alternatively, you can choose to only copy the config files.
```shell
$ wp acorn vendor:publish --tag=acorn
```
## Advanced directory modifications
You can modify the path for any Acorn directory by defining the following constants:
- `ACORN_BASEPATH`
- `ACORN_APP_PATH`
- `ACORN_CONFIG_PATH`
- `ACORN_STORAGE_PATH`
- `ACORN_RESOURCES_PATH`
- `ACORN_PUBLIC_PATH`
================================================
FILE: acorn/eloquent-models.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2025-10-01 00:00
description: Use Laravel's Eloquent ORM in WordPress with Acorn. Create models for WordPress posts, users, and custom tables with relationships, scopes, and clean query syntax.
title: Using Eloquent Models in WordPress
authors:
- ben
---
# Using Eloquent Models in WordPress
Acorn brings Laravel's powerful Eloquent ORM to WordPress, allowing you to interact with WordPress data using clean, expressive syntax. Create models for posts, users, custom tables, and more with relationships, scopes, and all the Eloquent features you love.
We recommend referencing the [Laravel docs on Eloquent](https://laravel.com/docs/13.x/eloquent) for a complete understanding of the ORM.
## Creating your first model
Since Acorn doesn't include the `make:model` command, you'll need to create model files manually. Create a new PHP file in the `app/Models/` directory with the following structure.
## WordPress post model
Here's an example of an Eloquent model for WordPress posts:
```php
belongsTo(User::class, 'post_author');
}
public function meta()
{
return $this->hasMany(PostMeta::class, 'post_id');
}
public function scopePublished($query)
{
return $query->where('post_status', 'publish');
}
public function scopeOfType($query, $type)
{
return $query->where('post_type', $type);
}
}
```
### Key considerations for WordPress models
When creating models for WordPress tables, keep these points in mind:
- **Table names**: WordPress tables don't follow Laravel naming conventions, so explicitly set the `$table` property
- **Primary keys**: WordPress uses `ID` (uppercase) instead of `id`, so set `$primaryKey = 'ID'`
- **Timestamps**: WordPress handles timestamps differently, so set `$timestamps = false` and handle dates manually
- **Table prefixes**: WordPress table prefixes are handled automatically by WordPress's database configuration
## WordPress user model
```php
hasMany(Post::class, 'post_author');
}
public function meta()
{
return $this->hasMany(UserMeta::class, 'user_id');
}
}
```
## Post meta model
```php
belongsTo(Post::class, 'post_id');
}
}
```
## Using models in your application
### Basic queries
```php
// Get all published posts
$posts = Post::published()->get();
// Get posts of a specific type
$pages = Post::ofType('page')->published()->get();
// Get a post with its author
$post = Post::with('author')->find(123);
// Create a new post
$post = Post::create([
'post_title' => 'Hello World',
'post_content' => 'This is my first post using Eloquent!',
'post_status' => 'publish',
'post_type' => 'post',
'post_author' => get_current_user_id(),
]);
```
### Working with relationships
```php
// Get a post's author
$post = Post::find(123);
$author = $post->author;
// Get an author's posts
$user = User::find(1);
$posts = $user->posts()->published()->get();
// Get post meta
$post = Post::with('meta')->find(123);
foreach ($post->meta as $meta) {
echo $meta->meta_key . ': ' . $meta->meta_value;
}
```
## Custom tables
You can also create models for custom database tables:
```php
Boot Acorn in your own theme or plugin
Add the following in your theme's `functions.php` file, or in your main plugin file:
```php
'https://roots.io/acorn/docs/installation/',
'link_text' => __('Acorn Docs: Installation', 'domain'),
]
);
}
add_action('after_setup_theme', function () {
Application::configure()
->withProviders([
App\Providers\ThemeServiceProvider::class,
])
->boot();
}, 0);
```
### Advanced booting
Acorn provides several additional configuration methods that can be chained before booting. Here's a comprehensive example with explanations:
```php
add_action('after_setup_theme', function () {
Application::configure()
->withProviders([
// Register your service providers
App\Providers\ThemeServiceProvider::class,
])
->withMiddleware(function (Middleware $middleware) {
// Configure HTTP middleware for WordPress requests
$middleware->wordpress([
Illuminate\Cookie\Middleware\EncryptCookies::class,
Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
Illuminate\Session\Middleware\StartSession::class,
Illuminate\View\Middleware\ShareErrorsFromSession::class,
Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
// You can also configure middleware for web and API routes
// $middleware->web([...]);
// $middleware->api([...]);
})
->withExceptions(function (Exceptions $exceptions) {
// Configure exception handling
// $exceptions->reportable(function (\Throwable $e) {
// Log::error($e->getMessage());
// });
})
->withRouting(
// Configure routing with named parameters
web: base_path('routes/web.php'), // Laravel-style web routes
api: base_path('routes/api.php'), // API routes
wordpress: true, // Enable WordPress request handling
)
->boot();
}, 0);
```
## Add the autoload dump script
Acorn has a function that should be added to the `scripts` section of your `composer.json` file for the `post-autoload-dump` event. To automatically configure this script, run the following command:
```shell
$ wp acorn acorn:install
```
Select **Yes** when prompted to install the Acorn autoload dump script.
::: warning
`wp acorn` commands won't work if your theme/plugin that boots Acorn hasn't been activated and will result in the following message:
**Error: 'acorn' is not a registered wp command.**
:::
Manually adding Acorn's post autoload dump function
Open `composer.json` and add Acorn's `postAutoloadDump` function to Composer's `post-autoload-dump` event in the `scripts`:
```json
"scripts": {
//...
"post-autoload-dump": [
"Roots\\Acorn\\ComposerScripts::postAutoloadDump"
]
}
```
## Server requirements
Acorn's server requirements are minimal, and mostly come from WordPress and [Laravel 13's requirements](https://laravel.com/docs/13.x/deployment#server-requirements).
- PHP >=8.3 with extensions: Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer, XML
- WordPress >= 5.4
- [WP-CLI](https://wordpress.org/cli/)
================================================
FILE: acorn/laravel-cache-alternative-to-wordpress-transients.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2023-01-30 17:32
description: Use Laravel's caching system instead of WordPress Transients. Acorn supports multiple cache drivers including Redis, Memcached, and files.
title: Laravel Cache as an Alternative to WordPress Transients
authors:
- ben
---
# Laravel Cache as an Alternative to WordPress Transients
Acorn provides [Laravel integration with WordPress](/acorn/), which means that certain Laravel components are able to be used within your WordPress site.
Compared to WordPress transients API, [Laravel Cache](https://laravel.com/docs/13.x/cache) provides a more standardized and developer-friendly approach to caching data. It also has a wider range of cache storage options, compared to the WordPress Transients API, which only supports storing data in the WordPress database.
::: tip
Review the [Laravel Cache docs](https://laravel.com/docs/13.x/cache) to get a more detailed understanding about how it works, along with the various ways that the cache can be configured
:::
## Storing data in the cache
```php
use Illuminate\Support\Facades\Cache;
Cache::put('key', 'value', $minutes);
```
## Retrieving data from the cache
```php
use Illuminate\Support\Facades\Cache;
$value = Cache::get('key');
```
## Removing items from the cache
```php
use Illuminate\Support\Facades\Cache;
Cache::forget('key');
```
You can also use Acorn's WP-CLI integration to interact with the cache:
```shell
$ wp acorn cache:clear
```
================================================
FILE: acorn/laravel-redis-configuration.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2025-10-27 10:00
title: Laravel Redis Configuration for Acorn
description: Configure Redis with Laravel and Acorn in WordPress. Enable high-performance caching using PhpRedis or Predis with your WordPress sites.
authors:
- ben
- Log1x
---
# Laravel Redis Configuration for Acorn
Acorn provides [Laravel integration with WordPress](/acorn/), which means that Laravel's Redis setup can be configured to work on your WordPress sites.
We recommend referencing the [Laravel docs on Redis](https://laravel.com/docs/13.x/redis) for a complete understanding of the integration.
## Requirements
The [PhpRedis PECL extension](https://github.com/phpredis/phpredis), or the [`predis/predis`](https://github.com/predis/predis) package are required in order to use Redis.
## Configuration
Add the Laravel Redis package as a dependency:
```shell
$ composer require illuminate/redis
```
Update `config/app.php` to add the Redis Service Provider:
```diff
Roots\Acorn\Providers\AcornServiceProvider::class,
Roots\Acorn\Providers\RouteServiceProvider::class,
Roots\Acorn\View\ViewServiceProvider::class,
+ Illuminate\Redis\RedisServiceProvider::class,
```
Update `config/app.php` to add the Redis facade:
```diff
'aliases' => Facade::defaultAliases()->merge([
// 'ExampleClass' => App\Example\ExampleClass::class,
+ 'Redis' => Illuminate\Support\Facades\Redis::class
])->toArray(),
```
================================================
FILE: acorn/logging.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2021-10-21 13:21
description: Acorn provides Laravel's logging services for WordPress. Configure multiple channels and send logs to files, syslog, Slack, and custom handlers.
title: Laravel Logging in WordPress
authors:
- ben
---
# Laravel Logging in WordPress
::: tip
We recommend referencing the [Laravel docs on Logging](https://laravel.com/docs/13.x/logging)
:::
The location of your application logs depends on your [directory structure](/acorn/docs/directory-structure/).
For zero-config setups, logs live at `[wp-content]/cache/acorn/logs/`.
For traditional setups, logs live at `storage/logs/`.
## Basic PHP logging example
```php
use Illuminate\Support\Facades\Log;
Log::debug('👋 Howdy');
```
## Basic Blade logging example
```blade
{{ logger('👋 Howdy') }}
```
================================================
FILE: acorn/package-development.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2021-10-21 13:21
title: Developing Packages for Acorn
description: Use the Acorn Example Package as a template for creating custom packages and reusable functionality for WordPress with Laravel architecture.
authors:
- ben
- Log1x
---
# Developing Packages for Acorn
We have an [Acorn Example Package](https://github.com/roots/acorn-example-package) repo that can be used as a template for creating your own Acorn packages. It's similar to some of the other Laravel package templates out there, but more specific to Acorn.
Creating Acorn packages is useful for when you want to reuse specific functionality on your Acorn-powered WordPress sites, or open-sourcing functionality that's not tied directly to your site. You can think of Acorn packages similiar to WordPress plugins, or any other dependency.
Packages are installed by Composer, just like Acorn is.
::: tip
We recommend referencing the [Laravel docs on Packages](https://laravel.com/docs/13.x/packages)
:::
## Creating an Acorn package
From the [roots/acorn-example-package](https://github.com/roots/acorn-example-package) repo, click the **Use this template** button to create a new repo with the template.
After cloning your new repo, run the configure script to replace the placeholder names with your own:
```shell
$ php configure.php
```
The script will prompt you for your vendor name, package name, namespace, and other details. You can also run it non-interactively:
```shell
$ php configure.php --no-interaction --author-name="Your Name" --author-email="you@example.com" --vendor-slug="your-vendor" --vendor-namespace="YourVendor" --package-slug="your-package" --class-name="YourPackage" --package-description="Your package description"
```
To preview changes without modifying any files, use `--dry-run`:
```shell
$ php configure.php --dry-run
```
## Developing an Acorn package
Once your package is created, clone your new git repo somewhere on your machine that's accessible from a WordPress site with Acorn installed. To work on a package locally, you can require it by defining a new local repository from the `composer.json` file used for your site/theme:
```json
"repositories": [
{
"type": "path",
"url": "./packages/vendor-name/example-package"
}
],
```
Replace `./packages/vendor-name/example-package` above with the path to your local package, along with the correct names.
Then require the package in your project:
```shell
$ composer require vendor-name/example-package
```
Then run the Acorn WP-CLI command to discover your package:
```shell
$ wp acorn package:discover
```
```plaintext
INFO Discovering packages.
vendor-name/example-package ...... DONE
roots/sage ....................... DONE
```
::: tip
If you haven't already, run `php configure.php` from the root of your package to replace the placeholder names
:::
================================================
FILE: acorn/rendering-blade-views.md
================================================
---
date_modified: 2026-02-02 12:00
date_published: 2023-02-21 11:30
description: Render Blade templates anywhere in WordPress using the `view()` helper function. Examples for Gutenberg blocks, ACF blocks, and email notifications.
title: Rendering Blade Views in WordPress
authors:
- ben
- chuckienorton
- rafaucau
- strarsis
- talss89
---
# Rendering Blade Views in WordPress
You can use the `view()` helper function from Acorn to render Blade templates anywhere in your WordPress site.
## Rendering blocks with Blade templates
### First-party blocks
In the following example we'll render a `vendor/example` block with `resources/views/blocks/example.blade.php`:
```php
register_block_type('vendor/example', [
'render_callback' => function ($attributes, $content) {
return view('blocks/example', compact('attributes', 'content'));
},
]);
```
In the following example register an ACF block named `example` and render it with `resources/views/blocks/example.blade.php`:
### ACF blocks with Blade templates
```php
acf_register_block_type([
'example',
'render_callback' => function ($block) {
echo view('blocks/example', ['block' => $block]);
},
]);
```
### Existing blocks with Blade templates
In the following example we'll render the `core/buttons` block with `resources/views/blocks/button.blade.php`:
```php
add_filter('register_block_type_args', function ($args, $name) {
if ($name === 'core/buttons') {
$args['render_callback'] = function ($attributes, $content) {
return view('blocks/buttons', compact('attributes', 'content'));
};
}
return $args;
}, 10, 2);
```
### block.json `render` field with Blade templates
If you're registering blocks using `block.json` with a `render` field pointing to a Blade template (e.g. `"render": "file:./render.blade.php"`), you can automatically handle the rendering with a single filter:
```php
add_filter('register_block_type_args', function (array $args, string $name): array {
if (empty($args['render_callback']) || ! ($args['render_callback'] instanceof \Closure)) {
return $args;
}
$reflector = new \ReflectionFunction($args['render_callback']);
$renderCallbackVariables = $reflector->getStaticVariables();
if (array_key_exists('template_path', $renderCallbackVariables) && str_ends_with($renderCallbackVariables['template_path'], '.blade.php')) {
$args['render_callback'] = function ($attributes, $content, $block) use ($renderCallbackVariables) {
return view()
->file($renderCallbackVariables['template_path'], compact('attributes', 'content', 'block'))
->render();
};
}
return $args;
}, 1, 2);
```
## Rendering emails with Blade templates
The following example uses the `resources/views/emails/welcome.blade.php` template file customizing the new WordPress user notification emails:
```php
add_filter('wp_new_user_notification_email', function ($wp_new_user_notification_email, $user, $blogname) {
$key = get_password_reset_key($user);
$encoded_user_login = rawurlencode($user->user_login);
$password_reset_link = network_site_url('wp-login.php?action=rp&key='.$key.'&login='.$encoded_user_login, 'login');
$message = view('emails/welcome', compact('user', 'blogname', 'password_reset_link'))->render();
$wp_new_user_notification_email['message'] = $message;
$wp_new_user_notification_email['headers'] = ['Content-Type: text/html; charset=UTF-8'];
return $wp_new_user_notification_email;
}, 10, 3);
```
================================================
FILE: acorn/routing.md
================================================
---
date_modified: 2025-03-07 09:00
date_published: 2024-06-03 15:00
description: Add Laravel's routing system to WordPress with Acorn. Create custom routes with parameters, controllers, and middleware for advanced applications.
title: Laravel Routing in WordPress
authors:
- ben
---
# Laravel Routing in WordPress
::: tip
See [Laravel's routing documentation](https://laravel.com/docs/10.x/routing) to better understand how routing works in Acorn
:::
Acorn allows you to use Laravel's routing functionality on your WordPress sites, and will automatically handle Laravel routes defined in the `routes/web.php` file if it exists.
Routes are an easier way to implement virtual pages in WordPress.
## Basic routing example
### Create the route file
Create `routes/web.php` with the following:
```php
name('welcome');
```
### Create the view file
Create `resources/views/welcome.blade.php` with the following:
```blade
@extends('layouts.app')
@section('content')
Welcome
@endsection
```
## Update Acorn's configuration
Find where `Application::configure` is used in your setup. On a Sage theme, this would be `functions.php`.
Add `->withRouting(web: base_path('routes/web.php'))`:
```diff
Application::configure()
->withProviders([
App\Providers\ThemeServiceProvider::class,
])
+ ->withRouting(web: base_path('routes/web.php'))
->boot();
```
See [Advanced booting](/acorn/docs/installation/#advanced-booting) for more examples.
## Configuring SEO elements
Since registered routes are dynamic, WordPress is not aware of how to handle some SEO elements and functionality:
* Setting the canonical URL
* Setting the ``
* Adding SEO-related meta data
* Adding pages to the sitemap
[Laravel's `Route` facade allows you to access information about the route](https://laravel.com/docs/11.x/routing#accessing-the-current-route), which can be used with hooks to populate this data:
```php
/**
* Set the page for the welcome route
*/
add_filter('pre_get_document_title', function ($title) {
$name = Route::currentRouteName();
if ($name === 'welcome') {
return 'Welcome Page';
}
return $name;
});
```
## Advanced routing features
For more complex applications, you can use:
- **[Controllers, Middleware, and HTTP Kernel](controllers-middleware-kernel.md)** - Organize route logic with controllers, filter requests with middleware, and customize the HTTP kernel
- **[Eloquent Models](eloquent-models.md)** - Work with WordPress data using Laravel's ORM in your controllers
### Using controllers
Instead of defining route logic directly in your routes file, you can organize it into controller classes:
```php
group(function () {
Route::post('/api/posts', [PostController::class, 'store']);
Route::put('/api/posts/{id}', [PostController::class, 'update']);
});
```
## Route caching
If you're using routes then you should enable [Laravel's route cache](https://laravel.com/docs/10.x/routing#route-caching) during your deployment process:
```shell
$ wp acorn route:cache
```
================================================
FILE: acorn/upgrading-acorn.md
================================================
---
date_modified: 2026-03-22 12:00
date_published: 2023-01-13 13:12
description: Learn how to upgrade Acorn to the latest version with guidance on breaking changes, dependency requirements, and configuration updates.
title: Upgrading Acorn to the Latest Version
authors:
- ben
- chrillep
- joshf
---
# Upgrading Acorn to the Latest Version
## Upgrading to v6.x from v5.x
Acorn v6 includes Laravel v13 components, whereas Acorn v5 includes Laravel v12 components.
### Upgrading dependencies
Acorn v6 requires PHP >= 8.3.
Update the `roots/acorn` dependency in your `composer.json` file to `^6.0`:
```shell
$ composer require roots/acorn ^6.0 -W
```
The `-W` flag is required to upgrade the included Laravel dependencies.
::: warning
If any packages/dependencies have conflicts while updating, try removing and then re-requiring them after Acorn is bumped to 6.x.
:::
### Breaking changes
#### Cache, session, and Redis prefix separators
The default prefix/cookie separators have changed from underscores to hyphens to match Laravel 13 defaults. This means:
- **Cache prefix**: `laravel_cache_` → `laravel-cache-`
- **Session cookie**: `laravel_session` → `laravel-session`
- **Redis prefix**: `laravel_database_` → `laravel-database-`
This will **invalidate existing caches and log out all sessions** unless you have explicitly set these values via environment variables (`CACHE_PREFIX`, `SESSION_COOKIE`, `REDIS_PREFIX`).
To preserve existing behavior, add these to your `.env`:
```plaintext
CACHE_PREFIX=your_app_name_cache_
SESSION_COOKIE=your_app_name_session
REDIS_PREFIX=your_app_name_database_
```
#### Mail configuration
The SMTP `encryption` key has been replaced with `scheme`:
```diff
'smtp' => [
'transport' => 'smtp',
- 'encryption' => env('MAIL_ENCRYPTION', 'tls'),
+ 'scheme' => env('MAIL_SCHEME'),
],
```
If you are using the `MAIL_ENCRYPTION` environment variable, rename it to `MAIL_SCHEME`.
If you use [`roots/acorn-mail`](https://github.com/roots/acorn-mail), bump it to `^2.0` — earlier versions read the removed `encryption` key and will silently ignore `MAIL_SCHEME`:
```shell
$ composer require roots/acorn-mail ^2.0
```
#### Logging configuration
The stderr channel's `with` key has been renamed to `handler_with`:
```diff
'stderr' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
- 'with' => [
+ 'handler_with' => [
'stream' => 'php://stderr',
],
],
```
### Config changes
If you have published Acorn's configs, you should review and update them based on the latest versions in the [Acorn repo](https://github.com/roots/acorn/tree/main/config). Notable changes include:
- **cache.php**: New `serializable_classes` option
- **session.php**: New `serialization` option
- **database.php**: New Redis retry/backoff keys, SQLite `transaction_mode`, SSL CA guard updated for PHP 8.5
- **mail.php**: New `resend` and `roundrobin` mailers, `retry_after` on failover, `markdown` section removed
- **services.php**: Postmark and Resend env variable names updated
- **All configs**: `(string)` casts added to `env()` calls per Laravel 13 conventions
## Upgrading to v5.x from v4.x
Acorn v5 includes Laravel v12 components, whereas Acorn v4 includes Laravel v10 components.
### Upgrading dependencies
Acorn v5 requires PHP >= 8.2.
Update the `roots/acorn` dependency in your `composer.json` file to `^5.0`:
```shell
$ composer require roots/acorn ^5.0 -W
```
The `-W` flag is required to upgrade the included Laravel dependencies.
::: warning
If any packages/dependencies have conflicts while updating, try removing and then re-requiring them after Acorn is bumped to 5.x.
:::
### Breaking changes
The most significant change in v5 is how Acorn is booted. The `bootloader()` helper has been deprecated in favor of using `Application::configure()`. This change aligns Acorn with Laravel 11's new application configuration system, providing a more fluent and powerful way to configure your application.
You'll need to import the Application class at the top of your file:
```php
use Roots\Acorn\Application;
```
Then update your bootstrapping code:
```diff
- add_action('after_setup_theme', fn () => \Roots\bootloader()->boot(), 0);
+ add_action('after_setup_theme', function () {
+ Application::configure()
+ ->withProviders([
+ App\Providers\ThemeServiceProvider::class,
+ ])
+ ->boot();
+ }, 0);
```
If you have previously registered service providers through either `composer.json` (`extra.acorn.providers`) or `config/app.php`, you'll need to migrate these to the new configuration method. All providers should now be registered using `withProviders()` when configuring the application. Remove any provider configurations from your composer.json and config files, and instead register them directly in your bootstrapping code:
```php
Application::configure()
->withProviders([
App\Providers\ThemeServiceProvider::class,
App\Providers\ExampleServiceProvider::class,
])
->boot();
```
### Routing
Acorn v5 introduces support for Laravel’s routing features within WordPress. If you previously used Livewire, you may encounter an error such as `Route [livewire.update] not defined`, or experience other routing-related issues.
To resolve this, and to enable routing, ensure your application is properly configured by adding the `withRouting` method:
```diff
Application::configure()
->withProviders([
App\Providers\ThemeServiceProvider::class,
])
+ ->withRouting(wordpress: true)
->boot();
```
### Config changes
If you have published Acorn's configs, you should review and update them based on the latest versions in the [Acorn repo](https://github.com/roots/acorn/tree/main/config).
## Upgrading to v4.x from v3.x
Acorn v4 includes Laravel v10 components, whereas Acorn v3 includes Laravel v9 components.
### Upgrading dependencies
Acorn v4 requires PHP >= 8.1.
Update the `roots/acorn` dependency in your `composer.json` file to `^4.0`:
```shell
$ composer require roots/acorn ^4.0 -W
```
The `-W` flag is required to upgrade the included Laravel dependencies.
::: warning
If any packages/dependencies have conflicts while updating, try removing and then re-requiring them after Acorn is bumped to 4.x.
:::
### Config changes
If you previously published Acorn's config(s), you will need to update them based on the configs in the [Acorn repo](https://github.com/roots/acorn/tree/main/config) ([history](https://github.com/roots/acorn/commits/main/config?since=2023-11-01&until=2024-01-31)). You mainly need the [new provider changes](https://github.com/roots/acorn/blob/v4.0.0/config/app.php#L160-L169) if you published `config/app.php`.
```diff
+ use Roots\Acorn\ServiceProvider;
- 'timezone' => get_option('timezone_string', 'UTC'),
+ 'timezone' => get_option('timezone_string') ?: 'UTC',
- 'providers' => [
+ 'providers' => ServiceProvider::defaultProviders()->merge([
-
- /*
- * Framework Service Providers...
- */
- Illuminate\Auth\AuthServiceProvider::class,
- Illuminate\Broadcasting\BroadcastServiceProvider::class,
- Illuminate\Bus\BusServiceProvider::class,
- // ...
- Roots\Acorn\Providers\AcornServiceProvider::class,
- Roots\Acorn\Providers\RouteServiceProvider::class,
- Roots\Acorn\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
/*
* Application Service Providers...
*/
// App\Providers\ThemeServiceProvider::class,
- ],
+ ])->toArray(),
```
### Breaking changes
The breaking changes this time are minimal and should not impact most users.
Service providers should now extend Illuminate:
```diff
- use Roots\Acorn\ServiceProvider;
+ use Illuminate\Support\ServiceProvider;
```
View Composer `Arrayable` trait uses property [`Composer::$except`](https://github.com/roots/acorn/blob/70d179955cddc61f0c6101717af2fdf88cf38831/src/Roots/Acorn/View/Composer.php#L35-L54) instead of `Arrayable::$ignore`.
```diff
class Alert extends Composer
{
use Arrayable;
- $ignore = ['token'];
+ $except = ['token'];
}
```
Asset Contract adds [`relativePath()` method](https://github.com/roots/acorn/blob/70d179955cddc61f0c6101717af2fdf88cf38831/src/Roots/Acorn/Assets/Contracts/Asset.php#L38). So if you're implementing this contract, you'll need to update it. (Most users will not be impacted by this.)
```diff
class MyAsset implements Asset
{
+ relativePath(string $base_path): string
+ {
+ // ...
+ }
}
```
## Upgrading to v3.x from v2.x
Acorn v3 includes Laravel v9 components, whereas Acorn v2 includes Laravel v8 components.
### Upgrading dependencies
Acorn v3 requires PHP >= 8.0.2.
Update the `roots/acorn` dependency in your `composer.json` file to `^3.0`:
```shell
$ composer require roots/acorn ^3.0 -W
```
The `-W` flag is required to upgrade the included Laravel dependencies.
### Theme/application
Acorn v2 is typically booted in your WordPress theme's `functions.php` file. Look for the line that includes `\Roots\bootloader()`, and replace it with `\Roots\bootloader()->boot()`.
```diff
-\Roots\bootloader();
+\Roots\bootloader()->boot();
```
We highly recommend removing the exception from bootloader to prevent service providers from silently skipping on local dev, a change that was introduced in Acorn v3.1.0 ([PR #266](https://github.com/roots/acorn/pull/266)) and Sage v10.5.1 ([PR #3121](https://github.com/roots/sage/pull/3121/files)). Replace the original bootloader method:
```php
try {
\Roots\bootloader()->boot();
} catch (Throwable $e) {
wp_die('You need to install Acorn to use this theme.'),
...
}
```
With the new one:
```php
if (! function_exists('\Roots\bootloader')) {
wp_die(
__('You need to install Acorn to use this theme.', 'sage'),
'',
[
'link_url' => 'https://roots.io/acorn/docs/installation/',
'link_text' => __('Acorn Docs: Installation', 'sage'),
]
);
}
add_action('after_setup_theme', fn () => \Roots\bootloader()->boot(), 0);
```
You can also remove the theme support added for Sage if you are working on a Sage-based WordPress theme:
```diff
-add_theme_support('sage');
```
#### Target class [sage.view] does not exist
Some setups may require changes if you run into the following error:
```plaintext
Target class [sage.view] does not exist
```
In this case, edit the `ThemeServiceProvider` and make sure it extends `SageServiceProvider` and has `parent::` calls to `register()` and `boot()` if they are present:
```diff
# app/Providers/ThemeServiceProvider.php
namespace App\Providers;
-use Roots\Acorn\ServiceProvider;
+use Roots\Acorn\Sage\SageServiceProvider;
-class ThemeServiceProvider extends ServiceProvider
+class ThemeServiceProvider extends SageServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
- //
+ parent::register();
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
- //
+ parent::boot();
}
}
```
After doing so, you may need to delete [Acorn's application cache directory](https://roots.io/acorn/docs/directory-structure/). By default, this is located in `[wp-content|app]/cache/acorn/`.
Reference the [Acorn v3 upgrade pull request on the Sage repo](https://github.com/roots/sage/pull/3097) to see a full diff.
#### Target class [assets.manifest] does not exist
Some setups may require changes if you run into the following error:
```plaintext
Target class [assets.manifest] does not exist
```
This error can be fixed by copying over the latest changes to the [`config/app.php` file](https://github.com/roots/acorn/blob/67cce76e6ca13e28acaced3333d77e2f779b07a3/config/app.php) from Acorn.
================================================
FILE: acorn/using-livewire-with-wordpress.md
================================================
---
date_modified: 2025-03-06 07:00
date_published: 2024-03-05 16:41
description: Use Laravel Livewire with Acorn to create reactive, dynamic components in WordPress themes and plugins without complex JavaScript frameworks.
title: Using Livewire with WordPress
authors:
- ben
- Log1x
---
# Using Livewire with WordPress
With the release of Acorn v4 came the final implementations needed for [Livewire](https://livewire.laravel.com/) support alongside your Acorn-powered WordPress themes and plugins.
In this guide, we will walk through installing Livewire and using a component in a [Sage 11](https://roots.io/sage/) theme.
## Installing Livewire
Start by installing Livewire alongside where you installed Acorn:
```bash
$ composer require livewire/livewire
```
Once installed, Livewire requires you have an `APP_KEY` set in your environment. You can generate this using Acorn's CLI:
```bash
$ wp acorn key:generate
```
## Enqueueing Livewire
Adding the Livewire styles and scripts can be done using the `@livewireStyles` and `@livewireScripts` directives.
This can be done by [manually inserting](https://livewire.laravel.com/docs/installation#manually-including-livewires-frontend-assets) them inside of `resources/views/layouts/app.blade.php`:
```blade
...
@livewireStyles
...
@livewireScripts
```
## Update Acorn's configuration
Find where `Application::configure` is used in your setup. On a Sage theme, this would be `functions.php`. On a Radicle setup, this would be in `mu-plugins/00-acorn-boot.php`.
Add `->withRouting(wordpress: true)`:
```diff
Application::configure()
->withProviders([
App\Providers\ThemeServiceProvider::class,
])
+ ->withRouting(wordpress: true)
->boot();
```
See [Advanced booting](/acorn/docs/installation/#advanced-booting) for more examples.
## Creating a Component
For this example, we will create a simple searchable **Post List** component. Start by generating the component using Acorn's CLI:
```sh
$ wp acorn make:livewire PostList
```
Inside of `app/Livewire/PostList.php`, we can create a `$query` property to hold our search term and perform a simple `get_posts()` with the query if it is not empty:
```php
query ? get_posts([
'post_type' => 'post',
'post_status' => 'publish',
's' => $this->query,
]) : [];
$posts = collect($posts);
return view('livewire.post-list', compact('posts'));
}
}
```
In `resources/views/livewire/post-list.blade.php`, we can add some simple markup consisting of an `` for the search `$query` and a loop of any found posts:
```php
@if ($query)
@if ($posts)
Found {{ $posts->count() }} result(s) for "{{ $query }}"
```
Once done, you can add the Livewire component into one of your existing Blade views/templates:
```php
```
To learn more about Livewire, head over to the official [Livewire Documentation](https://livewire.laravel.com/docs/quickstart).
================================================
FILE: acorn/wp-cli.md
================================================
---
date_modified: 2026-03-10 12:00
date_published: 2021-11-19 11:58
description: Acorn provides WP-CLI commands similar to Laravel's `artisan` for managing WordPress. Clear caches, compile views, and run administrative tasks.
title: WP-CLI Commands for Acorn
authors:
- alwaysblank
- ben
- QWp6t
---
# WP-CLI Commands for Acorn
Acorn comes with WP-CLI commands similar to Laravel's `artisan` CLI.
## Available commands
| Command | Description |
| --- | --- |
| `wp acorn about` | Display basic information about your application |
| `wp acorn clear-compiled` | Remove the compiled class file |
| `wp acorn completion` | Dump the shell completion script |
| `wp acorn db` | Start a new database CLI session |
| `wp acorn env` | Display the current framework environment |
| `wp acorn help` | Display help for a command |
| `wp acorn list` | List commands |
| `wp acorn migrate` | Run the database migrations |
| `wp acorn optimize` | Cache framework bootstrap, configuration, and metadata to increase performance |
| `wp acorn test` | Run the application tests |
| `wp acorn acorn:init` | Initializes required paths in the base directory |
| `wp acorn acorn:install` | Install Acorn into the application |
| `wp acorn cache:clear` | Flush the application cache |
| `wp acorn cache:forget` | Remove an item from the cache |
| `wp acorn config:cache` | Create a cache file for faster configuration loading |
| `wp acorn config:clear` | Remove the configuration cache file |
| `wp acorn db:seed` | Seed the database with records |
| `wp acorn db:table` | Display information about the given database table |
| `wp acorn db:wipe` | Drop all tables, views, and types |
| `wp acorn key:generate` | Set the application key |
| `wp acorn make:command` | Create a new Artisan command |
| `wp acorn make:component` | Create a new view component class |
| `wp acorn make:composer` | Create a new view composer class |
| `wp acorn make:controller` | Create a new controller class |
| `wp acorn make:job` | Create a new job class |
| `wp acorn make:middleware` | Create a new HTTP middleware class |
| `wp acorn make:migration` | Create a new migration file |
| `wp acorn make:model` | Create a new Eloquent model class |
| `wp acorn make:provider` | Create a new service provider class |
| `wp acorn make:queue-batches-table` | Create a migration for the batches database table |
| `wp acorn make:queue-failed-table` | Create a migration for the failed queue jobs database table |
| `wp acorn make:queue-table` | Create a migration for the queue jobs database table |
| `wp acorn make:seeder` | Create a new seeder class |
| `wp acorn migrate:fresh` | Drop all tables and re-run all migrations |
| `wp acorn migrate:install` | Create the migration repository |
| `wp acorn migrate:refresh` | Reset and re-run all migrations |
| `wp acorn migrate:reset` | Rollback all database migrations |
| `wp acorn migrate:rollback` | Rollback the last database migration |
| `wp acorn migrate:status` | Show the status of each migration |
| `wp acorn optimize:clear` | Remove the cached bootstrap files |
| `wp acorn package:discover` | Rebuild the cached package manifest |
| `wp acorn queue:clear` | Delete all of the jobs from the specified queue |
| `wp acorn queue:failed` | List all of the failed queue jobs |
| `wp acorn queue:flush` | Flush all of the failed queue jobs |
| `wp acorn queue:forget` | Delete a failed queue job |
| `wp acorn queue:listen` | Listen to a given queue |
| `wp acorn queue:monitor` | Monitor the size of the specified queues |
| `wp acorn queue:pause` | Pause job processing for a specific queue |
| `wp acorn queue:prune-batches` | Prune stale entries from the batches database |
| `wp acorn queue:prune-failed` | Prune stale entries from the failed jobs table |
| `wp acorn queue:restart` | Restart queue worker daemons after their current job |
| `wp acorn queue:resume` | Resume job processing for a paused queue |
| `wp acorn queue:retry` | Retry a failed queue job |
| `wp acorn queue:retry-batch` | Retry the failed jobs for a batch |
| `wp acorn queue:work` | Start processing jobs on the queue as a daemon |
| `wp acorn route:cache` | Create a route cache file for faster route registration |
| `wp acorn route:clear` | Remove the route cache file |
| `wp acorn route:list` | List all registered routes |
| `wp acorn schedule:clear-cache` | Delete the cached mutex files created by scheduler |
| `wp acorn schedule:interrupt` | Interrupt the current schedule run |
| `wp acorn schedule:list` | List all scheduled tasks |
| `wp acorn schedule:run` | Run the scheduled commands |
| `wp acorn schedule:test` | Run a scheduled command |
| `wp acorn schedule:work` | Start the schedule worker |
| `wp acorn vendor:publish` | Publish any publishable assets from vendor packages |
| `wp acorn view:cache` | Compile all of the application's Blade templates |
| `wp acorn view:clear` | Clear all compiled view files |
================================================
FILE: bedrock/auditing-wordpress-vulnerabilities-with-composer.md
================================================
---
date_modified: 2026-05-03 12:00
date_published: 2026-05-03 12:00
description: Audit WordPress plugins and themes for known vulnerabilities with Composer using WP Sec Adv, a security advisory repository sourced from Wordfence Intelligence.
title: Auditing WordPress Vulnerabilities with Composer
authors:
- ben
---
# Auditing WordPress Vulnerabilities with Composer
`composer audit` reports known vulnerabilities for PHP packages on Packagist, but it has no awareness of WordPress plugin and theme advisories. [WP Sec Adv](https://github.com/typisttech/wpsecadv) closes that gap by exposing WordPress security data — sourced from the [Wordfence Intelligence](https://www.wordfence.com/help/wordfence-intelligence/v3-accessing-and-consuming-the-vulnerability-data-feed/) feed — as a Composer repository.
Once added, Composer treats WordPress advisories the same as any other:
- `composer audit` reports known vulnerabilities in installed WordPress packages
- `composer require` and `composer update` block installation of vulnerable packages
- Advisories include CVEs, severity ratings, and links to vulnerability reports
The advisory data refreshes twice daily.
## Adding the repository
From your Bedrock project root:
```shell
$ composer repo --append add wpsecadv composer https://repo-wpsecadv.typist.tech
```
Composer will now check for WordPress vulnerabilities during `install`, `require`, `update`, and `audit`.
## Package support
WP Sec Adv matches advisories to Composer packages by slug, with built-in support for:
- [WordPress plugin and theme packages](https://wp-packages.org/)
- [WordPress core packages](https://wp-packages.org/wordpress-core)
Unrecognized vendors still attempt to match against known plugin and theme slugs, so custom mirrors and private registries work too.
## Ignoring advisories
Not every advisory requires immediate action. Composer lets you acknowledge specific advisories with a documented reason:
```json
{
"config": {
"audit": {
"ignore": {
"CVE-2026-3589": {
"apply": "block",
"reason": "Waiting for upstream fix in v1.2.3. Allow during updates but still report in audits"
}
}
}
}
}
```
Every exception is tracked in `composer.json`, keeping your security posture intentional rather than reactive.
## Auditing in CI
Pair WP Sec Adv with a CI step to audit your lockfile on every push. For GitHub Actions:
```yaml
- name: Audit
run: composer audit --locked
```
This gives you continuous vulnerability monitoring for both PHP and WordPress dependencies with no additional tooling.
::: tip
WP Sec Adv is maintained by [Tang Rufus](https://github.com/tangrufus). If it's useful to your projects, consider [sponsoring his work](https://github.com/sponsors/tangrufus).
:::
================================================
FILE: bedrock/bedrock-with-ddev.md
================================================
---
date_modified: 2024-07-09 18:30
date_published: 2023-02-19 12:16
description: Set up DDEV for Bedrock WordPress development using Docker. Configure docroot to `web/` directory and adjust DDEV services for Bedrock's structure.
title: Bedrock Local Development with DDEV
authors:
- ben
---
# Bedrock Local Development with DDEV
[DDEV](https://ddev.readthedocs.io/en/stable/) is a local PHP development environment. In this guide you will learn how to setup a Bedrock-based WordPress site with DDEV.
## Setting up a Bedrock site
```shell
$ ddev config --project-type=wordpress --docroot=web --create-docroot
```
```shell
$ ddev composer create roots/bedrock
```
## Configure environment variables
Bedrock requires [environment variables to be configured](https://roots.io/bedrock/docs/installation/#getting-started) in order to get started.
The `.env` file must be configured with DDEV's database settings along with your home URL. Update the following values in your `.env` file:
```dotenv
DB_NAME='db'
DB_USER='db'
DB_PASSWORD='db'
DB_HOST='db'
WP_HOME="${DDEV_PRIMARY_URL}"
WP_SITEURL="${DDEV_PRIMARY_URL}/wp"
```
After configuring the environment variables, run `ddev start`. Your site will be accessible at `https://ddevtest.ddev.site/`.
================================================
FILE: bedrock/bedrock-with-devkinsta.md
================================================
---
date_modified: 2023-02-19 12:16
date_published: 2023-02-19 12:16
description: Set up DevKinsta for Bedrock WordPress development. Configure site settings and document root to work with Bedrock's unique `wp/` and `app/` directories.
title: Bedrock Development with DevKinsta
authors:
- ben
---
# Bedrock Development with DevKinsta
[DevKinsta](https://kinsta.com/devkinsta/?kaid=OFDHAJIXUDIV) is a local WordPress development environment. In this guide you will learn how to setup a Bedrock-based WordPress site with DevKinsta.
## Create a new site
1. Create a new site from the DevKinsta interface using the **Custom site** option
2. Select the **Empty site** option
In this guide, we'll use `example` as the site name.
## Installing Bedrock from the terminal
Navigate to the site path for your DevKinsta site:
```shell
$ cd ~/DevKinsta/public/example
```
Once you are in the `example/` folder for your DevKinsta site, either install Bedrock with Composer or clone your existing git repository into this directory:
```shell
$ composer create-project roots/bedrock
```
Your folder structure should now look like this:
```plaintext
# @ ~/DevKinsta/
.
├── kinsta
├── logs
├── nginx_sites
├── private
├── public
│ └── example
│ ├── bedrock
│ └── index.html
├── ssl
└── wp
```
## Configure environment variables
Bedrock requires [environment variables to be configured](https://roots.io/bedrock/docs/installation/#getting-started) in order to get started.
The `.env` file in the `app/bedrock/` directory must be configured with Local's database settings along with your home URL. Update the following values in your `.env` file:
```dotenv
DB_NAME='example'
DB_USER='root'
DB_PASSWORD='password'
DB_HOST='devkinsta_db'
WP_HOME='http://example.local'
```
Make sure to populate the `DB_PASSWORD` based on the provided password in the DevKinsta interface for your site.
## Set the webroot in DevKinsta's site config
DevKinsta's site config is located at `~/DevKinsta/nginx_sites/example.conf`. Open this file and modify the`root` path:
```diff
-root /www/kinsta/public/example;
+root /www/kinsta/public/example/bedrock/web;
```
You will need to restart your site after making these changes, and then your site will be accessible at `http://example.local`.
================================================
FILE: bedrock/bedrock-with-lando.md
================================================
---
date_modified: 2026-03-08 10:00
date_published: 2023-02-19 12:16
description: Set up Lando for Bedrock WordPress development. Configure webroot to `web/` directory and adjust Lando settings for Bedrock's unique folder structure.
title: Bedrock Local Development with Lando
authors:
- ben
- james0r
---
# Bedrock Local Development with Lando
[Lando](https://lando.dev/) is a local development environment. In this guide you will learn how to setup a Bedrock-based WordPress site with Lando.
## Configuring a Lando recipe for Bedrock
After [installing Bedrock](/bedrock/docs/installation/), you can either use `lando init` to create the recipe, or you can just drop in the contents of the recipe file that you will find below within a file called `.lando.yml`.
To use the CLI, run `lando init --recipe wordpress` and answer the following prompts:
* From where should we get your app's codebase? **current working directory**
* Where is your webroot relative to the init destination? **web**
* What do you want to call this app? **bedrock**
Or, just drop in the following `.lando.yml` file in the root of your Bedrock directory:
```yaml
# .lando.yml
name: bedrock
recipe: wordpress
config:
webroot: web
services:
appserver:
type: php:8.3 # Bedrock requires PHP >= 8.3
```
## Configure environment variables
Bedrock requires [environment variables to be configured](https://roots.io/bedrock/docs/installation/#getting-started) in order to get started.
The `.env` file must be configured with Lando's database settings along with your home URL. Update the following values in your `.env` file:
```dotenv
DB_NAME='wordpress'
DB_USER='wordpress'
DB_PASSWORD='wordpress'
DB_HOST='database'
WP_HOME='https://bedrock.lndo.site'
```
## Setup trusted certificates
Make sure to follow the instructions in the Lando docs for [Trusting the CA](https://docs.lando.dev/config/security.html#trusting-the-ca) to avoid warnings on your browser when visiting your site.
## Start your Lando site
Run `lando start`, and then your site will be accessible from `https://bedrock.lndo.site/`.
================================================
FILE: bedrock/bedrock-with-local.md
================================================
---
date_modified: 2026-03-10 17:00
date_published: 2023-02-19 12:16
description: Configure Local for Bedrock WordPress development. Adjust document root to `web/` directory and configure Local for Bedrock's structure.
title: Using Bedrock with Local
authors:
- ben
- ethanclevenger91
---
# Using Bedrock with Local
[Local](https://localwp.com/), previously known as Local by Flywheel, is one of the many local development tools available for WordPress developers. In this guide you will learn how to configure Local for a Bedrock-based WordPress site.
Bedrock sites are structured in a way that your [entire WordPress site is managed from a git repository](https://roots.io/bedrock/docs/folder-structure/). Local's workflow isn't friendly towards this approach, but it's still possible to configure Local to work with Bedrock sites.
## Create a new site
Create a new site from the Local interface. In this guide, we'll use `bedrock` as the site name.
## Installing Bedrock from the terminal
From your new Local site, click **Open site shell**. When the terminal opens, you should be under `/Local Sites/bedrock/app/public`.
First, remove the default WordPress installation that is in the public folder:
```shell
rm -rf *
rm .htaccess
```
This will remove all content of the public folder.
Now install Bedrock with Composer into the public directory or clone your existing git repository into this directory:
```shell
composer create-project roots/bedrock .
```
## Configure environment variables
Bedrock requires environment variables to be configured in order to get started.
First, copy the example environment file:
```shell
cp .env.example .env
```
The `.env` file must be configured with Local's database settings along with your home URL. Update the following values in your `.env` file:
```plaintext
DB_NAME='local'
DB_USER='root'
DB_PASSWORD='root'
WP_HOME='https://bedrock.local'
```
For Local WP these are the default DB credentials. If you changed them manually, then you need to change them here accordingly. The `WP_HOME` should be the website URL we configured in Local - in our case here it's `bedrock.local`.
## Set the webroot in Local's site config
Local's site config is located at `~/Local Sites/bedrock/conf/nginx/site.conf.hbs`. Open this file and append `/web` to the server root:
```diff
server {
listen {{port}};
- root "{{root}}";
+ root "{{root}}/web";
```
You will need to restart your site after making these changes, and then your site will be accessible at `https://bedrock.local`.
================================================
FILE: bedrock/bedrock-with-valet.md
================================================
---
date_modified: 2023-03-08 8:55
date_published: 2023-02-19 12:16
description: Set up Laravel Valet for Bedrock WordPress development on macOS. Configure Valet drivers and local domains for seamless development workflow.
title: Using Bedrock with Laravel Valet
authors:
- ben
---
# Using Bedrock with Laravel Valet
[Laravel Valet](https://laravel.com/docs/10.x/valet) is a local development environment. In this guide you will learn how to setup a Bedrock-based WordPress site with Valet.
Valet supports Bedrock out of the box, along with traditional WordPress installations, Laravel apps, Drupal sites, and more. Since Valet is very lightweight, it is a great local development setup for folks that are working on several WordPress sites at any given time.
See the [Valet installation docs](https://laravel.com/docs/10.x/valet#installation) for information on how to install Valet. You will also want to install the Valet WP-CLI command:
```shell
$ wp package install aaemnnosttv/wp-cli-valet-command:@stable
```
## Setting up a Bedrock site
To create a new Bedrock site for Valet, navigate to Valet sites directory and use the `wp valet` command:
```shell
$ cd ~/Sites/valet
```
```shell
$ wp valet new bedrock --project=bedrock
```
You should now be able to access your new site at `https://bedrock.test`.
If you hit a 404, make sure that you have ran `valet park` from your Valet sites directory first.
### Bedrock multisite
#### Subdomain installs
* `wp valet new bedrock-multisite --project=bedrock`
* Add to `config/application.php` in Bedrock:
```php
Config::define('WP_ALLOW_MULTISITE', true);
```
* Visit `https://bedrock-multisite.test/wp/wp-admin/network.php` to install the network and select subdomain install
* Add to `.env`: `DOMAIN_CURRENT_SITE=bedrock-multisite.test`
* Update `config/application.php` again with full multisite constants:
```php
/**
* Multisite
*/
Config::define('WP_ALLOW_MULTISITE', true);
Config::define('MULTISITE', true);
Config::define('SUBDOMAIN_INSTALL', true);
Config::define('DOMAIN_CURRENT_SITE', env('DOMAIN_CURRENT_SITE'));
Config::define('PATH_CURRENT_SITE', env('PATH_CURRENT_SITE') ?: '/');
Config::define('SITE_ID_CURRENT_SITE', env('SITE_ID_CURRENT_SITE') ?: 1);
Config::define('BLOG_ID_CURRENT_SITE', env('BLOG_ID_CURRENT_SITE') ?: 1);
```
* Add the Bedrock multisite URL fixer plugin: `composer require roots/multisite-url-fixer`
* Link any subdomains to current site with Valet:
```shell
$ valet link test.bedrock-multisite
```
```shell
$ valet link site2.bedrock-multisite
```
#### Subfolder / subdirectory installs
* Copy the [Bedrock multisite subdirectory driver](https://gist.github.com/QWp6t/1e055482d722e2b02dfff1bb21698194) into `~/.valet/Drivers/`
* `wp valet new bedrock-multisite --project=bedrock`
* Add to `config/application.php` in Bedrock:
```php
Config::define('WP_ALLOW_MULTISITE', true);
```
* Visit `https://bedrock-multisite.test/wp/wp-admin/network.php` to install the network and select subfolder install
* Add to `.env`: `DOMAIN_CURRENT_SITE=bedrock-multisite.test`
* Update `config/application.php` again with full multisite constants:
```php
/**
* Multisite
*/
Config::define('WP_ALLOW_MULTISITE', true);
Config::define('MULTISITE', true);
Config::define('SUBDOMAIN_INSTALL', false);
Config::define('DOMAIN_CURRENT_SITE', env('DOMAIN_CURRENT_SITE'));
Config::define('PATH_CURRENT_SITE', env('PATH_CURRENT_SITE') ?: '/');
Config::define('SITE_ID_CURRENT_SITE', env('SITE_ID_CURRENT_SITE') ?: 1);
Config::define('BLOG_ID_CURRENT_SITE', env('BLOG_ID_CURRENT_SITE') ?: 1);
```
* Add the Bedrock multisite URL fixer plugin: `composer require roots/multisite-url-fixer` (Optional)
* * *
Thank you to [Evan Mattson](https://discourse.roots.io/u/aaemnnosttv) for contributing Bedrock's driver to Valet, and for creating the [Valet WP-CLI command](https://github.com/aaemnnosttv/wp-cli-valet-command).
Thank you to [Craig](https://discourse.roots.io/u/QWp6t) for the multisite subdirectory driver.
================================================
FILE: bedrock/compatibility.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2020-02-20 09:25
description: If plugins or themes work with regular WordPress but not Bedrock, it's usually due to hardcoded paths, not Bedrock itself. Solutions included.
title: WordPress Plugin Compatibility with Bedrock
authors:
- alwaysblank
- ben
- QWp6t
---
# WordPress Plugin Compatibility with Bedrock
Bedrock does certain things a bit differently than the default WordPress installation, but it does so by leveraging functionality that WordPress Core provides.
If a plugin or theme works with a vanilla WordPress install and not with Bedrock, the plugin or theme is likely at fault:
In most cases, it is hard-coding assumptions about directory structure or file location and ignoring the systems WordPress has in place to determine those things dynamically.
This type of issue will often arise not only on Bedrock, but also on sites that have [installed WordPress in its own directory](https://wordpress.org/support/article/giving-wordpress-its-own-directory/) or [moved the wp-content folder](https://developer.wordpress.org/apis/wp-config-php/#moving-wp-content-folder).
Common issues include:
- Assuming the content directory is `wp-content`.
- Assuming WordPress is not in a subdirectory.
- [Trying to include wp-load.php](https://ottopress.com/2010/dont-include-wp-load-please/).
If you reach a WordPress error page on a non-development environment that says `"Sorry, you are not allowed to access this page."`, then the plugin or theme could be conflicting with Bedrock's use of `DISALLOW_FILE_MODS`.
If you run into an issue with a specific theme or plugin, please contact their authors first and link them to this page.
================================================
FILE: bedrock/composer.md
================================================
---
date_modified: 2023-08-16 12:45
date_published: 2015-09-06 07:42
description: Bedrock treats WordPress core, plugins, and themes as Composer dependencies. Use WP Packages to require plugins and automate updates efficiently.
title: WordPress Dependencies with Composer
authors:
- ben
- Log1x
- swalkinshaw
- TangRufus
- EHLOVader
---
# WordPress Dependencies with Composer
Bedrock uses [Composer](https://getcomposer.org/) to manage dependencies. Any 3rd party library is considered a dependency, including WordPress itself and any plugins.
## Adding WordPress plugins with Composer
[WP Packages](https://wp-packages.org/) is already registered in the `composer.json` file so any plugins from the [WordPress Plugin Directory](https://wordpress.org/plugins/) can easily be required.
To add a plugin, add it under the `require` directive or use `composer require /` from the command line. If the plugin is from WordPress.org, then the namespace is always `wp-plugin`:
```shell
$ composer require wp-plugin/akismet
```
`plugins` and `mu-plugins` are ignored in Git by default since Composer manages them. If you want to add something to those folders that *isn't* managed by Composer, you need to update `.gitignore` to allow them to be added to your repository:
`!web/app/plugins/plugin-name`
### Force a plugin to be a mu-plugin
To force a regular `wordpress-plugin` to be treated as a `wordpress-muplugin`, you can update the `installer-paths` config to tell Bedrock to install it in the `mu-plugins` directory.
In the following example, Akismet will be installed in the `mu-plugins` directory:
```yaml
...
"extra": {
"installer-paths": {
"web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin", "wp-plugin/akismet"],
"web/app/plugins/{$name}/": ["type:wordpress-plugin"],
"web/app/themes/{$name}/": ["type:wordpress-theme"]
},
"wordpress-install-dir": "web/wp"
},
...
```
#### Configuring multiple mu-plugins
To configure more than one regular plugin to be installed to `mu-plugins`, add additional strings to the same array value for the `web/app/mu-plugins/{$name}/` JSON key, for example:
```yaml
...
"web/app/mu-plugins/{$name}/": [
"type:wordpress-muplugin",
"wp-plugin/akismet",
"wp-plugin/turn-comments-off"
],
...
```
## Updating WordPress and WordPress plugin versions with Composer
Updating your WordPress version, or the version of any plugin, is best achieved by re-requiring the dependencies to install the latest versions or specific versions:
```shell
$ composer require roots/wordpress -W
```
```shell
$ composer require wp-plugin/akismet
```
```shell
$ composer require roots/wordpress:6.8.3 -W
```
### Automating WordPress updates
Tools like [Dependabot](https://dependabot.com/) and [Renovate](https://www.mend.io/free-developer-tools/renovate/) can be used to automate updates of your Composer dependencies in Bedrock, including WordPress itself.
The Bedrock repo [uses Renovate to bump WordPress versions](https://github.com/roots/bedrock/blob/e14658bbae2c64df9605168a9c7932e5e10a9dd8/.github/renovate.json) when new versions become available.
## Adding WordPress themes with Composer
Themes can also be managed by Composer but should only be done so under two conditions:
1. You're using a parent theme that won't be modified at all
2. You want to separate out your main theme and use that as a standalone package
Under most circumstances, we recommend keeping your main theme as part of your repository.
Just like plugins, WP Packages maintains a Composer mirror of the WP theme directory. To require a theme, just use the `wp-theme` namespace:
```shell
$ composer require wp-theme/twentytwentythree
```
## Recommended resources
[WordPress with Composer resources](https://roots.io/composer-wordpress-resources/) for more extensive documentation and background information:
- [📝 Composer in WordPress from Rarst](https://composer.rarst.net/)
- [📝 `roots/wordpress` Composer Package](https://roots.io/announcing-the-roots-wordpress-composer-package/)
- [📝 Using Composer with WordPress](https://roots.io/using-composer-with-wordpress/)
- [📝 WordPress Plugins with Composer](https://roots.io/wordpress-plugins-with-composer/)
- [🎥 Using Composer With WordPress screencast](https://www.youtube.com/watch?v=2cFRQA1_GY0) (2013)
- [📝 Private or Commercial WordPress Plugins as Composer Dependencies](https://roots.io/bedrock/docs/private-or-commercial-wordpress-plugins-as-composer-dependencies/)
================================================
FILE: bedrock/configuration.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-09-06 07:42
description: Bedrock replaces `wp-config.php` with modern configuration files. Set global config in `application.php` and override per environment as needed.
title: Configuring Bedrock for WordPress
authors:
- ben
- Log1x
- mZoo
- swalkinshaw
---
# Configuring Bedrock for WordPress
The file to modify for configuration options is `config/application.php`. This is the file that contains what `wp-config.php` usually would.
The root `web/wp-config.php` is required by WordPress and is only used to load the other main configs. Nothing else should be added to it.
Bedrock's base configuration options are production-standard safe settings and used in all environments except where specifically overridden. To override configuration settings based on environments:
- Use an existing environment config in `config/environments` or create a new one. Bedrock will `require` any file in the `config/environments` directory with a filename matching the `WP_ENV` environment variable. This environment variable can be set in a few ways:
- in the `.env` file as described in our [installation docs](installation.md)
- via [Trellis config](/trellis/docs/wordpress-sites/) if you're using Trellis
- or as a last resort, hardcoding it in `config/application.php`
- Bedrock comes with `development.php` and `staging.php` configs included. If you create an additional environment, configure it with a matching PHP file in `config/environments`.
- The [`development.php` file](https://github.com/roots/bedrock/blob/master/config/environments/development.php) sets `WP_DEBUG_DISPLAY` to `true`, so WordPress will display PHP errors in the browser when your `WP_ENV` is `development`.
Bedrock 1.9.0 (2018-09-17) introduced [`roots/wp-config`](https://github.com/roots/wp-config/blob/master/docs/why.md) ([discussion](https://github.com/roots/bedrock/pull/380)).
`Config::define` is a static method that overrides the application options (WP) with environment specific options where they are defined, defaulting to the application options set in `config/application.php`.
================================================
FILE: bedrock/converting-wordpress-sites-to-bedrock.md
================================================
---
date_modified: 2025-10-24 12:00
date_published: 2025-10-24 12:00
description: Convert traditional WordPress sites to Bedrock using Lithify. Automatically updates database references and file paths for Bedrock's directory structure.
title: Converting WordPress Sites to Bedrock
authors:
- ben
- MWDelaney
---
# Converting WordPress Sites to Bedrock
[Lithify](https://github.com/MWDelaney/lithify) is a WordPress plugin that adds a WP-CLI command to convert traditional WordPress sites into Bedrock-style installations. Created by [MWDelaney](https://github.com/MWDelaney), Lithify automates the database changes needed to make your existing WordPress site work with Bedrock's improved folder structure.
Converting an existing WordPress site to Bedrock typically involves manual database updates and file path changes. Lithify handles this conversion process automatically, updating your database to work seamlessly with Bedrock's modern architecture.
## Prerequisites
Before starting the conversion, you'll need:
- A fresh Bedrock installation
- A database backup of your existing WordPress site
- Your existing site's plugins, themes, and uploads directories
## Create a new Bedrock site
Create a new Bedrock installation:
```bash
$ composer create-project roots/bedrock example.com
```
```bash
$ cd example.com
```
## Update WordPress version
Update Bedrock's WordPress version to match your current installation. For example, if your site runs WordPress 6.8.2, update `composer.json`:
```json
"roots/wordpress": "6.8.2"
```
## Copy your content files
Copy your WordPress `plugins`, `themes`, `mu-plugins`, and `uploads` directories into the Bedrock `web/app` directory.
## Add Lithify as a dependency
Install Lithify using Composer:
```bash
$ composer require mwdelaney/lithify
```
## Import your database
Navigate to your Bedrock directory and import your WordPress database:
```bash
$ wp db import example.sql
```
## Run the conversion
Activate Lithify and run the conversion command:
```bash
$ wp plugin activate lithify
```
```bash
$ wp lithify
```
### What Lithify does
When you run `wp lithify`, the plugin:
- Updates file path references from `wp-content` to `app`
- Adjusts plugin and theme paths to match Bedrock's structure
- Ensures upload paths work correctly with the new organization
- Verifies that WordPress core references point to the correct locations
The conversion is non-destructive—your existing content and configuration remain intact while gaining all the benefits of Bedrock's modern development workflow.
================================================
FILE: bedrock/deployment.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-10-15 16:17
description: Bedrock deployments require running `composer install` to fetch dependencies. Learn deployment workflows for various hosting platforms and CI/CD tools.
title: Deploying WordPress with Bedrock
authors:
- alwaysblank
- ben
- knowler
- Log1x
- noplanman
- swalkinshaw
---
# Deploying WordPress with Bedrock
Running `composer install` from the Bedrock folder must be part of your deployment process.
## Supported deployment tools
These tools include supporting deploying Bedrock out of the box:
- [Trellis](https://roots.io/trellis/) – Recommended if self-hosting WordPress or [hosting with Kinsta](https://kinsta.com/?kaid=OFDHAJIXUDIV).
Other methods need to account for setting the `WP_ENV` [environment variable](environment-variables.md) to `production` when your site is in a production environment.
::: warning Note
Bedrock's [Disallow Indexing mu-plugin](https://github.com/roots/bedrock-disallow-indexing) will prevent indexing of a site when `WP_ENV` is not set to `production`.
:::
================================================
FILE: bedrock/disable-plugins-based-on-environment.md
================================================
---
date_modified: 2023-04-04 11:30
date_published: 2018-05-15 12:00
description: Use Bedrock Plugin Disabler to prevent specific plugins from loading in certain environments. Disable debug tools in production or heavy plugins locally.
title: Disable Plugins Based on Environment
authors:
- ben
- luke
- owi
---
# Disable Plugins Based on Environment
Bedrock supports defining an environment with the `WP_ENV` environment variable. A typical setup for a project could contain several different environments:
* `development` for local development
* `staging` for a staging environment
* `production` for the live/production environment
In some cases, you may want to enforce certain plugins to be deactivated on one or more of your environments.
The [Bedrock Plugin Disabler](https://github.com/lukasbesch/bedrock-plugin-disabler) mu-plugin package by [@luke](https://discourse.roots.io/u/luke) can be used to configure a list of disabled plugins in your Bedrock environment configs located in `config/environments/`.
Install the mu-plugin with Composer:
```shell
$ composer require lukasbesch/bedrock-plugin-disabler
```
This package requires defining a `DISABLED_PLUGINS` constant with an array of plugin filenames to be disabled.
## Disabling plugins on local development
The most common use-case is disabling caching plugins on local development. We'll cover disabling WP Rocket and WP Super Cache in the following example.
Open `config/environments/development.php` and add the `DISABLED_PLUGINS` constant:
```php
Config::define('DISABLED_PLUGINS', [
'wp-rocket/wp-rocket.php',
'wp-super-cache/wp-cache.php',
]);
```
================================================
FILE: bedrock/environment-variables.md
================================================
---
date_modified: 2023-02-16 20:55
date_published: 2015-09-06 07:42
description: Bedrock uses `.env` files for environment-specific settings like database credentials. Keep sensitive data out of Git with environment variables.
title: WordPress Environment Variables in Bedrock
authors:
- alwaysblank
- ben
- Log1x
- swalkinshaw
- tristanbes
---
# WordPress Environment Variables in Bedrock
Bedrock tries to separate config from code as much as possible and environment variables are used to achieve this. The benefit is there's a single place (`.env`) to keep settings like database or other 3rd party credentials that aren't committed to your repository.
[PHP dotenv](https://github.com/vlucas/phpdotenv) is used to load the `.env` file. All variables are then available in your app by the built-in `getenv`, `$_SERVER`, or `$_ENV` methods.
However, we use the [env](https://github.com/oscarotero/env) library and its `env` function which handles simple type coercion (such as converting the string `'True'` to the boolean `true`). We recommend you use `env` as well for reading environment variables.
Currently, the following env vars are required:
- `WP_HOME`
- `WP_SITEURL`
The following vars are required if `DATABASE_URL` is not set:
- `DB_USER`
- `DB_NAME`
- `DB_PASSWORD`
::: tip Note
There is also the `DATABASE_URL` which is optional.
:::
## WP_ENV
Although it isn't required (if not defined elsewhere, Bedrock will default to `production`), `WP_ENV` is used by several pieces of the Roots stack, as well as software outside of it, to modify behavior based on environment. There are three values you can set for `WP_ENV` that Bedrock will understand:
- `production`
- `staging`
- `development`
Make sure that these are set correctly in your different environments.
### WP_ENVIRONMENT_TYPE
Bedrock also infers [`WP_ENVIRONMENT_TYPE`](https://developer.wordpress.org/reference/functions/wp_get_environment_type/) based on `WP_ENV`. `WP_ENVIRONMENT_TYPE` was introduced in WordPress 5.5.0.
================================================
FILE: bedrock/folder-structure.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-09-06 07:42
description: Bedrock organizes WordPress differently. `wp-content` renamed to `app/`, WordPress core isolated in `wp/` directory for improved project structure.
title: Bedrock WordPress Folder Structure
authors:
- ben
- Log1x
- mZoo
- swalkinshaw
---
# Bedrock WordPress Folder Structure
```plaintext
├── composer.json # → Manage versions of WordPress, plugins & dependencies
├── config # → WordPress configuration files
│ ├── application.php # → Primary WP config file (wp-config.php equivalent)
│ └── environments # → Environment specific configs
│ ├── development.php # → Development config
│ └── staging.php # → Staging config
├── vendor # → Composer packages (never edit)
└── web # → Web root (document root on your webserver)
├── app # → wp-content equivalent
│ ├── mu-plugins # → Must use plugins
│ ├── plugins # → Plugins
│ ├── themes # → Themes
│ └── uploads # → Uploads
├── wp-config.php # → Required by WP (never edit)
├── index.php # → WordPress view bootstrapper
└── wp # → WordPress core (never edit)
```
The organization of Bedrock is similar to putting WordPress in its own subdirectory but with some improvements:
- In order not to expose sensitive files in the web root, Bedrock moves what's required into a `web/` directory including the `wp/` source, and the `wp-content` source.
- `wp-content` has been named `app` to better reflect its contents. It contains application code and not just "static content". It also matches up with other frameworks such as Symfony and Rails.
- `wp-config.php` remains in the `web/` because it's required by WP, but it only acts as a loader. The actual configuration files have been moved to `config/` for better separation.
- `vendor/` is where the Composer managed dependencies are installed to.
- `wp/` is where WordPress core lives. It's also managed by Composer but can't be put under `vendor` due to WP limitations.
================================================
FILE: bedrock/installation.md
================================================
---
date_modified: 2026-03-08 16:09
date_published: 2015-10-15 12:29
description: Install Bedrock with PHP 8.3+ and Composer. Configure environment variables in `.env` file and set document root to `web/` directory to access WordPress.
title: Installing the Bedrock WordPress Boilerplate
authors:
- ben
- Log1x
- swalkinshaw
---
# Installing the Bedrock WordPress Boilerplate
## What is Bedrock?
Bedrock is a [WordPress boilerplate](https://roots.io/bedrock/).
### Why use Bedrock?
- Better folder structure
- Dependency management with [Composer](https://getcomposer.org)
- Easy WordPress configuration with environment specific files
- Environment variables with [Dotenv](https://github.com/vlucas/phpdotenv)
- Autoloader for mu-plugins (use regular plugins as mu-plugins)
## Requirements
- PHP >= 8.3
- [Composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos)
## Installing Bedrock with Composer
Create a new Bedrock project:
```shell
$ composer create-project roots/bedrock
```
## Getting Started
- Create a `.env` file with the following environment variables (see `.env.example` as an example):
- Database variables
- `DB_NAME` - Database name
- `DB_USER` - Database user
- `DB_PASSWORD` - Database password
- `DB_HOST` - Database host
- Optionally, you can define `DATABASE_URL` for using a DSN instead of using the variables above (e.g. `mysql://user:password@127.0.0.1:3306/db_name`)
- `WP_ENV` - Set to environment (`development`, `staging`, `production`)
- `WP_HOME` - Full URL to WordPress home (https://example.com)
- `WP_SITEURL` - Full URL to WordPress including subdirectory (https://example.com/wp)
- `AUTH_KEY`, `SECURE_AUTH_KEY`, `LOGGED_IN_KEY`, `NONCE_KEY`, `AUTH_SALT`, `SECURE_AUTH_SALT`, `LOGGED_IN_SALT`, `NONCE_SALT`
- Generate with [wp-cli-dotenv-command](https://github.com/aaemnnosttv/wp-cli-dotenv-command)
- Generate with [our WordPress salts generator](https://roots.io/salts.html)
- Add theme(s) in `web/app/themes/` as you would for a normal WordPress site
- Run the test suite with `composer test` (see [Testing Bedrock with Pest](/bedrock/docs/testing/))
- Set the document root on your webserver to Bedrock's `web` folder: `/path/to/site/web/`
- Access WordPress admin at `https://example.com/wp/wp-admin/`
### Multisite
Bedrock is multisite network compatible, but needs the [roots/multisite-url-fixer](https://github.com/roots/multisite-url-fixer) mu-plugin on subdomain installs to make sure admin URLs function properly. This plugin is not _needed_ on subdirectory installs but will work well with them. From your Bedrock directory:
```shell
$ composer require roots/multisite-url-fixer
```
================================================
FILE: bedrock/local-development.md
================================================
---
date_modified: 2026-03-08 16:07
date_published: 2018-12-28 13:54
description: Bedrock supports various local development tools including Trellis, Laravel Valet, Local, DDEV, Lando, and DevKinsta for flexible WordPress development.
title: Local WordPress Development with Bedrock
authors:
- ben
- Log1x
- swalkinshaw
---
# Local WordPress Development with Bedrock
Bedrock can be used with most local development setups. [Trellis](https://roots.io/trellis/) is our WordPress LEMP stack that supports Bedrock out of the box. We also have guides for using Bedrock with some popular setups:
- [Bedrock with DDEV](/bedrock/docs/bedrock-with-ddev/)
- [Bedrock with DevKinsta](/bedrock/docs/bedrock-with-devkinsta/)
- [Bedrock with Lando](/bedrock/docs/bedrock-with-lando/)
- [Bedrock with Local](/bedrock/docs/bedrock-with-local/)
- [Bedrock with Valet](/bedrock/docs/bedrock-with-valet/)
For test setup and commands, see [Testing Bedrock with Pest](/bedrock/docs/testing/).
Additionally, [WP-CLI's server command](https://developer.wordpress.org/cli/commands/server/) can be used with Bedrock (the `docroot` for the server is set in Bedrock's [`wp-cli.yml`](https://github.com/roots/bedrock/blob/master/wp-cli.yml))
MAMP, XAMPP, and others setups work with Bedrock once the [virtual host is configured](configuration.md).
================================================
FILE: bedrock/mu-plugin-autoloader.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-09-06 07:42
description: Bedrock's autoloader lets you install regular plugins as must-use plugins via Composer, ensuring critical plugins always load without user control.
title: WordPress Must-use Plugin Autoloader
authors:
- ben
- Log1x
- swalkinshaw
---
# WordPress Must-use Plugin Autoloader
Bedrock includes an autoloader that enables standard plugins to be required just like must-use plugins.
The autoloaded plugins are included after all mu-plugins and standard plugins have been loaded. An asterisk (*) next to the name of the plugin designates the plugins that have been autoloaded. To remove this functionality, just delete `web/app/mu-plugins/bedrock-autoloader.php`.
This enables the use of mu-plugins through Composer if their package type is `wordpress-muplugin`. You can also override a plugin's type like the following example:
```json
"installer-paths": {
"web/app/mu-plugins/{$name}/": ["type:wordpress-muplugin", "roots/wp-stage-switcher"],
"web/app/plugins/{$name}/": ["type:wordpress-plugin"],
"web/app/themes/{$name}/": ["type:wordpress-theme"]
},
```
[wp-stage-switcher](https://github.com/roots/wp-stage-switcher) is a package with its type set to `wordpress-plugin`. Since it implements `composer/installers` we can override its type.
================================================
FILE: bedrock/patching-wordpress-plugins-with-composer.md
================================================
---
date_modified: 2025-10-13 12:00
date_published: 2025-10-13 12:00
description: Use Composer patches to modify WordPress plugins without forking. Resolve dependency conflicts and apply fixes to third-party plugins in Bedrock projects.
title: Patching WordPress Plugins with Composer
authors:
- ben
---
# Patching WordPress Plugins with Composer
When managing WordPress plugins through Composer in Bedrock, you may encounter situations where you need to modify third-party plugin code. Common scenarios include resolving dependency conflicts, fixing bugs before official updates are released, or adapting plugins for your specific environment.
Rather than forking plugins or manually editing vendor code, Composer patches provide a maintainable solution that persists across updates.
## Installing the patches plugin
Add the [cweagans/composer-patches](https://github.com/cweagans/composer-patches) package to your project:
```shell
$ composer require cweagans/composer-patches
```
Enable the plugin in your `composer.json`:
```json
"config": {
"allow-plugins": {
"cweagans/composer-patches": true
}
}
```
## Creating a patch file
Patches are standard unified diff files. You can create them using Git or the `diff` command.
### Using Git to create a patch
The easiest method is to make changes to the plugin and generate a diff. First initialize a temporary Git repo for the plugin:
```shell
$ cd web/app/plugins/example-plugin
```
```shell
$ git init
```
```shell
$ git add . && git commit -m "Base plugin"
```
Make your changes to the plugin files, then generate the patch and clean up:
```shell
$ git diff > ../../../../patches/example-plugin-fix.patch
```
```shell
$ rm -rf .git
```
### Example: resolving PSR library conflicts
A common issue in WordPress is multiple plugins bundling the same PSR libraries with different versions, causing conflicts. Here's a real-world example of commenting out conflicting `psr/log` class registrations:
```diff
diff --git a/vendor/composer/jetpack_autoload_classmap.php b/vendor/composer/jetpack_autoload_classmap.php
index 1855b18..d52edf3 100644
--- a/vendor/composer/jetpack_autoload_classmap.php
+++ b/vendor/composer/jetpack_autoload_classmap.php
@@ -194,11 +194,11 @@ return array(
'version' => '3.1.3',
'path' => $vendorDir . '/automattic/jetpack-autoloader/src/class-plugins-handler.php'
),
- 'Psr\\Log\\LoggerInterface' => array(
- 'version' => '1.1.4.0',
- 'path' => $vendorDir . '/psr/log/Psr/Log/LoggerInterface.php'
- ),
+ // 'Psr\\Log\\LoggerInterface' => array(
+ // 'version' => '1.1.4.0',
+ // 'path' => $vendorDir . '/psr/log/Psr/Log/LoggerInterface.php'
+ // ),
```
Save this patch file to a `patches/` directory in your project root.
## Configuring patches in `composer.json`
Add your patches to the `extra.patches` section of `composer.json`:
```json
"extra": {
"patches": {
"vendor/package-name": [
{
"description": "Brief description of patch",
"url": "patches/example-plugin-fix.patch"
}
]
}
}
```
## Applying patches
Once configured, patches are automatically applied when you install or update dependencies:
```shell
$ composer install
```
You'll see output confirming patches are being applied:
```plaintext
- Applying patches for vendor/package-name
patches/example-plugin-fix.patch (Brief description of patch)
```
## When to use patches
Composer patches are ideal for:
* **Dependency conflicts** - Removing bundled libraries that conflict with your main dependencies
* **Bug fixes** - Applying fixes before official plugin updates are available
* **Environment adjustments** - Modifying plugins for specific hosting requirements
* **Temporary workarounds** - Addressing issues while waiting for upstream fixes
::: tip
Document your patches clearly. Include the reason for each patch in the description and maintain separate patch files for different issues to make future maintenance easier.
:::
## Maintaining patches across updates
When updating plugins, patches are reapplied automatically. If a patch fails to apply (usually because the plugin code changed), Composer will show an error. You'll need to:
1. Review the plugin changes
2. Update or remove the patch as needed
3. Test that your fix is still necessary
================================================
FILE: bedrock/patching-wordpress-with-composer.md
================================================
---
date_modified: 2026-03-10 12:00
date_published: 2026-03-10 12:00
description: Apply patches to WordPress core in Bedrock using Composer. Fix bugs or apply upstream changes before official releases without modifying core files directly.
title: Patching WordPress with Composer
authors:
- ben
---
# Patching WordPress with Composer
Sometimes you need to patch WordPress core — to fix a bug before an official release, suppress noisy deprecation notices, or backport a change from an open pull request. Composer patches let you apply these changes in a maintainable way that persists across installs and deploys.
## Installing the patches plugin
Add the [cweagans/composer-patches](https://github.com/cweagans/composer-patches) package to your project:
```shell
$ composer require cweagans/composer-patches
```
Enable the plugin in your `composer.json`:
```json
"config": {
"allow-plugins": {
"cweagans/composer-patches": true
}
}
```
## Which package to patch
Bedrock's `roots/wordpress` is a metapackage — it doesn't contain any files. The actual WordPress core files are in the `roots/wordpress-no-content` package, which is installed to your `wordpress-install-dir` (typically `web/wp`).
Patches must target `roots/wordpress-no-content`, and patch file paths should be relative to the package root (e.g., `wp-includes/load.php`, not `web/wp/wp-includes/load.php`).
## Creating a patch file
Create a `patches/` directory in your project root. Patches are standard unified diff files with paths relative to the WordPress root.
### Example: suppressing deprecated notices
```diff
--- a/wp-includes/load.php
+++ b/wp-includes/load.php
@@ -607,7 +607,7 @@ function wp_debug_mode() {
}
if ( WP_DEBUG ) {
- error_reporting( E_ALL );
+ error_reporting( E_ALL & ~E_DEPRECATED );
if ( WP_DEBUG_DISPLAY ) {
ini_set( 'display_errors', 1 );
```
Save this as `patches/wordpress.patch`.
## Configuring patches in `composer.json`
Add your patches to the `extra.patches` section of `composer.json`:
```json
"extra": {
"patches": {
"roots/wordpress-no-content": [
{
"description": "Suppress E_DEPRECATED notices",
"url": "patches/wordpress.patch"
}
]
}
}
```
## Applying patches
Patches are automatically applied when you install or update dependencies:
```shell
$ composer install
```
You'll see output confirming patches are being applied:
```plaintext
- Patching roots/wordpress-no-content
- Applying patch patches/wordpress.patch (Suppress E_DEPRECATED notices)
```
To force patches to reapply, reinstall the package:
```shell
$ composer reinstall roots/wordpress-no-content
```
## Maintaining patches across updates
When WordPress is updated, patches are reapplied automatically. If a patch fails to apply (usually because the patched code changed in the new version), Composer will show an error. You'll need to:
1. Review the WordPress changes
2. Update or remove the patch as needed
3. Test that your fix is still necessary — the issue may have been resolved upstream
::: warning
WordPress core patches are version-specific. After updating WordPress, always verify that your patches still apply cleanly and are still needed.
:::
================================================
FILE: bedrock/private-or-commercial-wordpress-plugins-as-composer-dependencies.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2018-08-02 14:04
description: Add paid and private plugins to Bedrock through Composer using private Git repositories, custom Composer repos, or services like WP Packages.
title: Private or Commercial WordPress Plugins as Composer Dependencies
authors:
- MWDelaney
- strarsis
---
# Private or Commercial WordPress Plugins as Composer Dependencies
Bedrock (and by extension Trellis) uses Composer to manage its dependencies, which includes WordPress themes and plugins. This is great for version control as many WordPress plugins are easily available via [WP Packages](https://wp-packages.org/), but what happens when you need to add a private, commercial, or paid plugin to your site? This guide will explain a simple way to add private plugins to your site via Composer.
There are many ways to add private or paid plugins to your Bedrock-based project. Popular methods include:
* Private Git repositories
* [SatisPress](https://github.com/cedaro/satispress)
* [Private Packagist](https://packagist.com/)
* [Toran Proxy](https://toranproxy.com/)
For the purposes of this document we will focus only on the first option: **private Git repositories**. We welcome contributed guides covering these methods or others.
**Note:** We recommend hosting private and commercial plugins in private Git repositories. GitHub [offers private repositories](https://github.com/pricing) for free and BitBucket includes them in its [limited free plan](https://bitbucket.org/product/pricing). The following guide assumes you’re using a GitHub private repository.
## Create a private GitHub repository for your plugin
[Create the repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/quickstart-for-repositories) as normal and clone the empty repository to your computer.
```shell
$ git clone git@github.com:YourGitHubUsername/example-plugin.git
```
## Create `composer.json`
In your empty repository, create a file named `composer.json` with the following content (edited to include your correct user, repository, and plugin information):
```json
{
"name": "YourGithubUsername/example-plugin",
"description": "",
"keywords": ["wordpress", "plugin"],
"homepage": "https://github.com/YourGitHubUsername/example-plugin",
"authors": [
{
"name": "Original Plugin Author's Name",
"homepage": "https://originalpluginurl.com"
}
],
"type": "wordpress-plugin",
"require": {
"php": ">=8.0"
}
}
```
::: tip
Composer can create a skeleton `composer.json` for you: Just run `composer init` in your empty directory.
:::
## Copy plugin files into your repository
Copy all the plugin’s files into your new repository.
## Commit your plugin to Git and push your changes to GitHub
Run each of the following commands from your repository directory:
Add all of your plugin’s files to Git.
```shell
$ git add .
```
Commit your changes
::: tip
Include the plugin’s version number in your commit message so that you can easily reference it later!
:::
```shell
$ git commit .
```
## Tag the release
Composer will need a way to know the version of a plugin. Fortunately, it understands [git tags](https://getcomposer.org/doc/articles/versions.md#tags) and can interpret them correctly if they use [semantic versioning](https://semver.org/), so we’ll be using those.
Let’s assume you’re pushing SearchWP version 2.9.14. That means we’ll be creating the 2.9.14 tag. Remember, tags are tied to commits, so be sure to commit all your changes _before_ creating the tag.
```shell
$ git tag 2.9.14
```
Push your changes, and your tags to GitHub:
```shell
$ git push --tags
```
::: tip
Tags pushed to GitHub will automatically be turned into “Releases,” a feature of GitHub. You can also [create releases](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository) manually on the GitHub website.
:::
## Edit your Bedrock `composer.json` file, add your repository and plugin
In your Bedrock site’s `composer.json`
* Add a new your GitHub repository to the `repositories` section referencing your GitHub repository:
```json
"repositories": [
...
{
"type": "vcs",
"url": "git@github.com:YourGitHubUsername/example-plugin.git"
}
...
],
```
* Add your plugin to the `require` section using the version number you named your `release` after:
```json
"require": {
...
"YourGitHubUsername/example-plugin": "2.9.14",
...
},
```
## Update your dependencies
Run `composer update` in your Bedrock directory to get your new plugin.
[**Join the discussion on Roots Discourse**](https://discourse.roots.io/t/private-or-commercial-wordpress-plugins-as-composer-dependencies/13247)
================================================
FILE: bedrock/server-configuration.md
================================================
---
date_modified: 2026-03-08 10:00
date_published: 2018-12-21 18:24
description: Configure Nginx or Apache for Bedrock by setting document root to `web/` directory. Includes complete server configuration examples and rewrite rules.
title: Server Configuration for Bedrock
authors:
- ben
- Lachlan_Arthur
- Log1x
- swalkinshaw
---
# Server Configuration for Bedrock
Bedrock can run on any webserver that supports Composer and PHP >= 8.3. The document root for your site must be pointed to Bedrock's `web` folder.
## Nginx configuration for Bedrock
If you aren't using a Nginx-based setup that already supports Bedrock, such as Valet or [Trellis](https://roots.io/trellis/), you'll need to configure Nginx with the following rules:
```nginx
server {
listen 80;
server_name example.com;
root /srv/www/example.com/web;
index index.php index.htm index.html;
# Prevent PHP scripts from being executed inside the uploads folder.
location ~* /app/uploads/.*.php$ {
deny all;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
}
```
### Nginx multisite config
Multisite installations on Nginx need additional rewrites depending on the type of multisite install.
#### Subdomain multisite rewrites
```nginx
rewrite ^/(wp-.*.php)$ /wp/$1 last;
rewrite ^/(wp-(content|admin|includes).*) /wp/$1 last;
```
#### Subfolder multisite rewrites
```nginx
if (!-e $request_filename) {
rewrite /wp-admin$ $scheme://$host$uri/ permanent;
rewrite ^(/[^/]+)?(/wp-.*) /wp$2 last;
rewrite ^(/[^/]+)?(/.*.php) /wp$2 last;
}
```
## Apache configuration for Bedrock
Make sure the `DocumentRoot` is set to the `web` folder:
```apache
DocumentRoot /var/www/html/bedrock/web
DirectoryIndex index.php index.html index.htm
Options -Indexes
# .htaccess isn't required if you include this
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
```
You can also add the suggested `.htaccess` file from WordPress at `web/.htaccess`:
```apache
# BEGIN WordPress
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
# END WordPress
```
## Managed WordPress hosts and Bedrock
If you're using a [supported WordPress host](deployment.md#supported-wordpress-hosts) such as Kinsta, then contact support and ask them to set your document root to the `web` folder.
Sometimes you can't change the document root on hosted web server. In this case, you can create an `.htaccess` file at the root of your project with the following content:
```apache
RewriteEngine on
RewriteCond %{REQUEST_URI} !web/
RewriteRule ^(.*)$ /web/$1 [L]
```
================================================
FILE: bedrock/testing.md
================================================
---
date_modified: 2026-03-08 16:05
date_published: 2026-03-08 16:05
description: Learn how to run tests in Bedrock using Pest (powered by PHPUnit), create feature tests, and run the suite locally and in CI.
title: Testing Bedrock with Pest
authors:
- ben
---
# Testing Bedrock with Pest
Bedrock includes a minimal testing setup based on [Pest](https://pestphp.com/) (powered by PHPUnit).
## Running tests
Run the test suite from your Bedrock root:
```shell
$ composer test
```
## Default test structure
Bedrock ships with these testing files:
- `phpunit.xml.dist` - PHPUnit configuration used by Pest
- `tests/Pest.php` - Pest bootstrap and shared test configuration
- `tests/Feature/ExampleTest.php` - Example test
## Writing tests
Add tests anywhere under `tests/`:
```php
not->toBeEmpty();
});
```
Then run:
```shell
$ composer test
```
## Scope of the default setup
The default setup is intentionally minimal and framework-agnostic:
- It gives you a ready-to-run PHP testing baseline
- It does **not** include WordPress core integration test bootstrap or database test provisioning
If you need deeper WordPress integration testing, you can extend this baseline with your preferred tooling.
================================================
FILE: bedrock/wp-cron.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-09-06 07:42
description: Disable WordPress's unreliable internal cron with `DISABLE_WP_CRON` in Bedrock and set up proper system cron jobs for scheduled tasks.
title: Managing WP Cron in Bedrock
authors:
- ben
- Log1x
- swalkinshaw
---
# Managing WP Cron in Bedrock
Bedrock allows you to disable the internal WP Cron via the `DISABLE_WP_CRON` environment variable. If you enable this setting and disable WP Cron, you'll need to manually set a cron job like the following in your crontab file:
```plaintext
*/5 * * * * curl https://example.com/wp/wp-cron.php
```
================================================
FILE: netlify.toml
================================================
[[redirects]]
from = "/"
to = "https://roots.io/"
status = 301
force = true
[[redirects]]
from = "/docs/*"
to = "/:splat"
status = 301
force = true
[[redirects]]
from = "/acorn/"
to = "https://roots.io/acorn/docs/installation/"
status = 301
force = true
[[redirects]]
from = "/acorn/2.x/*"
to = "https://roots.io/acorn/docs/:splat"
status = 301
force = true
[[redirects]]
from = "/bedrock/"
to = "https://roots.io/bedrock/docs/installation/"
status = 301
force = true
[[redirects]]
from = "/bedrock/master/*"
to = "https://roots.io/bedrock/docs/:splat"
status = 301
force = true
[[redirects]]
from = "/examples/*"
to = "https://roots.io/"
status = 301
force = true
[[redirects]]
from = "/sage/"
to = "https://roots.io/sage/docs/installation/"
status = 301
force = true
[[redirects]]
from = "/sage/10.x/*"
to = "https://roots.io/sage/docs/:splat"
status = 301
force = true
[[redirects]]
from = "/trellis/"
to = "https://roots.io/trellis/docs/installation/"
status = 301
force = true
[[redirects]]
from = "/trellis/master/*"
to = "https://roots.io/trellis/docs/:splat"
status = 301
force = true
[[redirects]]
from = "/getting-started/"
to = "https://roots.io/"
status = 301
force = true
[[redirects]]
from = "/getting-started/macos/"
to = "https://roots.io/"
status = 301
force = true
[[redirects]]
from = "/getting-started/ubuntu-linux/"
to = "https://roots.io/"
status = 301
force = true
[[redirects]]
from = "/getting-started/windows/"
to = "https://roots.io/"
status = 301
force = true
[[redirects]]
from = "/sage/10.x/installing-packages/"
to = "https://roots.io/acorn/docs/available-packages/"
status = 301
force = true
[[redirects]]
from = "/acorn/2.x/installing-packages/"
to = "https://roots.io/acorn/docs/available-packages/"
status = 301
force = true
[[redirects]]
from = "/sage/10.x/available-packages/"
to = "https://roots.io/acorn/docs/available-packages/"
status = 301
force = true
[[redirects]]
from = "/sage/10.x/package-development/"
to = "https://roots.io/acorn/docs/package-development/"
status = 301
force = true
[[redirects]]
from = "/trellis/master/languages/"
to = "https://roots.io/trellis/docs/guides/install-wordpress-language-files/"
status = 301
force = true
[[redirects]]
from = "/trellis/master/deploys/"
to = "https://roots.io/trellis/docs/deployments/"
status = 301
force = true
================================================
FILE: sage/adding-linting.md
================================================
---
date_modified: 2023-03-12 19:25
date_published: 2023-01-23 19:40
description: Set up ESLint, Prettier, and Stylelint in Sage to enforce code quality standards, consistent formatting, and best practices for theme development.
title: Adding ESLint, Prettier, and Stylelint
authors:
- ben
- chrillep
---
# Adding ESLint, Prettier, and Stylelint
::: tip We recommend enabling linting
Sage 10 no longer includes linting styles or scripts out of the box. We highly recommend adding and configuring [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), and [Stylelint](https://stylelint.io/) based on your needs.
:::
Bud has several extensions that can be added to your theme dependencies to help with linting. To add ESLint, Prettier, and Stylelint to your theme, run:
```
yarn add @roots/bud-eslint -D
yarn add @roots/bud-prettier -D
yarn add @roots/bud-stylelint -D
yarn add @roots/eslint-config -D
```
Add `scripts` to `package.json` for better access to linting your scripts and styles:
```json
...
"scripts": {
"lint": "yarn lint:js && yarn lint:css",
"lint:js": "eslint resources/scripts",
"lint:css": "stylelint \"resources/**/*.{css,scss,vue}\"",
"test": "yarn lint",
}
...
```
Then create new files for `.eslintrc.cjs`, `.prettierrc`, and `.stylelintrc`.
`.eslintrc.cjs`:
```javascript
module.exports = {
root: true,
extends: ['@roots/eslint-config/sage'],
};
```
`.prettierrc`:
```json
{
"bracketSpacing": false,
"jsxBracketSameLine": true,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}
```
`.stylelintrc`:
```json
{
"extends": [
"@roots/sage/stylelint-config",
"@roots/bud-tailwindcss/stylelint-config"
]
}
```
================================================
FILE: sage/blade-templates.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2018-02-07 09:46
description: Sage uses Laravel's Blade for powerful templating. Learn template inheritance with `@extends`, layouts with `@yield`, and passing data to WordPress views.
title: Using Blade Templates in Sage
authors:
- alwaysblank
- ben
- Log1x
---
# Using Blade Templates in Sage
Sage uses [Laravel's Blade](https://laravel.com/docs/10.x/blade) templating engine.
::: tip
The Blade templating language is described in much more depth in the [Laravel docs](https://laravel.com/docs/10.x/blade), which we recommend you read for a full understanding of how it works. Nearly everything described there should work in Sage.
:::
The following are some of the Blade features you're likely to find yourself using regularly.
## Including
One of the primary features of Blade is the `@include` directive (which also has a few useful variants). `@include` allows you to use a Blade file in any other Blade file, and creates a new scope for each included file.
Variables define in a given view will cascade down to views that it `@includes`, but you can also pass data directly to Blade templates by passing a keyed array as the second argument to the `@include()` directive.
The key names will become the variable names that their values are assigned to.
```blade
@include('partials.example-partial', ['variableName' => 'Variable Value']
{{ $variableName }}
```
## Layouts
A layout is a special kind of template that can be extended. It's useful when you have a lot of HTML content surrounding something you want to be dynamic—for instance the header and footer of a site.
```blade
@section('header')
@include('partials.nav.primary')
@show
@yield('content')
@extends('layouts.app')
@section('header')
@parent
@include('partials.nav.page')
@endsection
@section('content')
{{ $title }}
{!! $content !!}}
@endsection
```
The extending view (`page.blade.php` in this case) can then "insert" its content into these sections to be rendered.
## Passing data to templates
The best way to handle passing data to templates is to use [Composers](composers.md), which allow you to separate data handling and manipulation from the view where that data is used.
With Composers, you can bind data to _any_ Blade template file.
You can also pass data directly to Blade templates when `@include`ing them by passing a keyed array as the second argument to the `@include()` directive.
The key names will become the variable names that their values are assigned to.
```blade
@include('partials.example-partial', ['variableName' => 'Variable Value'])
{{ $variableName }}
```
## WP-CLI utility
If you need to clear or compile Blade templates, you can do so with WP-CLI:
### Compile all Blade templates
```shell
$ wp acorn view:cache
```
### Clear all Blade templates
```shell
$ wp acorn view:clear
```
## Additional resources
* [Rendering Blade views for blocks, emails, and more](/acorn/docs/rendering-blade-views/)
================================================
FILE: sage/bootstrap.md
================================================
---
date_modified: 2025-02-27 14:30
date_published: 2022-02-24 10:25
description: Add Bootstrap CSS framework to Sage themes. Install Bootstrap via npm and integrate Bootstrap styles, grid system, and components into WordPress theme development.
title: How to Use Bootstrap with Sage
authors:
- ben
- code23_isaac
- diomededavid
- MWDelaney
- kellymears
- talss89
- taylorgorman
---
# How to Use Bootstrap with Sage
::: warning Setup Sass first
See [how to use Sass](./sass.md) before you follow this guide
:::
## Install Bootstrap
Add Bootstrap as a dependency:
```shell
$ npm install --save bootstrap @popperjs/core
```
Add Bootstrap to `resources/css/app.scss`:
```scss
@import "bootstrap/scss/bootstrap";
```
::: tip Bootstrap's Vite docs
See [Bootstrap's Vite docs](https://getbootstrap.com/docs/5.2/getting-started/vite/) for more information.
:::
================================================
FILE: sage/compatibility.md
================================================
---
date_modified: 2023-04-26 10:35
date_published: 2018-04-25 13:52
description: Known compatibility issues between WordPress plugins and Sage starter theme, including solutions, workarounds, and alternative plugin recommendations.
title: WordPress Plugin Compatibility with Sage
authors:
- alwaysblank
- ben
- jure
- Log1x
---
# WordPress Plugin Compatibility with Sage
A list of currently known compatibility issues with any WordPress plugins and Sage. Also take a look at the [Acorn compatibility](/acorn/docs/compatibility/) docs.
## Adding support for plugins
### WooCommerce
WooCommerce support for Sage can be added by using the [generoi/sage-woocommerce](https://github.com/generoi/sage-woocommerce) package.
## Known issues with plugins
- Disqus Comment System is [not compatible with Sage](https://github.com/roots/sage/issues/2035#issuecomment-369673419). There is a [Laravel-based solution](https://github.com/yajra/laravel-disqus) which may work (untested).
================================================
FILE: sage/compiling-assets.md
================================================
---
date_modified: 2026-03-22 11:00
date_published: 2015-09-01 18:19
description: Sage uses Vite for fast asset compilation with HMR support. Includes custom plugin for hot module replacement in WordPress block editor during development.
title: Compiling Assets in Sage with Vite
authors:
- alwaysblank
- ben
- kero
- Log1x
- octoxan
- toddsantoro
---
# Compiling Assets in Sage with Vite
[Vite](https://vite.dev/) is front-end build tool used in Sage.
Sage also uses the Laravel Vite plugin, along with Laravel's Vite facade for referencing Vite assets in PHP and Blade template files. Because of this, [Laravel's Vite documentation](https://laravel.com/docs/13.x/vite) also applies to Sage.
## Available build commands
- `npm run build` — Build assets
- `npm run dev` — Start dev server (requires updating `vite.config.js` with your local dev URL)
## Theme assets
What files are built and how is controlled from the `vite.config.js` file in the root of the theme.
The configuration will generate the following files:
- `app.css` - The primary stylesheet for the theme.
- `app.js` - The primary JavaScript file for the theme.
- `editor.css` - Styles used by the editor when creating/editing posts.
- `editor.js` - JavaScript for the block editor, i.e. block styles and variants.
It will also copy any files in the `images` or `fonts` directories under `/resources/` into the `public` directory with the other compiled files, but does not optimize or compress them. This is handled by the `assets` option on `laravel-vite-plugin` in `vite.config.js`.
### Assets in Blade template files
Use the [`Vite::asset` method](https://laravel.com/docs/13.x/vite#blade-processing-static-assets) to call assets from Blade template files:
```blade
```
### Assets in CSS
You can reference images in CSS using the included Vite alias for images.
```css
.background {
background-image: url("@images/example.svg");
}
```
### Assets in PHP
#### Get the URL of the asset
```php
use Illuminate\Support\Facades\Vite;
$asset = Vite::asset('resources/images/example.svg');
```
#### Get the contents of the asset
```php
use Illuminate\Support\Facades\Vite;
$asset = Vite::content('resources/images/example.svg');
```
================================================
FILE: sage/components.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2021-10-21 13:21
description: Components in Sage provide a structured approach for creating reusable view elements with scoped data, ideal for frequently reused theme components.
title: Creating Blade Components in Sage
authors:
- alwaysblank
- bbuilds
- ben
- code23_isaac
- Log1x
---
# Creating Blade Components in Sage
Fundamentally, Components don't do anything you couldn't also accomplish with [partials](blade-templates.md) and [Composers](composers.md), but they provide system of interaction and a mental model that can be more intuitive.
Like Composers and Blade templates, Components are an extension of the Laravel feature, so the [Laravel documentation](https://laravel.com/docs/7.x/blade#components) applies.
Generally a Component consists of:
1) A Blade template in `/resources/views/components/`.
2) A Composer-like class in `/app/View/Components/`.
The easiest way to create a component is with WP-CLI:
```shell
$ wp acorn make:component ExampleComponent
```
Sage also ships with some examples.
::: tip
You can also create [inline](https://laravel.com/docs/7.x/blade#inline-component-views) and [anonymous](https://laravel.com/docs/7.x/blade#anonymous-components) components, which forgo the template or class respectively.
These would need to be created manually
(the WP-CLI command only creates "traditional" components).
:::
## Usage
A Component in action in a Blade template will look something like this:
```blade
```
The template for that Component might look like this:
```blade
{!! $title !!}
@if($imageElement)
{!! $imageElement !!}
@endif
```
In turn, the class might look like this:
```php
namespace App\View\Components;
use Roots\Acorn\View\Component;
class ExampleComponent extends Component
{
public $title;
public $imageElement;
protected $imageId;
public function __construct($title, $imageId = null) {
$this->title = $title;
$this->imageId = $imageId;
$this->imageElement = $this->getImage();
}
protected function getImage()
{
if (!is_numeric($this->imageId)) {
return false;
}
return wp_get_attachment_image($this->imageId, 'medium_large');
}
}
```
## Argument and attribute names
The names of the arguments in the definition of your Component's `__construct()` method must match the names of the attributes you use to pass data to your Component tag.
::: warning Note
Component constructor arguments should be specified using `camelCase`, while `kebab-case` should be used when referencing the argument names in your HTML attributes. [Laravel documentation](https://laravel.com/docs/10.x/blade#casing)
:::
In the above example
```blade
```
will work, but
```blade
```
will throw an error.
The attributes used to pass data to your Component tag can be in any order, so long as the names are correct:
```blade
```
These are equivalent.
## Passing data
By default, anything passed to an attribute on a Component tag will be treated as a string.
So if you do this:
```blade
```
Your component will treat that as a string containing `$variable`, _not_ whatever the contents of `$variable` is.
If you need to pass non-string data, just prefix your attribute with a colon, and its value will be evaluated as PHP:
```blade
```
::: warning Note
Because your argument is now evaluated as PHP, you _don't_ want to pass a simple string, or PHP will try and evaluate it:
```blade
```
This will throw an error when it tries to evaluate `Uh oh` as PHP.
:::
## Data in views
The view for your Component
(in the above example, `/resources/views/components/example-component.blade.php`)
does _not_ receive the arguments you pass to the Component tag;
The data it has access to is limited to any `public` properties you've set on your class.
So remember to set those properties, or your view won't have the data you need!
## Other attributes
In the Component tag, you use attributes to pass data to your component, but you can also add other, arbitrary attributes as well.
These attributes will be put in an "attribute bag" which you can then access in your Component view with the special `$attributes` variable.
If you echo the variable it will print out each attribute and its value, which is very useful for things like passing CSS selectors to your Components:
```blade
...
```
You can do many other things with attributes that are described in the [Laravel documentation](https://laravel.com/docs/7.x/blade#managing-attributes).
================================================
FILE: sage/composers.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2021-10-21 13:21
description: Use composers to pass scoped data to any Blade view in Sage. Bind variables to templates, partials, and components for organized theme development.
title: View Composers in Sage WordPress Theme
authors:
- alwaysblank
- ben
- code23_isaac
- Log1x
---
# View Composers in Sage WordPress Theme
Composers, also sometimes called View Composers, are essentially identical to the [Laravel system of the same name](https://laravel.com/docs/7.x/views#view-composers).
They allow you to pass data to views (blade templates), scoping that data to that view (and any views it subsequently includes).
If you're familiar with [Sage 9's data filters](https://roots.io/sage/docs/blade-templates/#passing-data-to-templates), or the [Controller package](https://github.com/soberwp/controller) often used with Sage 9, then Composers are a similar concept, but much more powerful:
Instead of only allowing data binding to top-level WordPress templates, Composers allow you target _any_ view.
## Construction
::: warning Note
Composers are autoloaded, which means their naming needs to conform to the [PSR-4 standard](https://www.php-fig.org/psr/psr-4/).
:::
If you're using WP-CLI, you can create composers from the command line:
```shell
wp acorn make:composer ExampleComposer
```
This would create a Composer called `ExampleComposer` in `app/View/Composers/`.
If you're not using WP-CLI, the most basic Composer looks like this:
```php
// app/View/Composers/ExampleComposer.php
namespace App\View\Composers;
use Roots\Acorn\View\Composer;
class ExampleComposer extends Composer
{}
```
This composer doesn't do anything yet, though, so let's give it some functionality.
```php
class ExampleComposer extends Composer
{
/**
* This tells the Composer that it should bind data to the 'example'
* partial.
*/
protected static $views = [
'partials.example',
];
/**
* This will make the variable `$roots` available in the 'example' partial
* with the value described here.
*/
public function with()
{
return [
'roots' => "Tools for modern WordPress development",
];
}
}
```
Because that variable is scoped to `example.blade.php`, we'll also see the following behavior:
```blade
{{ $roots }}
@include('partials.example')
```
```blade
{{ $roots }}
@include('partials.example2')
```
```blade
{{ $roots }}
```
## Data sources
We've seen how data can be bound to views, but we only returned a hard-coded string.
Usually you'll want something more involved than that.
### WordPress
Composers are executed in a context where WordPress functions like `get_the_ID()` and `the_post()` will return expected values, so you can retrieve data from WordPress much like you normally would.
### Inherited data
Inside of a Composer, you can easily access data that has been passed to or inherited by the view through the `data` property:
```php
class Example2 extends Composer
{
...
public function with()
{
return [
'better_roots' => str_replace(
'modern',
'*awesome*',
$this->data->get('roots')
),
];
}
}
```
```blade
{{ $better_roots }}
```
### "Automatic" view selection
You can always define what view a Composer will be bound to using the `$views` property to list the name(s) of the views.
However, if your Composer will target only a single view, you can save yourself a few lines of code.
Sage will attempt to match Composers to views based on some simple file path logic:
If your view and Composer share the same path segments and name, they'll be automatically bound together.
For example, if your view is a partial at `/resources/views/partials/page-header.blade.php`, a Composer at `/app/View/Composers/PageHeader.php` will be automatically bound to it.
In other words:
- Match paths below `/resources/views` and `/app/View`.
- Convert the `kebab-case` of view file names to the `PascalCase` of Composers.
### `with()` vs `override()`
You've seen `with()` used above to pass data to views, but it has a more aggressive sibling calling `override()` which does the same thing--except that it will replace data inherited by, or passed to, the view while `with()` will not.
```blade
@include('partials.example', ['roots' => "Resources for modern WordPress development"])
{{ $roots }}
```
Using `with()`:
```php
class Example extends Composer
{
public function with()
{
return [
'roots' => "An amazing stack!",
];
}
}
```
```blade
{{ $roots }}
```
Using `override()`:
```php
class Example extends Composer
{
public function override()
{
return [
'roots' => "An amazing stack!",
];
}
}
```
```blade
{{ $roots }}
```
================================================
FILE: sage/configuration.md
================================================
---
date_modified: 2024-01-17 08:22
date_published: 2015-09-01 19:02
description: Configure Sage theme features in `setup.php`. Register menus, define sidebars, enable theme support for WordPress features, and set configuration values.
title: Configuring the Sage WordPress Theme
authors:
- alwaysblank
- ben
- Log1x
---
# Configuring the Sage WordPress Theme
## Introduction
All of the configuration for Sage lives inside of `app/setup.php`. Each option is documented allowing for you to easily familiarize yourself with the options configured out of the box.
## Theme Configuration
Configuration specific to WordPress resides in the `app/setup.php` file. In this file, you will find the default enqueued stylesheets and scripts, the supported theme features added with `add_theme_support`, and the registration hooks for navigation menus and sidebars.
By default, Sage is configured to:
- Enqueue `app.css` and `app.js` on the frontend.
- Enqueue `editor.css` and `editor.js` in the Gutenberg editor.
- Add theme support for common functionality.
- Register a default navigation menu called `primary_navigation`.
- Register a primary and footer Sidebar widget area.
### `theme.json`
Sage ships with a starter `theme.json` that is generated from the build based on your Tailwind config. See the [Gutenberg docs](/sage/docs/gutenberg/) for further information.
================================================
FILE: sage/deployment.md
================================================
---
date_modified: 2025-10-30 11:30
date_published: 2015-09-01 19:29
description: Deploy Sage themes by building assets for production, running `composer install` for dependencies, and ensuring PHP version consistency across environments.
title: Deploying the Sage WordPress Theme
authors:
- alwaysblank
- ben
- kero
- Log1x
- MWDelaney
---
# Deploying the Sage WordPress Theme
::: warning PHP versions must match
Make sure the PHP version of your development environment matches the PHP version of your production environment, or you may hit a fatal error due to your Composer dependencies requiring a different PHP version.
:::
## Deploying a Sage-based WordPress theme
1. Build theme assets (`npm run build`)
2. Install Composer dependencies (`composer install --no-dev --optimize-autoloader`)
3. Upload all files and folders in your theme except the `node_modules` directory to your host
## Optimization
Similar to deploying a Laravel app, Acorn supports an `optimize` command that will cache your configuration and views. This command should be run as part of your deployment process:
```shell
$ wp acorn optimize
```
## Server configuration
::: tip Using Trellis or Radicle?
If you are using [Trellis](/trellis/) to provision your production environment, or you are using [Radicle](/radicle/), you can **skip** this section.
:::
### Securing Blade templates
Due to the nature of WordPress, any file residing in the theme folder is publicly accessible. By default, webservers will return any requests made to a `*.blade.php` template as plain-text.
**This can create an opening for potential security risks as well as unwanted snooping.**
To prevent this from happening, we will need to add configuration to the web server to deny access to the file extension.
#### Nginx
If you are using Nginx, add the following to your site configuration before the final location directive:
```nginx
location ~* \.(blade\.php)$ {
deny all;
}
```
#### Apache
If you are using Apache, add the following to your virtual host configuration or the `.htaccess` file at the root of your web application:
```apache
# Apache 2.4
Require all denied
# Apache 2.2
Order deny,allow
Deny from all
```
## Deploying Sage with Trellis
If you use [Trellis](https://roots.io/trellis/), you can build your assets locally (or on a CI server), then copy them to the remote server during deployment.
[See the `build-before.yml` example hook](https://github.com/roots/trellis/blob/master/deploy-hooks/build-before.yml) in Trellis.
## Deploying Sage on Kinsta
[Kinsta supports Bedrock and Trellis](https://kinsta.com/blog/bedrock-trellis/?kaid=OFDHAJIXUDIV), so deploying Sage with Trellis on [Kinsta](https://kinsta.com/?kaid=OFDHAJIXUDIV) is possible by following a few extra steps.
================================================
FILE: sage/fonts-setup.md
================================================
---
date_modified: 2025-02-27 14:30
date_published: 2023-02-20 11:30
description: Set up custom fonts in Sage using `theme.json`. Define font families that work in both theme frontend and WordPress block editor for consistent typography.
title: Setting Up Custom Fonts in Sage
authors:
- ben
---
# Setting Up Custom Fonts in Sage
Sage includes an empty `resources/fonts/` directory for you to use for any fonts you want to use in your theme.
## Add your fonts
The first step to setting up a font in Sage is to add the `woff2` file to the `resources/fonts/` directory. Since [woff2 usage](https://caniuse.com/?search=woff2) is so high, you probably don't need to consider any other font file formats.
For this example, we're going to download [Public Sans from the google-webfonts-helper](https://gwfh.mranftl.com/fonts/public-sans?subsets=latin). The [google-webfonts-helper](https://gwfh.mranftl.com/) is a good resource for quickly grabbing font files and their CSS from Google Fonts.
```plaintext
resources
├── css
│ ├── app.css
│ ├── fonts.css # Create this file
│ └── editor.css
├── fonts
│ └── public-sans-v14-latin-regular.woff2
├── images
├── js
└── views
```
## Add the CSS
You can place the CSS for your web fonts wherever you'd like. We recommend creating a `css/fonts.css` file and then importing it from `app.css` and `editor.css`:
```css
@import './fonts.css';
```
Define your `@font-face` in `css/fonts.css`:
```css
@font-face {
font-display: swap;
font-family: 'Public Sans';
font-style: normal;
font-weight: 400;
src: url('@fonts/public-sans-v18-latin-regular.woff2') format('woff2'),
}
```
## Add the font to your Tailwind theme
Open `app.css` and add the new font family:
```css
@theme {
--font-sans: "Public Sans", sans-serif;
}
```
See the [Tailwind CSS docs on customizing fonts](https://tailwindcss.com/docs/font-family#customizing-your-theme) for more information.
================================================
FILE: sage/functionality.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-09-01 19:05
description: The `app/` directory contains theme functionality. Sage is a starter theme, so modify files in `app/` to implement custom features for your WordPress site.
title: Adding Theme Functionality in Sage
authors:
- alwaysblank
- ben
- jure
- Log1x
---
# Adding Theme Functionality in Sage
The `app/` directory contains all the theme functionality. Since Sage is a starter theme, it’s okay for you to modify files within `app/` to meet the needs of the site you’re building.
Most of the PHP code in Sage is namespaced and autoloaded, so make sure to use namespaced functions and classes. If you aren't familiar with these methods, see our blog posts on:
* [Namespacing and Autoloading](/namespacing-and-autoloading/)
* [Upping PHP Requirements in Your WordPress Themes and Plugins](/upping-php-requirements-in-your-wordpress-themes-and-plugins/)
## The `app/` directory
- `app/setup.php` — Enqueue stylesheets and scripts, register support for theme features with `add_theme_support`, register navigation menus and sidebars.
See [Theme Configuration and Setup](configuration.md).
- `app/filters.php` — Add WordPress filters in this file.
Filters included by default:
- `excerpt_more` — add "… Continued" to excerpts.
- `app/Providers` — The place for any [Service Providers](https://laravel.com/docs/10.x/providers) you care to define for your theme.
Comes with `ThemeServiceProvider` that adds no functionality but provides a template for your own Service Providers.
- `app/View` — The place for view-related code, i.e. Composers and Components.
For more information, see the documentation on [Composers](composers.md) and [Components](components.md).
================================================
FILE: sage/gutenberg.md
================================================
---
date_modified: 2025-02-27 14:00
date_published: 2021-10-21 13:21
description: Sage includes full WordPress block editor support with HMR for editor styles, ensuring consistent styling between editor and frontend with `theme.json` integration.
title: Gutenberg Block Editor Support in Sage
authors:
- alwaysblank
- ben
- joshf
- Log1x
- strarsis
---
# Gutenberg Block Editor Support in Sage
Sage includes two assets that are enqueued when working with the WordPress block editor, also known as Gutenberg:
* `resources/js/editor.js`
* `resources/css/editor.css`
Any styles added to `editor.css` will only be applied to the block editor.
::: warning All blocks must have version 3 support
Sage's `editor.css` expects the block editor to be iframed. If you have any blocks that don't support version 3, then the block editor won't be iframed.
:::
([Reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-api-versions/#version-3-wordpress-6-3))
## `theme.json` generator
Sage includes a `theme.json` file for configuring the WordPress editor. It's generated during [asset builds](compiling-assets.md) automatically and accounts for settings from your Tailwind setup.
::: tip theme.json spec
Reference [the `theme.json` documentation](https://developer.wordpress.org/block-editor/how-to-guides/themes/global-settings-and-styles/) for the full specification.
:::
Due to Sage including a `theme.json` file, this means [trying to use `add_theme_support()` to configure the editor](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-support/) will not work.
Tailwind CSS colors, font families, and sizes are generated on the theme build for `theme.json`. See the [Sage docs on Tailwind CSS](/sage/docs/tailwind-css/) for more information. This can be disabled in `vite.config.js`.
================================================
FILE: sage/installation.md
================================================
---
date_modified: 2025-02-27 13:45
date_published: 2015-08-29 18:09
description: Install Sage WordPress starter theme by running `composer create-project roots/sage`. Start modern WordPress theme development with Sage foundation.
title: Installing Sage WordPress Starter Theme
authors:
- alwaysblank
- ben
- Jacek
- Lachlan_Arthur
- Log1x
- QWp6t
- TangRufus
---
# Installing Sage WordPress Starter Theme
Install Sage using Composer from your WordPress themes directory:
```shell
$ composer create-project roots/sage your-theme-name
```
To install the latest development version of Sage, add `dev-main` to the end of the command:
```shell
$ composer create-project roots/sage your-theme-name dev-main
```
## Build assets
- Edit the `base` path in `vite.config.js`
- Run `npm install` from the theme directory to install dependencies
- Run `npm run build` to compile assets
You must build theme assets in order to access your site. Failing to build the assets will result in the error:
```plaintext
Vite manifest not found at [/path/to/sage/public/build/manifest.json] cannot be found.
```
================================================
FILE: sage/localization.md
================================================
---
date_modified: 2025-12-22 13:00
date_published: 2018-04-24 09:47
description: Generate translation files for Sage themes with custom build scripts. Create and load language files for multilingual WordPress sites with proper loading.
title: Localizing the Sage WordPress Theme
authors:
- alwaysblank
- ben
- bonakor
- jure
- Log1x
- strarsis
---
# Localizing the Sage WordPress Theme
## Generating language files
Run `yarn translate:pot` from your theme directory to generate the language files. Then open the generated `.pot` file with [Poedit](https://poedit.com/), select "Create new translation", save `.mo` & `.po` files in the `resources/lang` folder with the correct name syntax (eg. `fr_FR`, `en_US`).
When adding/removing translations in templates, run `yarn translate:update`, then select "Catalog > Update from a POT file" in Poedit.
## Loading language files
Add the following to `app/setup.php`:
```php
add_action('after_setup_theme', function () {
load_textdomain( 'sage', get_template_directory() . '/resources/lang/' . determine_locale() . '.mo' );
});
```
Make sure language files exist in the `resources/lang` directory.
## Polylang and Sage
- Install [BenjaminMedia/wp-polylang-theme-strings](https://github.com/BenjaminMedia/wp-polylang-theme-strings)
- Replace `__()` with `pll__()` in your templates
Need to also translate strings from the `app/` folder? See [`Sage_Polylang_Theme_Translation`](https://github.com/roots/sage/issues/1875#issuecomment-380076482).
================================================
FILE: sage/sass.md
================================================
---
date_modified: 2026-03-22 11:15
date_published: 2023-06-06 17:30
description: Enable Sass in Sage by renaming `app.css` to `app.scss` for advanced CSS preprocessing with variables and mixins.
title: Using Sass with Sage WordPress Theme
authors:
- ben
- carlosfaria
- code23_isaac
- diomededavid
- MWDelaney
- kellymears
- talss89
---
# Using Sass with Sage WordPress Theme
Remove Tailwind CSS dependencies: `npm uninstall -D @tailwindcss/vite tailwindcss`
Delete the contents of `resources/css/app.css` and `resources/css/editor.css`.
Add the `sass` extension:
```shell
$ npm install -D sass
```
In the `resources/css` directory, rename `app.css` to `app.scss` and rename `editor.css` to `editor.scss`.
```plaintext
app.css -> app.scss
editor.css -> editor.scss
```
Modify `vite.config.js` to remove the Tailwind plugin, reference the new file extensions, and disable the Tailwind CSS `theme.json` generation:
```diff
import { defineConfig } from 'vite'
-import tailwindcss from '@tailwindcss/vite';
import laravel from 'laravel-vite-plugin'
import { wordpressPlugin, wordpressThemeJson } from '@roots/vite-plugin';
export default defineConfig({
base: '/app/themes/sage/public/build/',
plugins: [
- tailwindcss(),
laravel({
input: [
- 'resources/css/app.css',
+ 'resources/css/app.scss',
'resources/js/app.js',
- 'resources/css/editor.css',
+ 'resources/css/editor.scss',
'resources/js/editor.js',
],
assets: ['resources/images/**', 'resources/fonts/**'],
wordpressThemeJson({
- disableTailwindColors: false,
- disableTailwindFonts: false,
- disableTailwindFontSizes: false,
+ disableTailwindColors: true,
+ disableTailwindFonts: true,
+ disableTailwindFontSizes: true,
}),
],
```
Modify the `@vite()` directive in `resourves/views/layouts/app.blade.php` to use `app.scss` instead of `app.css`:
```diff
- @vite(['resources/css/app.css', 'resources/js/app.js'])
+ @vite(['resources/css/app.scss', 'resources/js/app.js'])
```
Modify the `block_editor_settings_all` filter in `app/setup.php` to use `editor.scss` instead of `editor.css`;
```diff
add_filter('block_editor_settings_all', function ($settings) {
- $style = Vite::asset('resources/css/editor.css');
+ $style = Vite::asset('resources/css/editor.scss');
$settings['styles'][] = [
'css' => "@import url('{$style}')",
];
return $settings;
});
```
================================================
FILE: sage/structure.md
================================================
---
date_modified: 2025-02-27 13:50
date_published: 2021-10-21 13:21
description: Sage's directory structure provides organized folders for scalable development. `resources/` for views, `app/` for functionality, `config/` for settings.
title: Sage WordPress Theme Structure
authors:
- alwaysblank
- ben
- jure
- Log1x
- MWDelaney
---
# Sage WordPress Theme Structure
## Introduction
The default Sage structure is intended to provide a sane starting point for both small and large WordPress sites alike.
Where a file or class is located is ultimately decided by you. As long as Composer can autoload the class or you have modified the necessary paths in your [configuration](configuration.md), things should work as expected.
```plaintext
themes/your-theme-name/ # → Root of your Sage based theme
├── app/ # → Theme PHP
│ ├── Providers/ # → Service providers
│ ├── View/ # → View models
│ ├── filters.php # → Theme filters
│ └── setup.php # → Theme setup
├── public/ # → Built theme assets (never edit)
├── resources/ # → Theme assets and templates
│ ├── css/ # → Theme stylesheets
│ ├── fonts/ # → Theme fonts
│ ├── images/ # → Theme images
│ ├── js/ # → Theme JavaScript
│ └── views/ # → Theme templates
│ ├── components/ # → Component templates
│ ├── forms/ # → Form templates
│ ├── layouts/ # → Base templates
│ └── partials/ # → Partial templates
├── vendor/ # → Composer packages (never edit)
├── composer.json # → Autoloading for `app/` files
├── functions.php # → Theme bootloader
├── index.php # → Theme template wrapper
├── node_modules/ # → Node packages (never edit)
├── package.json # → Node dependencies and scripts
├── screenshot.png # → Theme screenshot for WP admin
├── style.css # → Theme meta information
└── vite.config.js # → Vite configuration
```
## The root directory
### The `app/` directory
The majority of your theme functionality lives in the `app` directory. By default, this directory is namespaced under `App` and is automatically loaded by Composer using the [PSR-4 autoloading standard](https://www.php-fig.org/psr/psr-4/). See our blog post on [Namespacing and Autoloading](/namespacing-and-autoloading/) if you aren't familiar with these methods.
This directory is covered more in [Functionality](/sage/docs/functionality/).
### The `public/` directory
The `public` directory contains the compiled assets for your theme. This directory will never be manually modified.
### The `node_modules/` directory
The `node_modules` directory contains your [Node](https://nodejs.org/) dependencies used to build your assets during development or for production. This folder is automatically generated and should not be modified.
::: danger Don’t upload node_modules
Under no circumstances should there ever be a need to upload this folder or any of its contents to a live production server. It is a security risk.
:::
### The `resources/` directory
The `resources` directory contains your Blade views as well as the un-compiled assets of your theme such as CSS, JavaScript, images, and fonts.
### The `vendor/` directory
The `vendor` directory contains your [Composer](https://getcomposer.org/) dependencies and autoloader. This directory is automatically generated and should not be modified.
================================================
FILE: sage/tailwind-css.md
================================================
---
date_modified: 2025-02-27 14:00
date_published: 2023-03-13 11:00
description: Sage generates `theme.json` from Tailwind configuration automatically, making Tailwind color palette, font families, and sizes available in WordPress block editor.
title: Using Tailwind CSS with Sage Theme
authors:
- ben
- MWDelaney
---
# Using Tailwind CSS with Sage Theme
Sage includes support for Tailwind CSS out of the box, along with some helpful functionality for integrating your Tailwind config into the WordPress block editor.
## Generating `theme.json` from your Tailwind setup
Sage includes a `theme.json` file for configuring the WordPress editor. It's generated during [asset builds](compiling-assets.md) automatically and accounts for settings from your `app.css` file.
This functionality is handled by the [`@roots/vite-plugin`](https://github.com/roots/vite-plugin) included in Sage.
To modify this behavior, open `vite.config.js` and edit the `wordpressThemeJson` plugin's configuration.
### Default color palette
Rather than [manually defining the editor colors](https://developer.wordpress.org/themes/global-settings-and-styles/settings/color/) by adding them to `theme.json`, your Tailwind config will be used to generate colors for the WordPress editor.
Tailwind’s [default color palette](https://tailwindcss.com/docs/colors) is a good starting point for sites that don’t already have color/branding guidelines to follow.
### Sizes and font families
In addition to including Tailwind’s color palette for the WordPress editor, Sage will also configure the editor with Tailwind’s font families and font sizes.
================================================
FILE: sage/theme-templates.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-09-01 19:12
description: Sage's `resources/views/` directory contains Blade templates following WordPress template hierarchy. Extend templates using standard WordPress conventions.
title: WordPress Theme Templates in Sage
authors:
- alwaysblank
- ben
- Log1x
---
# WordPress Theme Templates in Sage
The `resources/views/` directory contains files that you can further extend with the normal [WordPress Template Hierarchy](https://developer.wordpress.org/themes/classic-themes/basics/template-hierarchy/):
- `404.blade.php` – Error 404 page
- `index.blade.php` – Archive page (used by blog page, category archives, author archives and more)
- `page.blade.php` – Single page
- `search.blade.php` – Search results page
- `single.blade.php` – Single post page
- `template-custom.blade.php` – An example single page template
All templates are wrapped by a base file in the `layouts/` directory:
- `app.blade.php` – The base template which wraps the base markup around all template files
::: warning Note
The `app` layout contains all the content generated by Blade templates, but is itself wrapped by the `index.php` in the root of the theme.
:::
These files include templates from the `resources/views/partials/` directory which is where you'll be making most of your customizations:
- `comments.blade.php` – Markup for comments
- `content-page.blade.php` – Markup included from `resources/views/page.blade.php`
- `content-search.blade.php` – Markup included from `resources/views/search.blade.php`
- `content-single.blade.php` – Markup included from `resources/views/single.blade.php`
- `content.blade.php` – Markup included from `resources/views/index.blade.php`
- `entry-meta.blade.php` – Post entry meta information included from `resources/views/content-single.blade.php`
- `footer.blade.php` – Footer markup included from `resources/views/app.blade.php`
- `header.blade.php` – Header markup included from `resources/views/app.blade.php`
- `page-header.blade.php` – Page title markup included from most of the files in the `resources/views/` directory
- `sidebar.blade.php` – Sidebar markup included from `resources/views/app.blade.php`
## Extending templates
The normal [WordPress Template Hierarchy](https://developer.wordpress.org/themes/classic-themes/basics/template-hierarchy/) is still intact. Here’s some examples:
- Copy `index.blade.php` to `author.blade.php` for customizing author archives
- Copy `index.blade.php` to `home.blade.php` for customizing the Home page if you’re showing the latest posts (under Reading Settings) instead of a static front page
- Copy `index.blade.php` to `archive-gallery.blade.php` for customizing the archive page for a custom post type registered as `gallery`
- Copy `page.blade.php` to `front-page.blade.php` for customizing the static front page
- Copy `page.blade.php` to `page-about.blade.php` for customizing a page called About
================================================
FILE: sage/use-blade-icons.md
================================================
---
date_modified: 2024-04-24 13:00
date_published: 2022-03-18 20:49
description: Install and use blade-icons in Sage for SVG icon components in Blade templates. Simplifies icon management with clean component syntax.
title: How to Use blade-icons with Sage
authors:
- altan
- ben
---
# How to Use blade-icons with Sage
The [blade-icons](https://github.com/driesvints/blade-icons) package allows you to easily use SVG's in your Blade views.
Besides being able to use your own SVG's, you can also add one of the many third party icon sets, such as:
* [Blade Bootstrap Icons](https://github.com/davidhsianturi/blade-bootstrap-icons)
* [Blade Font Awesome](https://github.com/owenvoke/blade-fontawesome)
* [Blade Heroicons](https://github.com/driesvints/blade-heroicons)
* [Blade Simple Icons](https://github.com/ublabs/blade-simple-icons)
[](https://blade-ui-kit.com/blade-icons)
## Installation
From the same directory where you've installed Acorn (typically your site root or your Sage theme folder), add `blade-icons` as a Composer dependency:
```shell
$ composer require blade-ui-kit/blade-icons
```
Then publish the configuration file:
```shell
$ wp acorn vendor:publish --tag=blade-icons
```
## Configuration
From the published `config/blade-icons.php` file, we recommend setting the default set to point to your theme directory:
```php
[
'default' => [
'path' => 'web/app/themes/sage/resources/images/icons', # Relative path to the new directory
'prefix' => 'icon',
],
],
];
```
## Adding icons
Add a new directory inside `resources/images/` named `icons/` and place your SVG icons in this directory.
## Using icons in Blade views
From your Blade views you can now use the provided Blade component, or the `@svg` directive:
```blade
@svg('example-icon')
```
## Adding icon sets
`blade-icons` supports a **lot** of different icon sets through packages made through the community. The [Blade icons search](https://blade-ui-kit.com/blade-icons#search) allows you to quickly find a new icon to use in your project.
To add aditional icon sets, require them as Composer dependencies the same as you did for the `blade-icons` package. In this example, we'll add the `blade-heroicons` package:
```shell
$ composer require blade-ui-kit/blade-heroicons
```
Now Heroicons can be referenced in any of the supported methods from inside your Blade views:
```blade
@svg('heroicon-s-menu')
{{ svg('heroicon-s-menu) }}
```
## Caching icons in production
It's recommended to enable icon caching to optimize performance by running `wp acorn icons:cache` during deployment.
If you are using Trellis, modify the `deploy_build_after` hook within your `deploy-hooks/build-after.yml` file:
```yml
- name: Cache Blade UI Icons
command: wp acorn icons:cache
args:
chdir: "{{ deploy_helper.new_release_path }}"
```
## Additional information
The [blade-icons README](https://github.com/driesvints/blade-icons) covers how to pass attributes, set default classes, and more.
================================================
FILE: sage/woocommerce.md
================================================
---
date_modified: 2025-06-26 11:00
date_published: 2025-06-26 11:00
description: Set up WooCommerce in Sage themes for eCommerce functionality. Configure templates, declare theme support, and integrate WooCommerce styling with Sage.
title: Setting Up WooCommerce with Sage Theme
authors:
- aitor
- csorrentino
- ben
- strarsis
- YourRightWebsite
---
# Setting Up WooCommerce with Sage Theme
[WooCommerce](https://woocommerce.com/) is compatible with Sage's Blade templates with the correct setup.
## Add the sage-woocommerce package
The [`generoi/sage-woocommerce`](https://github.com/generoi/sage-woocommerce) package adds functionality to allow Blade templates to work on Acorn and Sage powered WordPress sites:
```shell
$ composer require generoi/sage-woocommerce
```
### Publish the templates to your theme
Add the required `single-product.blade.php` and `archive-product.blade.php` views to your theme:
```shell
$ wp acorn vendor:publish --tag="woocommerce-template-views"
```
You can now edit these templates from `resources/views/woocommerce/`.
## Update WooCommerce default pages
In WooCommerce 9.x+, the default pages (Shop, Cart, Checkout) are created with block-based content by default. These do not use classic templates.
Remove the default blocks from these pages and replace them with the relevant shortcodes:
* `[woocommerce_cart]`
* `[woocommerce_checkout]`
## Disable "Coming soon mode"
In WooCommerce 9.x+, Coming soon mode is enabled by default for all stores from **Settings > Site visibility**.
Until it’s done, WooCommerce keeps the Shop page in a special unpublished state that behaves differently depending on whether you’re logged in or not:
* Logged-in users see the correct template
* Logged-out users are shown a block-based fallback, ignoring the theme's templates entirely
================================================
FILE: trellis/ansible.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2022-02-28 22:16
description: Understand how Trellis leverages Ansible for WordPress automation. Learn key concepts like playbooks, roles, tasks, and variables used for server management.
title: How Trellis Uses Ansible for WordPress
authors:
- swalkinshaw
---
# How Trellis Uses Ansible for WordPress
Since Trellis is powered by Ansible, the best way to understand Trellis is to understand Ansible itself.
Even knowing a few just key Ansible concepts will help you learn Trellis and how to
customize it to fit your needs.
Ansible's own [documentation](https://docs.ansible.com/projects/ansible/latest/user_guide/index.html) is very comprehensive and should be considered as an extension of Trellis' documentation.
However, since Ansible itself is unopinionated, this will explore some key
concepts and how they apply to Trellis.
## Playbooks
At the highest level, Trellis provides a few playbooks which execute _tasks_
organized into _roles_.
Trellis' playbooks are found in the root of Trellis itself:
* [`dev.yml`](https://github.com/roots/trellis/blob/master/dev.yml) - provisions a development server. This playbook assumes that your local Trellis project files have been synced to a virtual machine and automatically installs WordPress.
* [`server.yml`](https://github.com/roots/trellis/blob/master/dev.yml) -
provisions a remote (non-dev) server. This playbook assumes you will be
deploying sites separately and does not attempt to install WordPress.
* [`deploy.yml`](https://github.com/roots/trellis/blob/master/deploy.yml) - deploys a single site to an environment
* [`rollback.yml`](https://github.com/roots/trellis/blob/master/deploy.yml) - rolls back a previously deployed release
* [`xdebug-tunnel.yml`](https://github.com/roots/trellis/blob/master/xdebug-tunnel.yml) - opens or closes the PHP Xdebug tunnel
## Roles
Each playbook listed above contains a list of roles to run. A role's main
purpose is to group a collection of tasks to run within the `tasks` directory.
All of Trellis' roles are found under the top-level [`roles`](https://github.com/roots/trellis/tree/master/roles) directory. Additionally, there are some 3rd party community roles used from Ansible Galaxy which are specified in the [`galaxy.yml`](https://github.com/roots/trellis/blob/master/galaxy.yml) file.
Roles in Trellis usually contain one of more of these subfolders:
* `defaults` - variables defined with low precedence
* `tasks` - tasks to be executed - the main functional part of roles
* `templates` - templates in Jinja format which are used in tasks
## Inventory
In Ansible,
[inventory](https://docs.ansible.com/projects/ansible/latest/user_guide/intro_inventory.html#intro-inventory) is a list of defined hosts in your infrastructure.
For most Trellis projects, this list of hosts is usually one development
virtual machine, one staging server (optional), and one production server.
If you look at the default inventory files in [`hosts`](https://github.com/roots/trellis/tree/master/hosts) directory, you'll see three files named after the standard environments: `development`, `staging`, `production`.
Here's what an inventory file in Trellis looks like:
```ini
[production]
your_server_hostname
[web]
your_server_hostname
```
Each host is under two groups: the environment (`production` in this case) and `web`. These groups can be used
for any semantic grouping you want, but in Trellis you at least need those two
built-in ones.
## Group variables
The "group" naming isn't the most clear, but as shown above, these refer to Ansible's concept of "inventory groups".
And since Trellis' inventory hosts are named for environments, "group vars" are
really just _environment specific variables_. Though they can also be used for any
semantic grouping of inventory hosts for more advanced use cases.
Note: the `all` group (in `group_vars/all`) is special and applies to all groups.
## Variables
All variables in Ansible can be considered _global_. Even if a variable is
defined within a role (eg: `roles/nginx/defaults/main.yml`), it can be
referenced or re-defined in a `group_vars` file. Once a role is included in a
playbook, their variables (in `defaults` or `vars`) are available globally.
### Example
As an example, let's say you wanted to change PHP's max execution time in development to be
higher than in production.
[`php_max_execution_time`](https://github.com/roots/trellis/blob/40b949a910373398e3fda06105287e0edf24051a/roles/php/defaults/main.yml#L10) is found in [`roles/php/defaults/main.yml`](https://github.com/roots/trellis/blob/master/roles/php/defaults/main.yml).
We can apply two things we learned above:
1. variables are global
2. group vars can be used to define environment specific values
Taking advantage of Ansible's [variable
precendence](https://docs.ansible.com/projects/ansible/latest/user_guide/playbooks_variables.html#understanding-variable-precedence), we'll just override the variable by re-defining it in `group_vars/development/php.yml`:
```yaml
php_max_execution_time: 500
```
================================================
FILE: trellis/cli.md
================================================
---
date_modified: 2024-09-11 10:00
date_published: 2023-04-05 07:42
description: Use the Trellis CLI to manage WordPress projects via the `trellis` command. Simplifies provisioning servers, deploying sites, and common Trellis tasks.
title: Trellis CLI Command-Line Interface
authors:
- swalkinshaw
---
# Trellis CLI Command-Line Interface
trellis-cli is a command-line interface (CLI) to manage Trellis projects via the `trellis` command. The CLI provides a more consistent and integrated experience and includes:
* Automatic Python Virtualenv integration for easier dependency management
* Smart autocompletion (based on your defined environments and sites)
* One-command cloud server creation (DigitalOcean, Hetzner Cloud)
* Better Ansible Vault support for encrypting files
* (New) Built-in virtual machine support for development environments
and much more.
## Installation
### Quick Install (macOS, Linux via Homebrew)
```shell
$ brew install roots/tap/trellis-cli
```
### Script
We also offer a quick script version:
```shell
$ curl -sL https://roots.io/trellis/cli/get | bash
```
### Manual Install
trellis-cli provides binary releases for a variety of OSes. These binary versions can be manually downloaded and installed.
1. Download the [latest release](https://github.com/roots/trellis-cli/releases/latest) or any [specific version](https://github.com/roots/trellis-cli/releases)
2. Unpack it (eg: `tar -zxvf trellis_1.2.1_Linux_x86_64.tar.gz`)
3. Find the `trellis` binary in the unpacked directory, and move it to its desired destination (eg: `mv trellis_1.2.0_Darwin_x86_64/trellis /usr/local/bin/trellis`)
4. Make sure the above path is in your `$PATH`
### Dev/unstable install (macOS, Linux via Homebrew)
```shell
$ brew uninstall roots/tap/trellis-cli
```
```shell
$ brew install --HEAD roots/tap/trellis-cli-dev
```
```shell
$ brew upgrade --fetch-HEAD roots/tap/trellis-cli-dev
```
## Usage
Run `trellis` for the complete usage and help.
For subcommand documentation, run `trellis -h`.
### Commands
| Command | Description |
| --- | --- |
| `alias` | Generate WP CLI aliases for remote environments |
| `check` | Checks if the required and optional Trellis dependencies are installed |
| `db` | Commands for database management |
| `deploy` | Deploys a site to the specified environment |
| `dotenv` | Template .env files to local system |
| `server` | Commands for cloud server management (DigitalOcean, Hetzner Cloud) |
| `exec` | Exec runs a command in the Trellis virtualenv |
| `galaxy` | Commands for Ansible Galaxy |
| `info` | Displays information about this Trellis project |
| `init` | Initializes an existing Trellis project |
| `key` | Commands for managing SSH keys |
| `logs` | Tails the Nginx log files for an environment |
| `new` | Creates a new Trellis project |
| `open` | Opens user-defined URLs (and more) which can act as shortcuts/bookmarks specific to your Trellis projects |
| `provision` | Provisions the specified environment |
| `rollback` | Rollback the last deploy of the site on the specified environment |
| `shell-init` | Prints a script which can be eval'd to set up Trellis' virtualenv integration in various shells |
| `ssh` | Connects to host via SSH |
| `valet` | Commands for Laravel Valet |
| `vault` | Commands for Ansible Vault |
| `vm` | Commands for managing development virtual machines |
| `xdebug-tunnel` | Commands for managing Xdebug tunnels |
## Configuration
There are four ways to set configuration settings for trellis-cli and they are
loaded in this order of precedence:
1. global config (`$HOME/.config/trellis/cli.yml`)
2. project config (`trellis.cli.yml`)
3. project config local override (`trellis.cli.local.yml`)
4. env variables
The global CLI config (defaults to `$HOME/.config/trellis/cli.yml`)
and will be loaded first (if it exists).
Next, if a project is detected, the project CLI config will be loaded if it
exists at `trellis.cli.yml` (within your `trellis` directory).
A Git ignored local override config is also supported at `trellis.cli.local.yml`.
Finally, env variables prefixed with `TRELLIS_` will be used as
overrides if they match a supported configuration setting. The prefix will be
stripped and the rest is lowercased to determine the setting key.
Note: only string, numeric, and boolean values are supported when using environment
variables.
Current supported settings:
| Setting | Description | Type | Default |
| --- | --- | -- | -- |
| `allow_development_deploys` | Whether to allows deploy to the `development` env | boolean | false |
| `ask_vault_pass` | Set Ansible to always ask for the vault pass | boolean | false |
| `check_for_updates` | Whether to check for new versions of trellis-cli | boolean | true |
| `database_app` | Database app to use in `db open` (Options: `tableplus`, `sequel-ace`)| string | none |
| `load_plugins` | Load external CLI plugins | boolean | true |
| `open` | List of name -> URL shortcuts | map[string]string | none |
| `virtualenv_integration` | Enable automated virtualenv integration | boolean | true |
| `server` | Options for cloud server management | Object | see below |
| `vm` | Options for dev virtual machines | Object | see below |
### `server`
| Setting | Description | Type | Default |
| --- | --- | -- | -- |
| `provider` | Cloud provider (Options: `digitalocean`, `hetzner`)| string | `digitalocean` |
### `vm`
| Setting | Description | Type | Default |
| --- | --- | -- | -- |
| `manager` | VM manager (Options: `auto` (depends on OS), `lima`)| string | "auto" |
| `ubuntu` | Ubuntu OS version (Options: `18.04`, `20.04`, `22.04`, `24.04`)| string | `24.04` |
| `hosts_resolver` | VM hosts resolver (Options: `hosts_file`)| string | `hosts_file` |
| `images` | Custom OS image | object | Set based on `ubuntu` version |
#### `images`
| Setting | Description | Type | Default |
| --- | --- | -- | -- |
| `location` | URL of Ubuntu image | string | none |
| `arch` | Architecture of image (eg: `x86_64`, `aarch64`) | string | none |
Example config:
```yaml
ask_vault_pass: false
check_for_updates: true
load_plugins: true
open:
site: "https://mysite.com"
admin: "https://mysite.com/wp/wp-admin"
server:
provider: digitalocean
virtualenv_integration: true
vm:
manager: auto
ubuntu: 24.04
```
Example env var usage:
```shell
$ TRELLIS_ASK_VAULT_PASS=true trellis provision production
```
================================================
FILE: trellis/composer-authentication.md
================================================
---
date_modified: 2026-03-11 12:00
date_published: 2021-09-06 16:48
description: Set up Composer authentication in Trellis to access private packages, commercial plugins, and authenticated repositories during deployment.
title: Composer Authentication
authors:
- ben
- swalkinshaw
- TangRufus
---
# Composer Authentication
Many paid WordPress plugins also offer Composer support. Typically, this is accomplished by adding the plugin repository to your composer.json file:
```json
"repositories": [
{
"type":"composer",
"url":"https://example.com"
}
]
```
The actual plugin download is usually protected behind an authentication layer. This allows the plugin developer to restrict access to the plugin via Composer. The authentication credentials are stored in an auth.json file.
However, when using such plugins in a Trellis project, it is generally considered bad practice to implement this via [deploy hooks](https://discourse.roots.io/t/interactive-console-authentication-for-3rd-party-repository-on-deploy/8592/2) or adding the `auth.json` to your version control.
Trellis supports authentication for multiple Composer repositories, via the Ansible [Vault](/trellis/docs/vault/#steps-to-enable-ansible-vault) functionality, on a per environment configuration.
## Supported authentication types
| Type | Description |
| --- | --- |
| `http-basic` | HTTP basic authentication (username/password) |
| `bearer` | HTTP Bearer token authentication |
| `github-oauth` | GitHub OAuth token |
| `gitlab-oauth` | GitLab OAuth token |
| `gitlab-token` | GitLab personal/deploy token |
| `bitbucket-oauth` | Bitbucket OAuth consumer key/secret |
| `custom-headers` | Custom HTTP header authentication |
## HTTP Basic
If `type` is omitted, it defaults to `http-basic` for backward compatibility.
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: http-basic, hostname: example.com, username: my-username, password: my-password }
```
If the private repository doesn't use a password (because the username contains an API key for example), you can omit `password`:
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: http-basic, hostname: example.com, username: apikey }
```
## Bearer
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: bearer, hostname: example.com, token: my-token }
```
## GitHub OAuth
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: github-oauth, hostname: github.com, token: my-github-token }
```
## GitLab OAuth
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: gitlab-oauth, hostname: gitlab.com, token: my-gitlab-oauth-token }
```
## GitLab Token
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: gitlab-token, hostname: gitlab.com, token: my-gitlab-token }
```
## Bitbucket OAuth
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: bitbucket-oauth, hostname: bitbucket.org, consumer_key: my-consumer-key, consumer_secret: my-consumer-secret }
```
## Custom Headers
For private repositories that use custom HTTP headers for authentication:
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: custom-headers, hostname: repo.example.org, headers: ["API-TOKEN: my-api-token"] }
```
Multiple headers can be specified:
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: custom-headers, hostname: repo.example.org, headers: ["API-TOKEN: my-api-token", "X-CUSTOM-HEADER: value"] }
```
## Multiple repositories
Multiple private Composer repositories can be configured together:
```yaml
# group_vars//vault.yml
vault_wordpress_sites:
example.com:
composer_authentications:
- { type: http-basic, hostname: example.com, username: my-username, password: my-password }
- { type: github-oauth, hostname: github.com, token: my-github-token }
- { type: bearer, hostname: private-registry.com, token: my-token }
```
## Requirements
- Passwords and tokens should not be stored as plain text, as described in the [Vault](/trellis/docs/vault/) documentation
================================================
FILE: trellis/configuring-php.md
================================================
---
date_modified: 2025-04-01 00:00
date_published: 2025-04-01 00:00
description: Configure PHP settings in Trellis by overriding default values. Adjust memory limits, max execution time, upload sizes, and other PHP directives per site.
title: Configuring PHP Settings in Trellis
authors:
- dalepgrant
---
# Configuring PHP Settings in Trellis
Trellis will setup PHP and extensions suitable for a WordPress environment out of the box, but you may want to customise for your own setups. For example, you may want to change the version of PHP or add an extension.
::: tip Note
If you make a change to the PHP settings, you will need to [reprovision](https://roots.io/trellis/docs/local-development/#re-provisioning) your environments before seeing any changes. You can use `--tags=php` to only run the required role(s). Be sure to reprovision all the environments you need to.
:::
## Change the version of PHP
In [`group_vars/all/main.yml`](https://github.com/roots/trellis/blob/master/group_vars/all/main.yml), set the value of `php_version` to the version you're working with.
e.g. to use PHP 8.1: `php_version: "8.1"`
As of [#1560](https://github.com/roots/trellis/pull/1560) Trellis supports 7.4 & 8.1 up to 8.4. Newer versions may work when released but may not have been tested by the community yet - you can help by testing yourself and reporting your progress on [Roots Discourse](https://discourse.roots.io/).
## Changing the default extensions
Trellis will look for a version-specific override file before falling back to the default PHP extensions. If you'd like to change the extensions installed on your environments, duplicate the [`roles/php/vars/version-specific-defaults.yml`](https://github.com/roots/trellis/blob/master/roles/php/vars/version-specific-defaults.yml) file and rename it to the version of PHP you are targetting.
### Example
To target PHP 8.4, duplicate and rename so that your folder looks like this:
```diff
├── ...other folders...
└── roles/
├── ...other folders...
└── php/
├── ...other folders...
└── vars/
+ ├── 8.4.yml
└── version-specific-defaults.yml
```
In your `8.4.yml` file you can then set the extensions you'd like to use. Include all extensions from the defaults file unless you have a good reason not to, remembering that [WP requires some extensions](https://make.wordpress.org/hosting/handbook/server-environment/#php-extensions) to work.
```yaml
php_extensions_default:
php8.4-bcmath: "{{ apt_package_state }}"
php8.4-example: "{{ apt_package_state }}"
# etc.
```
================================================
FILE: trellis/cron-jobs.md
================================================
---
date_modified: 2026-03-10 12:00
date_published: 2026-03-10 12:00
description: How Trellis manages WordPress cron jobs with system cron, including configuration options for single sites and Multisite, plus adding custom cron jobs via deploy hooks.
title: Cron Jobs in Trellis
authors:
- ben
- chrillep
---
# Cron Jobs in Trellis
Trellis sets `DISABLE_WP_CRON` to `true` by default and replaces WP-Cron with a system cron job. This is more reliable than WP-Cron, which depends on site traffic to trigger scheduled tasks.
## How it works
When you provision a server, Trellis automatically:
1. Sets `DISABLE_WP_CRON` to `true` in your site's `.env` file
2. Creates a system cron job that runs `wp cron event run --due-now` on a schedule
This means WordPress scheduled events (like publishing scheduled posts, checking for updates, and running plugin tasks) are handled by the server's cron daemon instead of relying on page visits.
## Configuration
### Cron interval
The default cron interval is every 15 minutes. You can customize it per-site with the `cron_interval` option in `wordpress_sites.yml`:
```yaml
wordpress_sites:
example.com:
cron_interval: '*/5'
```
The value follows standard [cron schedule syntax](https://en.wikipedia.org/wiki/Cron#Overview) for the minute field.
### Disabling system cron
If you want to use WP-Cron instead of the system cron, set `disable_wp_cron` to `false` in your site's `env` configuration:
```yaml
wordpress_sites:
example.com:
env:
disable_wp_cron: false
```
This will re-enable WP-Cron and remove the system cron job on the next provision.
### Multisite
For Multisite installations, Trellis creates a separate cron job that iterates over all sites in the network. The default interval for Multisite is every 30 minutes, configurable with `cron_interval_multisite`:
```yaml
wordpress_sites:
example.com:
multisite:
enabled: true
cron_interval_multisite: '*/15'
```
You can disable the Multisite cron job while keeping `disable_wp_cron` enabled by setting `multisite.cron` to `false`:
```yaml
wordpress_sites:
example.com:
multisite:
enabled: true
cron: false
```
## Adding custom cron jobs
Trellis doesn't have a built-in configuration option for custom cron jobs, but you can add them using Ansible's [cron module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/cron_module.html) with [deploy hooks](/trellis/docs/deployments/#hooks).
### During provisioning
To add a custom cron job during provisioning, create a task file and include it via a hook or custom role. For example, create `roles/custom-crons/tasks/main.yml`:
```yaml
- name: Schedule database backup
cron:
name: "{{ item.key }} database backup"
minute: '0'
hour: '3'
user: "{{ web_user }}"
job: "cd {{ www_root }}/{{ item.key }}/current && wp db export /tmp/{{ item.key }}-backup.sql > /dev/null 2>&1"
cron_file: "{{ item.key | replace('.', '_') }}-db-backup"
loop: "{{ wordpress_sites | dict2items }}"
loop_control:
label: "{{ item.key }}"
```
### During deploy
If you only use the deploy portion of Trellis (e.g., you don't have root access to run full provisioning), you can manage cron jobs through deploy hooks. This is useful for keeping cron job definitions in sync with your deployed code.
Create a task file at `deploy-hooks/cron-jobs.yml`:
```yaml
- name: Schedule custom task
cron:
name: "{{ site }} custom task"
minute: '0'
hour: '*/6'
user: "{{ web_user }}"
job: "cd {{ deploy_helper.current_path }} && wp eval-file scripts/custom-task.php > /dev/null 2>&1"
```
::: tip
Always set `user` explicitly. Without it, the cron job is added to the crontab of whichever user Ansible connects as (often the `deploy` user), which can cause WP-CLI permission issues. Use `{{ web_user }}` to match how Trellis manages its own WordPress cron jobs.
:::
Then add the hook to your configuration in `group_vars/all/deploy-hooks.yml` (or `group_vars/all/main.yml`):
```yaml
deploy_finalize_after:
- "{{ playbook_dir }}/roles/deploy/hooks/finalize-after.yml"
- "{{ playbook_dir }}/deploy-hooks/cron-jobs.yml"
```
::: warning
When overriding `deploy_finalize_after`, make sure to keep the default Trellis hook (`roles/deploy/hooks/finalize-after.yml`) as the first item in the list. Omitting it will skip the default tasks that refresh WordPress settings and reload php-fpm.
:::
### Cron module options
The Ansible `cron` module supports the following scheduling options:
| Option | Description | Default |
|-----------|--------------------------------------|---------|
| `minute` | Minute (0-59, `*`, `*/N`) | `*` |
| `hour` | Hour (0-23, `*`, `*/N`) | `*` |
| `day` | Day of month (1-31, `*`, `*/N`) | `*` |
| `month` | Month (1-12, `*`, `*/N`) | `*` |
| `weekday` | Day of week (0-6, Sunday=0, `*`) | `*` |
| `job` | The command to run | |
| `name` | Description of the cron entry | |
| `user` | The user the cron job runs as | |
| `state` | `present` or `absent` | `present` |
## Verifying cron jobs
Trellis stores its provisioned cron jobs as files in `/etc/cron.d/`. To list them:
```bash
ls /etc/cron.d/wordpress-*
```
To view a specific cron file:
```bash
cat /etc/cron.d/wordpress-example_com
```
Cron jobs added without `cron_file` (including deploy hook examples that use `user`) are stored in the specified user's crontab instead. To view those:
```bash
sudo crontab -u web -l
```
## Related documentation
- [Managing WP-Cron in Bedrock](/bedrock/docs/wp-cron/)
- [Deployments and deploy hooks](/trellis/docs/deployments/)
================================================
FILE: trellis/database-access.md
================================================
---
date_modified: 2023-06-06 15:00
date_published: 2016-11-27 11:34
description: Access Trellis WordPress databases using GUI tools like Sequel Pro or TablePlus. Configure SSH tunnels for secure connections without phpMyAdmin.
title: WordPress Database Access with Trellis
authors:
- ben
- huubl
- Log1x
- MWDelaney
- mZoo
- swalkinshaw
- TangRufus
---
# WordPress Database Access with Trellis
Accessing your databases with client software like [Sequel Pro](https://www.sequelpro.com/), [Sequel Ace](https://sequel-ace.com/) and [TablePlus](http://tableplus.com/) is straight forward with [`trellis-cli`](https://github.com/roots/trellis-cli). Run the following from any directory within your project:
## Sequel Pro (or Sequel Ace):
```shell
$ trellis db open --app=sequel-pro production example.com
```
## TablePlus
```shell
$ trellis db open --app=tableplus production example.com
```
::: tip SSH Password?
Because Trellis provisions remote environments to use [SSH keys](/trellis/docs/ssh-keys/) rather than passwords, the password field or prompt is left blank.
:::
## Connection details
To access database passwords, run:
```shell
$ trellis vault view | grep "db_password"
```
### Remote servers
* Connection type: SSH
* MySQL host: `127.0.0.1`
* Username: `example_com`
* Password: `example_dbpassword`
* SSH Host: `example.com`
* SSH User: `web`
================================================
FILE: trellis/debugging-php.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2016-11-07 16:30
description: Debug WordPress PHP code with Trellis's built-in Xdebug support in development. Configure your IDE for step debugging, breakpoints, and inspection.
title: Debugging PHP in Trellis with Xdebug
authors:
- ben
- Log1x
- swalkinshaw
---
# Debugging PHP in Trellis with Xdebug
There are many ways to go about debugging a PHP application, and one of the most effective ways is using a debugger. One of the most powerful tools in the PHP community to go about doing this is [Xdebug](https://en.wikipedia.org/wiki/Xdebug).
## What is Xdebug?
Xdebug enables you to do the following:
- debug and profile PHP applications and scripts
- interactively debug running code
- measure the performance of your application
- see the state of your application at a point in time
Xdebug gives you all sorts of visibility into the internals of your application, like what variable values are at a certain point in time, what functions are taking a long time to execute, as well as what the return values of functions are. It gives you the ability to step through the execution of your application function by function, or even line by line if you really want to.
## Installation
Trellis is configured with Xdebug and ready to rock out of the box in development. All you have to do is select a compatible debugger. Xdebug is designed to be used with a DBGP-compatible debugger in order to interface with Xdebug on your site. [PHPStorm](https://www.jetbrains.com/phpstorm/) comes with support for this out of the box and [Sublime Text](https://github.com/martomo/SublimeTextXdebug), [Visual Studio Code](https://github.com/felixfbecker/vscode-php-debug), and [Vim](https://github.com/vim-vdebug/vdebug) have plugins available.
## Configuration
The variables used in the `roles/xdebug` role directly correlate to the configuration options used by Xdebug itself. For example, Xdebug has the option `xdebug.scream` to disable PHP error suppression using the `@` symbol before function calls. The corresponding Trellis variable would be `xdebug_scream`.
You can see all the available configuration options in `roles/xdebug/defaults/main.yml` and read about how they're used in Xdebug on their [documentation page](https://xdebug.org/docs/all_settings). Trellis ships with pretty sane defaults, but this gives you the option to override if necessary. To change those variables, it's recommended you set them in `group_vars//php.yml`.
## Using Xdebug in production
While we default to installing Xdebug in development, installing it in any other environment is "opt-in." **It is not recommended to use Xdebug in production**, but it _can_ be extremely useful in debugging production-like environments.
For example, if there's an issue you're encountering in Production, but cannot reproduce in Development, it's likely the problem lies with something specific to your VPS provider.
Duplicating your production environment and sanitizing the data using something like [WP Hammer](https://github.com/10up/wp-hammer) will allow you to debug your production environmment without affecting it. This is where trellis-cli's `xdebug-tunnel` commands comes in.
### `trellis xdebug-tunnel`: Xdebug + SSH tunnels
Xdebug gives a lot of visibility into your application that you do not want to give to anyone. Because of this, you want to restrict access to who is allowed to initiate a debugging session.
The way we go about doing that is by creating a remote SSH tunnel from the VPS to your local computer. `trellis xdebug-tunnel` makes it trivial to set up the connection by installing Xdebug if it is not already on the remote host as well as establishing the SSH tunnel between your server and your computer.
By default, Trellis configures Xdebug to look for a debugging session on the server's localhost port 9000:
```yaml
# roles/xdebug/defaults/main.yml
xdebug_remote_host: localhost
xdebug_remote_port: 9000
```
Because your debugger is located on your computer and not the server, Xdebug would attempt to communicate with `localhost:9000` unsuccessfully and proceed with the request as normal. Using `trellis xdebug-tunnel open` creates a tunnel from the server's `localhost:9000` to your computer's `localhost:9000`, bridging the gap and allowing the two to communicate.
### Establishing the tunnel
First, let's look at the command we'll be using to create the tunnel:
```shell
$ trellis xdebug-tunnel
```
The argument `action` can be `open` or `close` and `host` is the hostname, IP, or inventory alias in your `hosts/` file.
Provided this hosts file:
```plaintext
# let's pretend hosts/staging
some_inventory_hostname ansible_ssh_host=12.34.56.78
[staging]
some_inventory_hostname
[web]
some_inventory_hostname
```
You would execute:
```shell
$ trellis xdebug-tunnel open some_inventory_hostname
```
This script runs the `xdebug-tunnel.yml` playbook with the necessary variables to install Xdebug on the environment as well as establish the tunnel.
To close the tunnel, as well as disable Xdebug, run:
```shell
$ trellis xdebug-tunnel close some_inventory_hostname
```
This will remove the `/etc/php/8.0/fpm/conf.d/20-xdebug.ini` symlink, effectively disabling it for that environment while leaving xdebug installed. It also closes the SSH connection.
If you don't use inventory aliases in your host files, you can also use an ip address directly instead of the alias. For example, if your hosts file looks like this:
```plaintext
[staging]
12.34.56.78
[web]
12.34.56.78
```
You can do this:
```shell
$ trellis xdebug-tunnel open 12.34.56.78
```
You must specify the `host` exactly the same when opening and closing the tunnel. It would cause an error to open the tunnel with a `host` of `some_inventory_hostname` then close with a host of `12.34.56.78`. This is because the tunnel socket is created using the host parameter you pass:
```shell
/tmp/trellis-xdebug-{{ provided host }}
```
================================================
FILE: trellis/deploy-to-digitalocean.md
================================================
---
date_modified: 2026-04-03 10:00
date_published: 2019-01-07 10:05
description: Deploy Trellis WordPress sites to DigitalOcean servers. Create servers, configure settings, and automate WordPress deployment to DigitalOcean.
title: Deploying Trellis to DigitalOcean
authors:
- ben
---
# Deploying Trellis to DigitalOcean
[DigitalOcean](https://www.digitalocean.com/?refcode=09f11cfb5bac) is a cloud infrastructure provider that offers virtual servers (droplets) that can handle most normal WordPress sites when provisioned with Trellis.
To provision a server, Trellis requires a server running a bare/stock version of Ubuntu 24.04 LTS.
::: tip
ℹ️ If you [signup for DigitalOcean](https://www.digitalocean.com/?refcode=09f11cfb5bac) through the Roots referral link you will receive a free $200 in credit for 2 months, and you help cover the costs of our hosting.
:::
## Creating a new server
Trellis CLI comes with a `trellis server create` command to automatically create and provision a server for a specified environment:
```shell
$ trellis server create production
```
::: warning
This command requires a [DigitalOcean personal access token](https://cloud.digitalocean.com/account/api/tokens/new).
:::
If the `DIGITALOCEAN_ACCESS_TOKEN` environment variable is not set, the command will prompt for one.
DigitalOcean is the default provider. You can also set it explicitly with the `--provider` flag or in your `trellis.cli.yml`:
```yaml
server:
provider: digitalocean
```
### Quick start (region and size will be prompted)
```shell
$ trellis server create production
```

The remote server playbook will run and provision your server with PHP, Nginx, and everything else included in Trellis.
### Additional options
The command help file can be accessed by passing the `--help` flag:
```shell
$ trellis server create --help
```
trellis server create --help
```plaintext
Usage: trellis server create [options] ENVIRONMENT
Creates a server on a cloud provider for the environment specified.
Only remote servers (for staging and production) are currently supported.
This command requires a DigitalOcean personal access token.
Link: https://cloud.digitalocean.com/account/api/tokens/new
If the DIGITALOCEAN_ACCESS_TOKEN environment variable is not set, the command
will prompt for one.
Create a production server (region and size will be prompted):
$ trellis server create production
Create a 1gb server in the nyc3 region:
$ trellis server create --region=nyc3 --size=s-1vcpu-1gb production
Create a server but skip provisioning:
$ trellis server create --skip-provision production
Arguments:
ENVIRONMENT Name of environment (ie: production)
Options:
--provider Cloud provider (digitalocean, hetzner)
--region Region to create the server in
--image (default: ubuntu-24-04-x64) Server image (ie: Linux distribution)
--size Server size/type
--skip-provision Skip provision after server is created
--ssh-key Path to SSH public key to be added on the server
-h, --help show this help
```
## Changes made after running the command
After creating a new server, your local project will have a modified hosts file for the environment that you provisioned:
```diff
[production]
-your_server_hostname
+159.89.191.207
[web]
-your_server_hostname
+159.89.191.207
```
## Deploying
Once your server is provisioned you’ll want to perform the first deploy. If you try to visit your site before deploying you’ll see a server 500 error.
```shell
$ trellis deploy production
```
After the first deploy is done, you can now either install WordPress by visiting the site or even import an existing database.
================================================
FILE: trellis/deploy-to-hetzner-cloud.md
================================================
---
date_modified: 2026-04-03 10:00
date_published: 2026-04-03 10:00
description: Deploy Trellis WordPress sites to Hetzner Cloud servers. Create servers, configure settings, and automate WordPress deployment to Hetzner Cloud.
title: Deploying Trellis to Hetzner Cloud
authors:
- ben
---
# Deploying Trellis to Hetzner Cloud
[Hetzner Cloud](https://hetzner.cloud/?ref=V6DnI7GDHM4N) is a cloud infrastructure provider offering virtual servers with competitive pricing that can handle most normal WordPress sites when provisioned with Trellis.
::: tip
ℹ️ Sign up for [Hetzner Cloud](https://hetzner.cloud/?ref=V6DnI7GDHM4N) through the Roots referral link to receive $20 in cloud credits.
:::
## Creating a new server
Trellis CLI comes with a `trellis server create` command to automatically create and provision a server for a specified environment:
```shell
$ trellis server create --provider=hetzner production
```
::: warning
This command requires a [Hetzner API token](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/).
:::
If the `HCLOUD_TOKEN` environment variable is not set, the command will prompt for one.
To avoid passing `--provider` every time, set Hetzner as your default provider in `trellis.cli.yml`:
```yaml
server:
provider: hetzner
```
Then you can simply run:
```shell
$ trellis server create production
```
### Quick start (region and size will be prompted)
```shell
$ trellis server create production
```
The remote server playbook will run and provision your server with PHP, Nginx, and everything else included in Trellis.
### Additional options
The command help file can be accessed by passing the `--help` flag:
```shell
$ trellis server create --help
```
## Changes made after running the command
After creating a new server, your local project will have a modified hosts file for the environment that you provisioned:
```diff
[production]
-your_server_hostname
+49.13.25.100
[web]
-your_server_hostname
+49.13.25.100
```
## Deploying
Once your server is provisioned you'll want to perform the first deploy. If you try to visit your site before deploying you'll see a server 500 error.
```shell
$ trellis deploy production
```
After the first deploy is done, you can now either install WordPress by visiting the site or even import an existing database.
================================================
FILE: trellis/deploy-with-github-actions.md
================================================
---
date_modified: 2025-11-16 11:00
date_published: 2023-04-05 11:00
description: Deploy Trellis WordPress sites with GitHub Actions using `setup-trellis-cli`.
title: Deploying Trellis with GitHub Actions
authors:
- ben
- swalkinshaw
---
# Deploying Trellis with GitHub Actions
The [`roots/setup-trellis-cli` GitHub Action](https://github.com/roots/setup-trellis-cli) can be used for setting up continuous deploys for Trellis based WordPress sites.
::: warning
This guide requires that you already have a repo on GitHub with your WordPress site along with the `trellis` directory committed to it
:::
## Setup the GitHub action
### Add the Ansible Vault password
Add a GitHub secret for `ANSIBLE_VAULT_PASSWORD` that contains the value of your `.vault_pass` file. Either manually add it at **Settings > Secrets and variables > Actions**, or use the GitHub CLI to automatically add it:
```bash
$ gh secret set ANSIBLE_VAULT_PASSWORD -b $(cat trellis/.vault_pass)
```
### Generate a SSH key
The GitHub Action runner needs to SSH into your remote Trellis server. The easiest way to get setup is by using Trellis CLI:
```shell
$ trellis key generate
```
After running this command you'll have:
* A new file in `trellis/public_keys` — make sure to commit this addition
* A deploy key added to your repo automatically (**Settings > Deploy keys**)
* Two new repository secrets added to your repo automatically: `TRELLIS_DEPLOY_SSH_KNOWN_HOSTS` and `TRELLIS_DEPLOY_SSH_PRIVATE_KEY`
Further information can be found on the [`roots/setup-trellis-cli` README](https://github.com/roots/setup-trellis-cli#ssh-known-hosts).
## Add a workflow for deploying
The setup-trellis-cli repo contains some example workflows including:
* [Basic deploy](https://github.com/roots/setup-trellis-cli/blob/main/examples/basic.yml)
* [Deploy with a Sage-based theme](https://github.com/roots/setup-trellis-cli/blob/main/examples/sage.yml)
These examples are configured to deploy a Trellis site to the production environment when the `main` branch is pushed to. Copy the relevant example to your repo at `.github/workflows/deploy.yml`.
If you site uses a Sage-based theme, make sure to modify the `cache-dependency-path` to point to the `package-lock.json` file in your theme directory.
================================================
FILE: trellis/deployments.md
================================================
---
date_modified: 2025-02-21 17:30
date_published: 2015-09-07 20:44
description: Trellis provides zero-downtime WordPress deployment with atomic deploys. Customize each deployment step with hooks for builds, migrations, and cleanup tasks.
title: WordPress Deployments with Trellis
authors:
- ben
- dalepgrant
- dougjq
- Log1x
- MWDelaney
- swalkinshaw
- TangRufus
---
# WordPress Deployments with Trellis
Trellis allows zero-downtime WordPress deployment out of the box with a little configuration. Hooks let you customize what happens at each step of the deploy process.
Trellis deploys your site from a Git repository. In your `wordpress_sites.yml` file, found in the `group_vars/` directory, make sure the `repo` and `branch` keys are set correctly:
- `repo` - Git URL of your Bedrock-based WordPress project (in SSH format: `git@github.com:org/repo-name.git`)
- `branch` - Git branch to deploy (default: `master`)
```diff
wordpress_sites:
example.com:
...
- repo: git@github.com:example/example.com.git
+ repo: git@github.com:org/repo-name.git
- branch: master
+ branch: main
```
[Read more about WordPress Sites in Trellis](/trellis/docs/wordpress-sites/)
::: tip
Using DigitalOcean? Read our guide on [deploying Trellis to DigitalOcean](https://roots.io/trellis/docs/deploy-to-digitalocean/)
:::
## Deploying
Run the following from any directory within your project:
```shell
$ trellis deploy
```
::: warning Note
**Trellis does not automatically "install" WordPress on remote servers**.
It's normal and expected to see the WordPress install screen the first time you deploy. It's up to you to either import an existing database or install a fresh site.
:::
::: warning Note
**About zero-downtime deploys**.
Database migrations to a new schema are not included as part of a Trellis deploy. This means that if you need to migrate your database (for example, to account for new plugins), you may need to expect downtime depending on how you manage your database. Modify your deploy process to account for database migrations as with any other framework.
:::
## Rollbacks
Run the following from any directory within your project:
```shell
$ trellis rollback
```
Manually specify a different release using `--release=12345678901234` as such:
```shell
$ trellis rollback --release=12345678901234
```
By default Trellis stores five previous releases, not including the current release. See `deploy_keep_releases` in [Options - Remote Servers](wordpress-sites.md) to change this setting.
## Hooks
Trellis deploys let you customize what happens at each step of the atomic deployment process. A single deploy has the following steps in order:
1. `initialize` - creates the site directory structure (or ensures it exists)
2. `update` - clones the Git repo onto the remote server
3. `prepare` - prepares the files/directories in the new release path (such as moving the repo subtree if one exists)
4. `build` - builds the new release by copying templates, files, and folders
5. `share` - symlinks shared files/folders to new release
6. `finalize` - finalizes the deploy by updating the `current` symlink (atomic deployments)
Each step has a `before` and `after` hook. The hooks are variables that you can define with a list of custom task files to be included and run when the hook fires.
The hook variables available are:
- `deploy_before`
- `deploy_initialize_before`
- `deploy_initialize_after`
- `deploy_update_before`
- `deploy_update_after`
- `deploy_prepare_before`
- `deploy_prepare_after`
- `deploy_build_before`
- `deploy_build_after`
- `deploy_share_before`
- `deploy_share_after`
- `deploy_finalize_before`
- `deploy_finalize_after`
- `deploy_after`
### Default hooks
By default, Trellis defines and uses three hooks:
- `deploy_build_after` runs `composer install`.
- `deploy_finalize_before` checks the WordPress installation.
- `deploy_finalize_after` refreshes WordPress settings and reloads php-fpm.
The default deploy hooks are defined in `roles/deploy/defaults/main.yml`:
```yaml
deploy_build_before:
- '{{ playbook_dir }}/deploy-hooks/build-before.yml'
deploy_build_after:
- '{{ playbook_dir }}/roles/deploy/hooks/build-after.yml'
# - "{{ playbook_dir }}/deploy-hooks/sites/{{ site }}-build-after.yml"
deploy_finalize_before:
- '{{ playbook_dir }}/roles/deploy/hooks/finalize-before.yml'
deploy_finalize_after:
- '{{ playbook_dir }}/roles/deploy/hooks/finalize-after.yml'
```
The `deploy_build_before` definition and the commented path under `deploy_build_after` offer examples of using hooks for custom tasks, as described below.
### Custom tasks
To use a deploy hook, define or override the hook variable somewhere within your `group_vars` directory, such as in `group_vars/all/main.yml`. If you end up defining many hooks, you may want to create a new file such as `group_vars/all/deploy-hooks.yml`.
Each deploy hook variable is a list of task files to be included and run when the hook fires. We suggest keeping your hooked task files in a top level `deploy-hooks` folder. Here are some example hook variable definitions:
```yaml
# Defining a hook that Trellis does not already use by default
deploy_before:
- '{{ playbook_dir }}/deploy-hooks/deploy-before.yml'
# Overriding a hook that Trellis already uses by default
deploy_build_after:
- '{{ playbook_dir }}/roles/deploy/hooks/build-after.yml'
- '{{ playbook_dir }}/deploy-hooks/build-after.yml'
- '{{ playbook_dir }}/deploy-hooks/sites/{{ site }}-build-after.yml'
```
The second example above demonstrates overriding the `deploy_build_after` hook that Trellis already uses by default. The first include file in this hook's list is `roles/deploy/hooks/build-after.yml`, which is the task file Trellis usually executes. If you omit a hook's default file when overriding an existing hook variable, the default file's tasks will no longer execute.
The second include file in the `deploy_build_after` example above, `deploy-hooks/build-after.yml`, is an example of adding a custom task file that would run on every deploy, regardless the site being deployed. The third include file, deploy-hooks/sites/{{ site }}-build-after.yml, demonstrates how you could use a `{{ site }}` variable to include a file based on the name of the site being deployed, e.g., `example.com-build-after.yml`.
================================================
FILE: trellis/existing-projects.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2018-08-23 09:56
title: Adding Trellis to Existing WordPress Projects
description: Get started on existing Trellis projects. Clone the repository, install dependencies, set up Ansible Vault, and provision your local development environment.
authors:
- ben
- Log1x
- MWDelaney
- TangRufus
---
# Adding Trellis to Existing WordPress Projects
The majority of the Trellis documentation focuses on setting up new projects. If you are collaborating on, or taking over an existing project, the process is a little different.
::: tip Note
This documentation presumes your project follows the [Roots Example Project](https://github.com/roots/roots-example-project.com) recommendations.
:::
## Gather Information
To work on an existing Trellis project you need the following:
- Git repository access
- The Ansible Vault password
- Permissions for provisioning and deployment
- Your site's development URL
- A database dump and a copy of the project's `/uploads` folder
### Git Repository Access
Roots recommends that Trellis projects be kept in private Git repositories. Make sure you have permission and access to the project's Git repository and any dependent plugin or theme repositories.
### Ansible Vault password
Trellis stores passwords and other sensitive data in [encrypted vault files](vault.md). Retrieve the project's vault password from someone who already works on the project.
### Permission for provisioning and deployment
If you need to [provision this project's remote servers](remote-server-setup.md) or [deploy the project](deployments.md) to staging or production, add your SSH keys to the necessary remote servers either by accessing the server directly, or by having someone who already has access [add your SSH keys to the Trellis configuration](ssh-keys.md) and re-provision the server.
### Your site's development URL
Review the project's `trellis/group_vars/development/wordpress_sites.yml` and note its URL:
```yaml
wordpress_sites:
example.com:
site_hosts:
- canonical: example.test # <-- this is the development URL
```
## Clone Your Project
```shell
$ git clone git@github.com:YourOrganization/example.com.git
```
## Ansible Vault
Determine whether your vault files are encrypted by looking at the `vault.yml` files in `trellis/group_vars/`
```yaml
$ANSIBLE_VAULT;1.1;AES256
343163646662643438323831343332626234333233386666333162383265663
3132306538383762336332376165383530633838643937320a6363343238643
363065366664316364646561613163653866623566303235666537343437643
6638363265383831390a6631663239373833636133623333666363643166383
6237663637353638653266616562616535623465636265316231613331 etc.
```
If any of the `vault.yml` files look like the example above, follow the [vault instructions](vault.md) to configure your Ansible Vault and vault password.
## Create Your Development VM
Run the following from any directory within your project:
```shell
$ trellis up
```
Confirm you can access the development site at the development URL noted earlier.
## Import the database
Retrieve an export of the current project’s database.
::: tip Note
For easy access during the import process, place the database export in your local project’s `site` directory.
:::
Run the following from any directory within your project:
```shell
$ trellis ssh development
```
Navigate to the web root:
```shell
$ cd /srv/www/example.com/current
```
Import the database with wp-cli:
```shell
$ wp db import example.com.sql
```
If the export is not from another development environment, search-and-replace the site's URL with wp-cli:
```shell
$ wp search-replace http://example.com http://example.test
```
## Import the Uploads
Retrieve a copy of the current project’s `uploads` directory and place it in your local project's `site/web/app` directory.
================================================
FILE: trellis/fastcgi-caching.md
================================================
---
date_modified: 2025-02-27 15:48
date_published: 2015-09-06 07:42
description: Trellis offers built-in FastCGI caching with Nginx microcaching. No WordPress plugin required, with configurable cache skipping for eCommerce pages.
title: FastCGI Caching for WordPress in Trellis
authors:
- ben
- catgofire
- Log1x
- swalkinshaw
- vdrnn
---
# FastCGI Caching for WordPress in Trellis
You can enable caching for your site by changing the cache settings under each site key. Using caching provides substantial speed improvement once pages are cached. The full settings looks like this:
```yaml
cache:
enabled: false
duration: 30s
skip_cache_uri: /wp-admin/|/wp-json/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml
skip_cache_cookie: comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in
```
The `duration` parameter control how long your pages will stay in the cache. You should generally keep this value low (the default is 30 seconds), unless your content doesn't change frequently. Lowering the duration to `1s` will make the cache more like a DDOS protection; meaning that if you have a sudden spike of traffic, only one request will hit the back-end per second instead of the full load. The whole setup is "micro-cache" oriented, so there is no means of flushing the cache.
The `skip_cache_uri` is a regex that will be used to tell Nginx **not** cache pages matching it. **Use it if you have sections of your site that you don't want cached (like shopping carts)**. Override the global `nginx_skip_cache_uri` in `group_vars/all/main.yml` or override `skip_cache_uri` under `cache` to vary it per [WordPress site](wordpress-sites.md). The default value is shown above.
The `skip_cache_cookie` is a regex that will disable the cache when a cookie match it. Useful for disabling the cache for certain users.
Already cached content will continue being served if your back-end (PHP-FPM) goes down.
## Cache-Control Headers
As of [Trellis v1.24.0](https://github.com/roots/trellis/releases/tag/v1.24.0), Nginx now respects Cache-Control headers sent by WordPress and plugins. This means applications can control their own caching behavior by setting appropriate Cache-Control headers in their HTTP responses.
For e-commerce sites using WooCommerce, Easy Digital Downloads, or similar plugins that properly set Cache-Control headers on dynamic pages (like cart, checkout, and account pages), this can simplify cache configuration by reducing the need for extensive `skip_cache_uri` and `skip_cache_cookie` settings.
## Example cache configurations
### WooCommerce
Disable the cache for `/store/`, `/cart/`, `/my-account/`, `/checkout/`, `/addons/`, and when items are in the cart:
```yaml
cache:
enabled: true
skip_cache_uri: /wp-admin/|/wp-json/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml|/store.*|/cart.*|/my-account.*|/checkout.*|/addons.*
skip_cache_cookie: comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_
```
Alternatively, if you're using a recent version of WooCommerce that sets appropriate Cache-Control headers, you may be able to simplify this configuration by relying on those headers instead of extensive URI and cookie patterns.
### Easy Digital Downloads
Disable the cache for `/checkout/` and when items are in the cart:
```yaml
cache:
enabled: true
skip_cache_uri: /wp-admin/|/wp-json/|/checkout/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml
skip_cache_cookie: comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|edd_items_in_cart
```
Like with WooCommerce, if your version of Easy Digital Downloads sets Cache-Control headers correctly, you may be able to simplify this configuration.
================================================
FILE: trellis/install-wordpress-language-files.md
================================================
---
date_modified: 2023-09-27 14:05
date_published: 2021-09-08 00:29
description: Configure WordPress language file installation in Trellis for multi-language sites. Automate translation downloads for core, plugins, and themes.
title: Installing WordPress Language Files in Trellis
authors:
- strarsis
- hooley
---
# Installing WordPress Language Files in Trellis
## Current state of language management
### Locking in language versions?
With Composer (Bedrock site) the plugin versions are already locked-in. So it naturally makes sense to also lock-in the plugin languages.
The [Composer WordPress language packs project](https://wp-languages.github.io/) by Koodimonni offers composer packages for plugin languages.
However, providing all languages for each plugin version results in a very large amount of packages.
Because of this practical issue, the site offers language packages for only a subset of plugins, so chances are, that languages for some plugins your site is using are not available there (e.g. [Redirection plugin](https://wordpress.org/plugins/redirection/))
### Using custom Composer installers
There are some custom Composer installers which simply download the latest languages for core, plugins and themes. However, this approach doesn't allow any locking-in of language versions. One may end up with different translations for same site release.
- [wplang](https://github.com/bjornjohansen/wplang)
- [Composer Auto Language Updates](https://github.com/Angrycreative/composer-plugin-language-update)
## Using `wp language` command on Trellis deploys
At the time of writing there is no way yet for locking-in languages of core, plugins and themes (for all plugins and themes), so we are stuck with installing the latest language available.
The best approach is using the official mechanisms, which would be the `wp language` subcommand.
### Setup deploy hooks
We use the [`finalize-after` deploy hook](/trellis/docs/deployments/#hooks) for installing, activating and updating the core/plugins/themes languages of a site for the languages `en_GB`, `de_DE_formal` and `de_DE`:
`deploy-hooks/sites/example.com-finalize-after.yml`:
```yaml
# Install + activate languages
- name: Install core languages en_GB de_DE
command: wp language core install en_GB de_DE
args:
chdir: "{{ deploy_helper.current_path }}"
- name: Install (and activate) core language de_DE_formal
command: wp language core install de_DE_formal --activate
args:
chdir: "{{ deploy_helper.current_path }}"
- name: Install plugins languages en_GB de_DE de_DE_formal
command: wp language plugin install --all en_GB de_DE de_DE_formal
args:
chdir: "{{ deploy_helper.current_path }}"
- name: Install themes languages en_GB de_DE de_DE_formal
command: wp language theme install --all en_GB de_DE de_DE_formal
args:
chdir: "{{ deploy_helper.current_path }}"
# Update installed languages
- name: Update installed core languages
command: wp language core update
args:
chdir: "{{ deploy_helper.current_path }}"
- name: Update plugins languages
command: wp language plugin --all update
args:
chdir: "{{ deploy_helper.current_path }}"
- name: Install themes languages
command: wp language theme --all update
args:
chdir: "{{ deploy_helper.current_path }}"
```
(All these `wp` commands are idempotent, they only install/update when it is required.)
In the first part the required languages are installed for core, plugins and themes.
In the second part all the installed languages of core, plugins and themes are updated.
#### Removing no longer needed languages
Note: If you ever want to remove a language, you can do this here, too.
Ideally the language is removed before updating as this removes an unnecessary update of a language that is removed anyway.
- [`wp language core uninstall ...`](https://developer.wordpress.org/cli/commands/language/core/uninstall/)
- [`wp language plugin uninstall --all ...`](https://developer.wordpress.org/cli/commands/language/plugin/uninstall/)
- [`wp language theme uninstall --all ...`](https://developer.wordpress.org/cli/commands/language/theme/uninstall/)
### Initial deploy (non-setup site)
Many `wp` commands including `wp language` don't work on a WordPress site that is installed but not had been set up yet.
```plaintext
Error: The site you have requested is not installed.
Run `wp core install` to create database tables.
```
For making an initial deploy possible, the WordPress site has to be setup at the beginning of deploy. You may overwrite everything using a transfer script or backup/restore plugin, etc. This is only important here for being able to install/update the languages on initial deploy.
`deploy-hooks/finalize-after.yml`:
```yaml
- name: Install WP (required for installing languages on non-transferred site)
command: wp core {{ project.multisite.enabled | default(false) | ternary('multisite-install', 'install') }}
--allow-root
--url="{{ site_env.wp_home }}"
{% if project.multisite.enabled | default(false) %}
--base="{{ project.multisite.base_path | default('/') }}"
--subdomains="{{ project.multisite.subdomains | default('false') }}"
{% endif %}
--title="{{ project.site_title | default(site) }}"
--admin_user="{{ project.admin_user | default('admin') }}"
--admin_password="{{ vault_wordpress_sites[site].admin_password }}"
--admin_email="{{ project.admin_email }}"
args:
chdir: "{{ deploy_helper.current_path }}"
register: wp_install
changed_when: "'WordPress is already installed.' not in wp_install.stdout and 'The network already exists.' not in wp_install.stdout"
```
### Add deploy hooks
For making trellis actually using these new deploy hooks, they need to be added:
`groups_vars/all/main.yml`:
```yaml
# Deploy hooks
deploy_build_before:
- "{{ playbook_dir }}/deploy-hooks/sites/{{ site }}-build-before.yml" # build + upload theme assets
deploy_build_after:
- "{{ playbook_dir }}/roles/deploy/hooks/build-after.yml" # built-in
deploy_finalize_before:
- "{{ playbook_dir }}/roles/deploy/hooks/finalize-before.yml" # built-in
deploy_finalize_after:
- "{{ playbook_dir }}/roles/deploy/hooks/finalize-after.yml" # built-in
- "{{ playbook_dir }}/deploy-hooks/finalize-after.yml" # finish site setup for installing languages
- "{{ playbook_dir }}/deploy-hooks/sites/{{ site }}-finalize-after.yml" # install + update languages
````
### Improve performance / prevent "translation downtimes"
By default, for each new release the languages would have to be reinstalled, during that time the site can appear partially untranslated (falling back to English by default, or to the language setup by a language fallback plugin).
For preventing this "translation downtime" and for making a language update instead of install possible, the `languages` folder can be added to `project_copy_folders`, so it is copied between releases.
`group_vars/all/main.yml`:
```yaml
project_copy_folders:
- vendor
- web/app/languages # copy languages between releases
```
## Language fallback
By experience, for languages with a formal and non-formal variation, plugins are often only translated for one of these variations. It is possible to fall back at least to the non-formal variation by using a [language fallback plugin](https://wordpress.org/plugins/language-fallback/).
With such a plugin installed and enabled, when a string is not translated in current language, it is looked up from the fallback language first, instead of falling back immediately to English.
================================================
FILE: trellis/installation.md
================================================
---
date_modified: 2026-03-06 13:00
date_published: 2015-10-15 12:20
description: Install Trellis for WordPress projects. Complete setup instructions covering requirements, dependencies, project initialization, and initial configuration.
title: Installing Trellis for WordPress
authors:
- ben
- Log1x
- MWDelaney
- nikitasol
- swalkinshaw
- TangRufus
- MWDelaney
---
# Installing Trellis for WordPress
## What is Trellis?
[Trellis](https://roots.io/trellis/) is a tool to create WordPress web servers and deploy WordPress sites.
Trellis lets you create and manage servers that are production ready, performance optimized and based on best practices that are continuously improved. Trellis is self-hosting done right since you benefit from the community and experience of Roots.
### Why use Trellis?
You’ll get a complete WordPress server [running all the software](#software-installed) you need configured according to the best practices that are fully customizable.
Trellis features
#### Ansible
Trellis is powered by [Ansible](https://docs.ansible.com/projects/ansible/latest/index.html) for configuration management. You don’t have to use brittle and confusing Bash scripts or worry about commands you found to copy and paste.
You get the benefit of Ansible [documentation](https://docs.ansible.com/projects/ansible/latest/user_guide/index.html), its extensive library of [modules and plugins](https://docs.ansible.com/ansible/latest/collections/all_plugins.html), and the community ecosystem of [Galaxy roles](https://galaxy.ansible.com/).
#### Local development
Trellis comes with [Lima](https://lima-vm.io/) support for local development environments that run on isolated virtual machines. This means you don't have to worry about polluting your local OS with software that might break
or conflict with other tools you use.
However, using Lima is optional and you're free to use other local dev tools as well, or even none at all.
#### Customizable
While Trellis gives you everything for a standard WordPress server out of the
box, it's completely customizable as well. This is what makes Trellis different
from managed hosting or even tools like SpinupWP that automatically setup
WordPress servers.
Thanks to Ansible's YAML based configuration, Trellis is "infrastructure as
code" so you can easily see exactly what Trellis installs on your server and
customize if you want.
#### Portable without vendor-lock in
Trellis servers can be run on _any_ hosting platform; traditional dedicated server hosting or cloud platforms. All Trellis needs is a server running a plain Ubuntu operating system.
This means you can easily migrate hosting providers making your infrastructure much more flexible and portable. You can even "disconnect" your server from Trellis if you want and just manage your server manually. Trellis isn't required to keep your server running (but we do recommend it!).
#### Cost effective
Managed WP hosting can make your life easier, but it can also be
extremely expensive and is often overkill for simpler WordPress sites.
Trellis lets you run performant sites on extremely cheap servers ($5-10/month) and even supports running multiple sites on a single server for more efficiency.
#### Community backed
Since Trellis is open-source, we get the leverage of Roots and our community to continuously improve the defaults over time. We are constantly learning better settings and defaults for WordPress servers, and then we apply them to Trellis.
#### Development and production parity
Unlike many other solutions for WordPress server hosting, Trellis aims to have [parity between your development and production environments](https://roots.io/twelve-factor-10-dev-prod-parity/). Trellis comes setup to run locally with Lima so you can test your WordPress sites with full confidence that they'll work once you deploy to production.
#### CLI
Trellis has its own [CLI](cli.md) that makes managing your local and remote servers much easier. It also enables powerful CI/CD workflows like our [setup-trellis-cli](https://github.com/roots/setup-trellis-cli/) [GitHub action that can be used for continuous deploys](/trellis/docs/deploy-with-github-actions/).
#### Zero-downtime deploys
Trellis has atomic, zero-downtime deploys built-in that are completely
configurable with a powerful hook system. You can deploy and rollback releases
with a single command thanks to trellis-cli too.
### Trellis servers are production-ready
Trellis provisions a base Ubuntu 24.04 server by installing and configuring the following software:
* PHP 8.3+
* Nginx (including HTTP/2, HTTP/3, and optional FastCGI micro-caching)
* MariaDB (a drop-in MySQL replacement)
* SSL support (scores an A+ on the [Qualys SSL Server Test](https://www.ssllabs.com/ssltest/))
* Let's Encrypt for free SSL certificates
* Composer
* WP-CLI
* sSMTP (mail delivery)
* Memcached
* Fail2ban and ferm
In addition to configuring common services like ntp, sshd, etc.
## System requirements
* macOS or Linux
::: warning Windows users
Windows is not supported at this time. A community-maintained fork of trellis-cli with Windows/WSL support is available at [qwatts-dev/trellis-cli](https://github.com/qwatts-dev/trellis-cli).
:::
## Install Trellis CLI
```shell
$ brew install roots/tap/trellis-cli
```
## Create a new project with Trellis
Choose a descriptive project name (and use it in place of the default example.com). We recommend the domain of the site for uniqueness.
```shell
$ trellis new example.com
```
After you've created a project, the folder structure for a Trellis project will look like this:
```plaintext
example.com/ # → Root folder for the project
├── trellis/ # → Your server configuration (a customized install of Trellis)
└── site/ # → A Bedrock-based WordPress site
└── web/
├── app/ # → WordPress content directory (themes, plugins, etc.)
└── wp/ # → WordPress core (don't touch! - managed by Composer)
```
Check out the following files to review the basic site configuration:
* `trellis/group_vars/development/wordpress_sites.yml`
* `trellis/group_vars/production/wordpress_sites.yml`
## Start your development environment
```shell
$ trellis vm up
```
This command will start the Lima environment and provision the server. Once it's done, you can visit your development site at the URL you chose when you ran `trellis new`.
[Read more about Local Development](/trellis/docs/local-development/)
## Configure your environments
Trellis pre-configures most of your site's settings, but you'll need to fill in a few gaps in the [WordPress Sites](/trellis/docs/wordpress-sites/) configuration.
## Encrypt your vault files
You probably want to encrypt your vault files, which hold automatically-generated passwords and other sensitive information. [Read more about Vault](/trellis/docs/vault/)
## Provision your production server
Before deploying to production, you'll need to provision your server. [Read more about provisioning](/trellis/docs/remote-server-setup/)
```shell
$ trellis provision production
```
## Deploy to production
Ready to deploy your site to production? [Read more about deployments](/trellis/docs/deployments/)
```shell
$ trellis deploy production example.com
```
================================================
FILE: trellis/local-development.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-10-15 12:24
description: Trellis uses Lima VM's for local development. Trellis uses Ansible to automatically provision virtual machines running complete WordPress environments.
title: Local WordPress Development with Trellis
authors:
- ben
- fullyint
- IanEdington
- Log1x
- MWDelaney
- swalkinshaw
- TangRufus
---
# Local WordPress Development with Trellis
Trellis has an official integration with Lima for development environments using virtual machines.
Other options include:
* [Laravel Valet](#laravel-valet)
* [Nothing!](#nothing)
## Lima
Trellis integrates with Lima to automatically run the Ansible provisioning. Provisioning in development uses the `dev.yml` Ansible playbook to create a Lima virtual machine running your WordPress site.
Follow these steps to get a development server running:
1. Configure your site(s) based on the [WordPress Sites docs](wordpress-sites.md) and read the [development specific](wordpress-sites.md#development) ones.
2. Make sure you've edited both `group_vars/development/wordpress_sites.yml` and `group_vars/development/vault.yml`.
3. Run `trellis vm start` from anywhere in your project.
Then let Lima and Ansible do their thing. After roughly 5 minutes you'll have a virtual machine running and a WordPress site automatically installed and configured.
To access the VM, run `trellis vm shell`. Sites can be found at `/srv/www/` on the VM.
Note that each WP site you configured is synced between your local machine (the host) and the Lima VM. Any changes made to your host will be synced instantly to the VM. There's no need to manually sync files or deploy to the VM.
Composer and WP-CLI commands need to be run on the virtual machine for any post-provision modifications. Front-end build tools should be run from your host machine and not the Lima VM.
### WordPress installation
Trellis installs WordPress on your first `trellis vm start` with `admin` as the default user. You can override this by defining `admin_user`, as noted in the [WordPress sites options](wordpress-sites.md#options).
### Re-provisioning
Re-provisioning is always assumed to be a safe operation. When you make changes to your Trellis configuration, you should provision the VM again to apply the changes:
Run the following from your project's `trellis` directory:
```shell
$ trellis provision development
```
You can also provision with specific tags to only run the relevant roles:
Run the following from your project's `trellis` directory:
```shell
$ trellis provision --tags=users development
```
### Usage
There's 5 commands for working with VMs:
* `trellis vm start` - create or start a VM
* `trellis vm stop` - stop a running VM
* `trellis vm delete` - delete a stopped VM
* `trellis vm shell` - open a shell/terminal on the VM
* `trellis vm sudoers` - configure sudoers to avoid the need for `sudo`
Run `trellis vm -h` for details on each command.
For default use cases, `trellis vm start` can be run without any customization first. It will create a new virtual machine (using Lima) from a generated config file (`project/trellis/.trellis/lima/config/.yml`). The site's `local_path` will be automatically mounted on the VM and your `/etc/hosts` file will be updated.
Note: run `trellis vm sudoers -h` to make `/etc/hosts` file updates passwordless:
```bash
$ trellis vm sudoers | sudo tee /etc/sudoers.d/trellis
```
Under the hood, those commands wrap equivalent `limactl` features. You can always run `limactl` directly to manage your VMs.
### Configuration:
For the common use case, the default configuration should be all that's needed which is why config options are limited to start with. We will offer more customization over time.
The CLI [config file](cli.md#configuration) (global or project level) supports a new `vm` option. The only useful config option right now is `ubuntu` for setting the Ubuntu version.
Here's an example of specifying 20.04:
```yml
vm:
ubuntu: 20.04
```
Note: this must be changed _before_ creating the VM, otherwise you'll need to delete it first and re-create it.
### Integration details
When you first run `trellis vm start`, the CLI will do the following:
1. Generate a Lima config file (`.trellis/lima/example.com.yml`) based on your Trellis project's development site
2. Create the Lima instance by running `limactl start --name=example.com .trellis/lima/example.com.yml`
3. Generate an Ansible inventory/hosts file for the VM (`.trellis/lima/inventory`)
4. Add your sites hosts to your `/etc/hosts` file
Knowing how the CLI and Lima interact can help with troubleshooting and debugging. Issues with the VM itself are usually related to Lima, and the underlying `limactl` command can be run manually to try and isolate the issue.
Tip: run `limactl list` to see all Lima instances and their statuses.
### Ansible inventory
As detailed above, trellis-cli will automatically generate and manage a VM specific inventory file.
There is no need to manually edit the `hosts/development` file as it won't be used.
Commands like `trellis provision` will automatically detect and specify the Lima inventory file. If you need to run an Ansible command manually against the VM host, the `--inventory-file` flag needs to be set:
```bash
ansible-playbook dev.yml --inventory-file=.trellis/lima/inventory
```
#### SSH port
One reason why the inventory file needs to be generated each time a VM is created or started is due to SSH port forwarding.
Lima will find a free _local_ port and use it to forward to port 22 on the VM.
The inventory file references this forwarded port and Ansible will use that for its SSH connection.
It's recommended to use `trellis vm shell` to SSH to the VM and open a shell/terminal since you don't need to worry about hosts or ports.
To connect manually via SSH, run `limactl show-ssh -f config ` or `limactl show-ssh ` to view the SSH config in various formats.
There is no need to edit your `hosts/development` file unless you were manually using it in a non-standard setup. As mentioned in the [Ansible inventory](#ansible-inventory) section above, trellis-cli generates a separate inventory file.
## Other non-Lima options
While Trellis offers integrated Lima development environments, it is completely optional. There are other local development options as well. Most of these options mean you're using Trellis for your production servers but something else entirely in development which is why it's not recommended.
### Laravel Valet
[Valet](https://laravel.com/docs/10.x/valet) can be used in development if you're
already using it for Laravel projects or want a lighter-weight solution than a
full virtual machine.
However, be warned that doesn't guarantee [development and production parity](https://roots.io/twelve-factor-10-dev-prod-parity/).
Using Valet locally means you aren't using Trellis _at all_ in development.
trellis-cli does offer some basic Valet integration as well. Run `trellis valet`
for more information.
### Nothing
That's right... nothing! You might not care about a local development environment. Or you might only want to use Trellis for deploying to managed servers. Trellis is quite flexible and supports these uses cases as well.
================================================
FILE: trellis/mail.md
================================================
---
date_modified: 2026-04-04 07:00
date_published: 2015-09-06 07:42
description: Trellis uses Mailpit in development to capture outgoing emails. Configure production mail delivery with SMTP settings in the `mail.yml` configuration file.
title: WordPress Mail Configuration in Trellis
authors:
- ben
- fullyint
- jbicha
- Log1x
- MWDelaney
- mZoo
- swalkinshaw
- TangRufus
---
# WordPress Mail Configuration in Trellis
Trellis' mail functionality is separated between development and staging/production since you usually want different behaviour out of them.
## Development
Dealing with emails in development is never fun. The two common solutions are:
- Ignore it and hope it works fine on production
- Set up real SMTP credentials to send emails
Enter [Mailpit](https://github.com/axllent/mailpit). It's a simple tool which captures outgoing email and lets you view them from a web UI. And after that you can optionally "release" them which would actually send the email.

Mailpit is automatically set up in development. You can access it at `http://example.test:8025` (replacing the domain with yours that you set up for the WP site host).
::: warning Note
Mail will be automatically captured but you won't ever see it unless you access the Mailpit UI at the address above.
:::
Another benefit of using Mailpit is that if you are using real SMTP credentials in development, you can ensure you don't accidentally send emails to real email addresses which might exist in your database.
::: warning Note
This is not the case if you have an active WordPress plugin that is configured to send mail. You'll need to disable the mail plugin on development to ensure you don't accidentally send emails to real email addresses. You could also hook into `phpmailer_init` in WordPress for non-production environments to prevent emails from being sent out. Using a service like [Mailtrap](https://mailtrap.io/) is another option.\*\*
:::
Trellis is using the [Mailpit role](https://github.com/roots/ansible-role-mailpit). See that `README` for any extra configuration options although none should be required as Trellis integrates it automatically.
## Remote servers (staging/production)
Outgoing mail is handled by [msmtp](https://marlam.de/msmtp/), a lightweight SMTP client. Trellis uses the [msmtp role](https://github.com/roots/ansible-role-msmtp) to configure it. In order to send external emails, you'll need to configure an SMTP server.
We always suggest using an external email service rather than your own because it's very difficult to set up a proper email server.
Some suggested services:
- [Sendgrid](https://www.twilio.com/en-us/sendgrid)
- [Mailgun](https://www.mailgun.com/)
- [Amazon SES](https://aws.amazon.com/ses/)
All of these offer around 10k+ emails for free per month. Once you have SMTP credentials, configure them in `group_vars/all/mail.yml`.
- `mail_smtp_server`: hostname:port
- `mail_hostname`: hostname for mail delivery
- `mail_user`: username
- `mail_password`: password or "API key" (define in `group_vars/all/vault.yml`)
**Note:** Trellis sends emails through SMTP, which requires a username and password. Some email service providers refer to `mail_password` as an "API key", even though it is not actually used to access the email service provider's API. If you prefer to send email through your email service provider's API (instead of via SMTP), you will need to use a plugin.
### Example
```yaml
mail_smtp_server: smtp.example.com:587
mail_hostname: example.com
mail_user: admin@example.com
mail_password: '{{ vault_mail_password }}' # Define this in group_vars/all/vault.yml
```
If your SMTP settings are invalid, WordPress will return the following error message:
```plaintext
Could not instantiate mail function.
```
To fix this error, update your SMTP settings so that they're valid and then re-provision the remote server.
================================================
FILE: trellis/multiple-sites.md
================================================
---
date_modified: 2026-03-10 12:00
date_published: 2026-03-10 12:00
description: Learn how to structure your Trellis projects when managing multiple WordPress sites, including shared and separate Trellis configurations.
title: Managing Multiple Trellis Sites
authors:
- ben
---
# Managing Multiple Sites
Trellis supports hosting multiple WordPress sites on a single server out of the box. But when managing several sites, how you organize your project directories matters. There are two common approaches.
## Shared Trellis
A single Trellis instance manages multiple Bedrock sites on one server:
```plaintext
projects/ # → Root folder
├── trellis/ # → Single Trellis managing all sites
├── example.com/ # → First Bedrock site
└── another.com/ # → Second Bedrock site
```
Each site is defined in `wordpress_sites.yml` with its own `local_path` pointing to the corresponding directory. See the [WordPress Sites](/trellis/docs/wordpress-sites/) docs for configuration details.
This approach works well when:
- Sites share the same server and server configuration
- You want to minimize infrastructure costs by running multiple sites on one server
- You want a single place to manage provisioning and deploys
## Separate Trellis per site
Each site gets its own Trellis instance with independent server configuration:
```plaintext
example.com/ # → First project
├── trellis/
└── site/
another.com/ # → Second project
├── trellis/
└── site/
```
This approach works well when:
- Sites need different server configurations (PHP versions, Nginx settings, etc.)
- Sites are hosted on different servers or providers
- You want fully independent infrastructure per site
- Different teams manage different sites
The trade-off is more duplication of Trellis configuration, but you get full isolation between projects.
================================================
FILE: trellis/multisite.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-09-06 07:42
description: Set up WordPress multisite on Trellis by configuring Bedrock for multisite installation before provisioning. Supports subdomain and subdirectory networks.
title: WordPress Multisite Setup with Trellis
authors:
- ben
- evance
- iamkarex
- jmslbam
- JulienMelissas
- Log1x
- MWDelaney
- nathanielks
- ned
- Simeon
- swalkinshaw
---
# WordPress Multisite Setup with Trellis
Trellis assumes your WordPress configuration already has multisite set up. If not, ensure the following values are placed somewhere before calling `Config::apply()` in Bedrock's `config/application.php` and **before** provisioning your server:
```php
/* Multisite */
Config::define('WP_ALLOW_MULTISITE', true);
Config::define('MULTISITE', true);
Config::define('SUBDOMAIN_INSTALL', false); // Set to true if using subdomains
Config::define('DOMAIN_CURRENT_SITE', env('DOMAIN_CURRENT_SITE'));
Config::define('PATH_CURRENT_SITE', env('PATH_CURRENT_SITE') ?: '/');
Config::define('SITE_ID_CURRENT_SITE', env('SITE_ID_CURRENT_SITE') ?: 1);
Config::define('BLOG_ID_CURRENT_SITE', env('BLOG_ID_CURRENT_SITE') ?: 1);
```
You'll also need to update the multisite settings under your environment directory (`group_vars//wordpress_sites.yml`):
```yaml
multisite:
enabled: true
subdomains: false # Set to true if you're using a subdomain multisite install
```
You may also want to define the `env` dictionary for more multisite specific settings such as `DOMAIN_CURRENT_SITE` or `PATH_CURRENT_SITE`.
```yaml
env:
domain_current_site: store1.example.com
```
That `env` will be merged in with Trellis' defaults so you don't need to worry about re-defining all of the properties.
Here's an example of a complete entry set up for multisite:
```yaml
# group_vars/production/wordpress_sites.yml
wordpress_sites:
example.com:
site_hosts:
- canonical: example.com
local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
admin_email: admin@example.com
multisite:
enabled: true
subdomains: true
ssl:
enabled: false
cache:
enabled: false
env:
domain_current_site: store1.example.com
```
After provisioning your remote server and deploying your sites, you'll need to install WordPress as a final step in your staging and production environments. SSH into your server as the `web` user with `ssh web@` and in the `/srv/www//current/` directories run the following WP-CLI command to install WordPress:
```shell
$ wp core multisite-install --title="site title" --admin_user="username" --admin_password="password" --admin_email="you@example.com"
```
You may notice that your network's main site URLs contain `/wp/` before the post's or page's pathnames. This is a problem in WP core which occurs when WordPress is located in a subdirectory, as is the case with Bedrock. See issue [Bedrock issue #250](https://github.com/roots/bedrock/issues/250) for details, along with the site URL fix plugin in the [Multisite Fixes](https://github.com/felixarntz/multisite-fixes) plugin collection for a solution.
If you use [Let's Encrypt](ssl.md#let-s-encrypt) as your SSL provider and your multisite install uses subdomains, currently you have to generate individual certificates for each of your subdomains, but this may change soon as Let's Encrypt will begin issuing [wildcard certificates in January of 2018](https://letsencrypt.org/2017/07/06/wildcard-certificates-coming-jan-2018.html). You can generate SSL certificates for your subdomains if you know these subdomains in advance while provisioning your server. To do this, define multiple `canonical` entries under `site_hosts` in your corresponding `wordpress_sites.yml` file like this:
```yaml
site_hosts:
- canonical: example.com
redirects:
- www.example.com
- canonical: subdomain.example.com
redirects:
- www.subdomain.example.com
```
================================================
FILE: trellis/nginx-includes.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2020-02-05 16:24
description: Customize Nginx configuration in Trellis by placing files in `includes.d/` subdirectories. Add custom rules, headers, and configuration per WordPress site.
title: Custom Nginx Includes in Trellis
authors:
- alwaysblank
- ben
- fullyint
- Log1x
- swalkinshaw
- dalepgrant
---
# Custom Nginx Includes in Trellis
The Nginx site conf generated by Trellis is designed to work with a wide range of WordPress installations, but your sites may require customization. For example, perhaps you use custom `rewrite`s to redirect legacy URLs, or maybe you would like to proxy some requests to another server. You may create custom Nginx `include` files or use child templates to extend the Trellis Nginx conf templates.
## `include` files
You may put your Nginx customizations in Ansible/Jinja2 templates, making full use of variables and logic. Store your template files in a new `nginx-includes` directory in your project's `trellis/` directory (e.g., sibling to `server.yml` playbook). If you prefer to name this directory `some-other-name`, you may define `nginx_includes_templates_path: some-other-name` in your `group_vars/all/main.yml`.
Trellis will recurse `nginx-includes` and template any files ending in `*.conf.j2` to the `/etc/nginx/includes.d/` directory on the remote, preserving the subdirectory structure. If you want to edit, add to, or delete your Nginx include files, edit them on the local machine and re-run the playbook.
::: tip
Append `--tags nginx-includes` to your command to run only the relevant portion of the playbook.
:::
### Default
By default in Trellis, a WordPress site's Nginx conf will include any `nginx-includes` files found in a subdirectory named after the site. Only the directories that match sites in your `wordpress_sites.yml` will be templated to the remote by default, with the addition of `all/`.
To illustrate, suppose you have two sites managed by Trellis, defined in `wordpress_sites` as follows:
```yaml
wordpress_sites:
site1: ...
site2: ...
```
You could organize your `nginx-includes` templates in corresponding subdirectories:
```plaintext
trellis/
nginx-includes/
site1/
rewrites.conf.j2
proxy.conf.j2
site2/
rewrites.conf.j2
```
You could also have an "all" directory, which would apply conf to all sites:
```plaintext
trellis/
nginx-includes/
all/
rewrites.conf.j2
```
The above directory structure would be templated to the remote server as follows:
```plaintext
/
etc/
nginx/
includes.d/
site1/
rewrites.conf
proxy.conf
site2/
rewrites.conf
```
To explicitly walk through the example, consider just the `site1` site key in the `wordpress_sites` list. The corresponding `nginx-includes/site1/` directory contains two confs which are templated to the remote's `/etc/nginx/includes.d/site1/`. The primary Nginx conf for `site1` will have a statement `include includes.d/site1/*.conf;`, thus including `rewrites.conf` and `proxy.conf`.
This `include` directive is located inside the primary `server` block, just before the primary `location` block. Explore the [Child templates](#child-templates) section below for more options if this default does not satisfy your needs.
::: warning Note
This default `include` directive per site will not recurse subdirectories within `includes.d/site1` so if you place templates in `nginx-includes/site1/somedir/*.conf.j2`, they will be templated to the remote's `includes.d/site1/somedir/*.conf` but will not be included by default. See the [Child templates](#child-templates) section below for how you could include such confs.
:::
### File cleanup
By default, Trellis will remove from the remote's `includes.d` directory any `*.conf` file that lacks a corresponding template in your local machine's `nginx-includes`. If removing config files results in an empty directory, that directory is not removed. If you prefer to leave all conf files on the remote, you may disable this file cleanup by defining `nginx_includes_d_cleanup: false` in `group_vars/all/main.yml`.
### Deprecated templates directory
The original implementation of Trellis Nginx includes required template files to be stored in `roles/wordpress-setup/templates/includes.d`. That directory is now deprecated and will no longer function beginning with Trellis 1.0. Please move your templates to a directory named `nginx-includes` in the root of this project. It is preferable to store user-created templates separate from Trellis core files.
Trellis Nginx includes were originally made possible thanks to @chriszarate in [#242](https://github.com/roots/trellis/pull/242).
## Child templates
You may use child templates to override any `block` in the two Nginx conf templates:
- [`roles/nginx/templates/nginx.conf.j2`](https://github.com/roots/trellis/blob/master/roles/nginx/templates/nginx.conf.j2) is templated to the server as `/etc/nginx/nginx.conf`
- [`roles/wordpress-setup/templates/wordpress-site.conf.j2`](https://github.com/roots/trellis/blob/master/roles/wordpress-setup/templates/wordpress-site.conf.j2) is templated to the server as `/etc/nginx/sites-available/example.com.conf` (per each site)
Create your child templates following the [Jinja template inheritance](https://jinja.palletsprojects.com/en/stable/templates/#template-inheritance) docs and the guidelines below.
::: tip
Once you have set up your child templates, append `--tags nginx-includes` to your command to run only the Nginx conf portions of the playbook.
:::
### Designate a child template
You will need to inform Trellis of the child templates you have created.
#### `nginx_conf`
Use the `nginx_conf` variable to designate your child template for `nginx.conf.j2`. Given that this template applies to all sites, it would be appropriate to define the variable in a `group_vars//main.yml` file (including `group_vars/all/main.yml`).
```yaml
nginx_conf: nginx-includes/nginx.conf.child
```
The example above designates a child template in the `nginx-includes` path on your local machine (i.e., the default path for `nginx_includes_templates_path` variable; see [`include` files](#include-files) section above). You may choose a different path and assign the template any name and file extension you wish. When using the `nginx-includes` path, however, avoid using a filename that matches the `*.conf.j2` pattern required for `include` files described above.
#### `nginx_wordpress_site_conf`
Use the `nginx_wordpress_site_conf` variable to designate your child template for `wordpress-site.conf.j2`, which is used for each of your sites. To designate a global child template for all your sites, you could define the variable in a `group_vars//main.yml` file.
```yaml
nginx_wordpress_site_conf: nginx-includes/wordpress-site.conf.child
```
You may designate a child template per site by defining the variable in `group_vars//wordpress_sites.yml`.
```yaml
wordpress_sites:
example.com:
...
nginx_wordpress_site_conf: nginx-includes/example.com.conf.child
...
```
### Create a Child Template
Create your child templates at the paths you designated in the `nginx_conf` and `nginx_wordpress_site_conf` variables described above. [Child templates](https://jinja.palletsprojects.com/en/stable/templates/#child-template) must include two elements:
- an `{% extends 'base_template' %}` statement
- one or more `{% block block_name %}` blocks
#### Child Template Example – Simple
Here is an example child template that replaces the `http_begin` block in the `nginx.conf.j2` base template.
```jinja
{% extends 'roles/nginx/templates/nginx.conf.j2' %}
{% block http_begin -%}
server_names_hash_bucket_size 128;
server_names_hash_max_size 512;
{% endblock %}
```
The path for your base template – referenced in your `extends` statement – must be relative to the `server.yml` playbook (i.e., relative to the Trellis root directory).
#### Child Template Example – Complex
The first block in the example child template below augments the content of the `fastcgi_basic` block from the `wordpress-site.conf.j2` base template. It inserts {{ super() }}, which represents the original block content from the base template, then adds an extra `fastcgi_param`. The second block in the example rewrites the `redirects_https` block, omitting the `ssl_enabled` conditional and adding a new `listen 8080` directive.
```jinja
{% extends 'roles/wordpress-setup/templates/wordpress-site.conf.j2' %}
{% block fastcgi_basic -%}
{{ super() }}
fastcgi_param HTTPS on;
{%- endblock %}
{% block redirects_https %}
# Redirect to https
server {
listen 80;
listen 8080;
server_name {{ site_hosts | join(' ') }}{% if item.value.multisite.subdomains | default(false) %} *.{{ site_hosts_canonical | join(' *.') }}{% endif %};
{{ self.acme_challenge() -}}
location / {
return 301 https://$host$request_uri;
}
}
{% endblock -%}
```
You'll notice that these blocks use indentation and [whitespace control](https://jinja.palletsprojects.com/en/stable/templates/#whitespace-control) (e.g., `-%}`) parallel to their counterparts in the base template `wordpress-site.conf.j2`. This will achieve the best formatting of templated conf files on the server.
## Sites templates
You may use sites templates to add new sites confs to Nginx in addition to the standard WordPress confs. They are also Ansible/Jinja2 templates and thus can make full use of variables and logic.
Create your sites templates following the guidelines below.
::: tip
Once you have set up your sites templates, append `--tags nginx-sites` to your command to run only the Nginx sites portions of the playbook.
:::
### Default
By default in Trellis, a "no-default" site Nginx conf is included. Its purpose is to drop requests to unknown server names, preventing host header attacks and other potential problems.
The `nginx_sites_confs` variable contains the list of confs to be templated to the server's `sites-available` folder.
Its default value only registers the default site (whose template resides in `roles/nginx/templates/no-default.conf.j2`):
```yaml
nginx_sites_confs:
- src: no-default.conf.j2
```
Each entry to this variable also has an `enabled` parameter, which can be omitted, and defaults to `true`.
It controls whether the conf is linked to the server's `sites-enabled` folder, and thus activated.
The above default is equivalent to:
```yaml
nginx_sites_confs:
- src: no-default.conf.j2
enabled: true
```
However, you might want to add other sites for specific purposes.
### Designate a site template
You will need to inform Trellis of the sites templates you have created.
#### `nginx_sites_confs`
Use the `nginx_sites_confs` variable to designate your new site template. Given that this template applies to all environments, it would be appropriate to define the variable in a `group_vars//main.yml` file (including `group_vars/all/main.yml`).
Remember to keep the default site for security purposes if you don't have a specific reason to override it.
```yaml
nginx_sites_confs:
- src: no-default.conf.j2
- src: nginx-includes/example.conf.site.j2
```
The example above designates a site template in the `nginx-includes` path on your local machine (i.e., the default path for `nginx_includes_templates_path` variable; see [`include` files](#include-files) section above). You may choose a different path and assign the template any name and file extension you wish. When using the `nginx-includes` path, however, avoid using a filename that matches the `*.conf.j2` pattern required for `include` files described above.
### Create a site template
Create your site templates at the paths you designated in the `nginx_sites_confs` variable described above. Templates should start with an # {{ ansible_managed }} statement to indicate that the file is [managed by ansible](https://docs.ansible.com/projects/ansible/latest/reference_appendices/config.html).
#### Template example
Here is an example site template that hosts nginx default page, listening on `example.com` non-standard port 8080.
```nginx
# {{ ansible_managed }}
server {
listen 8080;
server_name example.com;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
}
```
### File cleanup
By default, Trellis will remove from the remote's `site-enabled` directory any link to a site conf file that has its `enabled` attribute set to `false`.
There is no cleanup of the confs in `sites-available`, they're only made mute by being disabled.
This example shows the addition of the above site template, while also disabling Trellis' default site.
```yaml
nginx_sites_confs:
- src: no-default.conf.j2
enabled: false
- src: nginx-includes/example.conf.site.j2
```
================================================
FILE: trellis/passwords.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-09-06 07:42
description: Manage passwords in Trellis for MySQL root, admin users, sudoer access, and WordPress databases. Store securely in Ansible Vault for production environments.
title: Password Management in Trellis
authors:
- alwaysblank
- ben
- fullyint
- Log1x
- swalkinshaw
---
# Password Management in Trellis
There are a few places you'll want to set/change passwords:
`group_vars//vault.yml`
- `vault_mysql_root_password`
- `vault_users.*.password`
- `vault_wordpress_sites.*.env.db_password`
`group_vars/development/vault.yml`
- `vault_wordpress_sites.admin_password`
`group_vars/all/vault.yml`
- `vault_mail_password`
For staging/production environments, it's best to randomly generate longer passwords using something like [random.org](https://www.random.org/passwords/).
You may be concerned about setting plaintext passwords in a Git repository, and you should be. We strongly recommend you encrypt these passwords before committing them to your repo. Trellis is structured to make it easy to enable [Ansible Vault](vault.md) to encrypt select files. Alternatively, you could try an option such as [git-crypt](https://github.com/AGWA/git-crypt).
::: warning Note
Any type of server configs such as this playbook should always be in a **private** Git repository.
:::
================================================
FILE: trellis/python.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2022-02-28 22:16
description: Install and configure Python for using Trellis. Python is required for Ansible automation that powers WordPress server provisioning and deployment.
title: Python Requirements for Trellis
authors:
- swalkinshaw
---
# Python Requirements for Trellis
Trellis' main requirement is Python because Ansible is built with Python.
This page documents the best way to install Python on your computer, how to
manage Python package dependencies (like Ansible), common issues to avoid, and
using trellis-cli to make your life easier.
When dealing with Trellis and Python, there's three key points:
1. Make sure you have a stable version of Python 3 and pip installed
2. Use [trellis-cli](https://github.com/roots/trellis-cli) since it handles
dependencies for you
3. **Never** use `sudo` when installing packages with `pip`
## Python 2 vs Python 3
Python 2 reached end-of-life in 2019 and hasn't been maintained since then. For
that reason, newer version of Trellis (and trellis-cli) only support Python 3.
Unlike most languages that have a single version installed at a time, and only
offer an "unversioned" single binary path (such as just `node`), Python can be
more confusing because most operating systems treat them separately with
`python3` and `python` (which can be version 2 or 3 depending on your setup).
Regardless of the OS, it's still possible to symlink `python3` to `python` for
convenience as well.
## Installing Python
### macOS
Newer versions of macOS like Monterey and Big Sur come with both versions 2 and
3. Annoyingly though, the unversioned `python` is 2.7x while `python3` needs to
be explicitly used for Python 3.
While using the system Python on macOS should work fine, the main downside is
that the versions are only updated when macOS itself has a new major version.
If you want to have more control over Python versions, we recommend using a tool
like [pyenv](https://github.com/pyenv/pyenv) or [asdf](https://github.com/asdf-community/asdf-python)
to install specific versions globally (or even per project if that's needed).
We **do not recommend** installing Python from Homebrew. This might be
surprising since it goes against most guides and recommendations but we believe
using Python from Homebrew will cause more problems long-term due to its newer
"feature" of auto-upgrading packages as described in [this article](https://justinmayer.com/posts/homebrew-python-is-not-for-you/).
After Python is installed and working, you'll also need to ensure pip is installed. If `pip` or `pip3` does not exist, it can be installed like this:
```shell
$ python3 -m ensurepip
```
### Ubuntu
Ubuntu 20.04 comes default with Python 3 available as `python3`
only. There's no "unversioned" `python`.
The [`python-is-python3`](https://packages.ubuntu.com/focal/python-is-python3) package
exists solely as an easy way to symlink `/usr/bin/python` to `python3`.
```shell
$ sudo apt-get install -y python3 python-is-python3 python3-pip
```
## Installing and managing dependencies
Once you have Python working, the next step is ensuring you can install Trellis'
dependencies. They are always declared in the
[`requirements.txt`](https://github.com/roots/trellis/blob/master/requirements.txt) file,
but mainly this involves installing Ansible.
[pip](https://pypi.org/project/pip/) is Python's package installer and what
Trellis recommends using. But this is where trellis-cli comes in!
### trellis-cli and Virtualenv
We **strongly recommend** using trellis-cli whenever possible since it will make
your life managing dependencies and installing Ansible much easier.
trellis-cli uses [Virtualenv](https://virtualenv.pypa.io) to manage dependencies _per_ project.
It creates a "virtual environment" within each Trellis project so the
dependencies are completely isolated. This allows different projects to have
different versions of Ansible installed for example.
trellis-cli automatically creates a virtualenv and installs dependencies via pip
at two points:
1. when a new project is created with `trellis new`
2. when `trellis init` is run for an existing project
Once the virtualenv exists, all other `trellis` commands automatically use it.
This means running `trellis deploy production` will activate the virtualenv
(within the CLI, for the lifetime of the command) and use the version of Ansible
within the virtual environment.
When using trellis-cli, you should almost never have to use `pip` manually
yourself. There's a more advanced Virtualenv integration offered as well. See
the [README](https://github.com/roots/trellis-cli#virtualenv) for more details.
### Manually using pip
If you do need to run `pip` manually to install Ansible, here's a few tips:
1. **Never** use `sudo` with `pip`. It will only cause problems.
2. Make sure you're using the version of pip that corresponds to your Python
version. If you're using Python 3, then you might need to use `pip3`.
3. Avoid installing `ansible` directly with pip. Instead run `pip install -r requirements.txt` within a Trellis project to ensure you're getting a supported version of Ansible.
================================================
FILE: trellis/redis.md
================================================
---
date_modified: 2026-03-02 12:00
date_published: 2025-10-24 12:00
description: Enable Redis in Trellis for WordPress object caching. Improve site performance by caching database queries and reducing load on MySQL database servers.
title: Redis Object Caching for WordPress in Trellis
authors:
- ben
- TangRufus
---
# Redis Object Caching for WordPress in Trellis
Trellis supports two types of caching that work together:
- [**FastCGI Cache**](/trellis/docs/fastcgi-caching/) - Full page caching at the nginx level
- **Object Cache** - Database query and transient caching (Redis or Memcached)
These can be used independently or together for optimal performance.
## Configuration
### Maximum Performance (FastCGI + Object Cache)
Enable both FastCGI page caching and Redis object caching:
```yaml
wordpress_sites:
example.com:
cache:
enabled: true # Enables FastCGI page caching
duration: 30s # FastCGI cache duration
object_cache:
enabled: true # Enables object caching
provider: redis # Use Redis (or memcached)
database: 0 # Redis database number
```
### FastCGI Cache Only (Default/Backward Compatible)
Existing configurations continue to work unchanged:
```yaml
cache:
enabled: true # FastCGI page caching only
duration: 30s
skip_cache_uri: /wp-admin/|/wp-json/|/xmlrpc.php
skip_cache_cookie: comment_author|wordpress_[a-f0-9]+
background_update: "on"
```
### Object Cache Only
If you need Redis object cache without page caching:
```yaml
cache:
enabled: false # Disable FastCGI page caching
object_cache:
enabled: true # Enable object caching
provider: redis # Use Redis (or memcached)
database: 0 # Redis database number
```
## Cache Types
### FastCGI Cache (Page Caching)
- **Purpose**: Caches complete HTML pages
- **Performance**: Fastest possible page loads for cached content
- **Best for**: High-traffic sites with mostly static content
- **Location**: nginx level, bypasses PHP entirely
### Redis Object Cache
- **Purpose**: Caches database queries, transients, and objects
- **Performance**: Reduces database load significantly
- **Best for**: Database-heavy sites, complex queries
- **Features**: Persistent across page loads, shared data
### Memcached Object Cache
- **Purpose**: Alternative to Redis for object caching
- **Performance**: Similar to Redis, slightly different characteristics
- **Best for**: When Redis isn't available or preferred
## WordPress Plugin Installation
### For Redis Object Cache
Install a Redis object cache plugin in your WordPress site:
1. [Redis Object Cache](https://wordpress.org/plugins/redis-cache/)
```bash
composer require wp-plugin/redis-cache
```
2. After deployment, activate the plugin and enable object caching:
```bash
wp plugin activate redis-cache
```
```bash
wp redis enable
```
3. If your Redis server requires a password, add the following to your Bedrock config (`config/application.php`):
```php
Config::define('WP_REDIS_PASSWORD', env('WP_REDIS_PASSWORD'));
```
### For Redis Full-Site Cache
Install [MilliCache](https://github.com/MilliPress/MilliCache) in your WordPress site:
1. Require the package:
```bash
composer require millipress/millicache
```
2. After deployment, activate the plugin:
```bash
wp plugin activate millicache
```
3. Verify the object cache is working:
```bash
wp millicache test
```
```bash
wp millicache status
```
### For Memcached Object Cache
Install [Memcached Object Cache](https://github.com/Automattic/wp-memcached):
::: tip
`reference` and `url` must be pinned to the same commit or tag. Do not reference a branch.
For a different commit or tag, inspect that revision's `composer.json` in the `automattic/wp-memcached` repository and copy its `require` section here so the requirements match the version you're pinning. Replace `composer/installers` with `"koodimonni/composer-dropin-installer":"^1.4"`.
:::
1. Add the package repository:
```bash
composer repo add wp-memcached '{"type":"package","package":{"name":"automattic/wp-memcached","type":"wordpress-dropin","version":"dev-master","dist":{"type":"file","url":"https://raw.githubusercontent.com/Automattic/wp-memcached/bb3b9f689dd99df66454b93ece34093556dc37f9/object-cache.php","reference":"bb3b9f689dd99df66454b93ece34093556dc37f9"},"require":{"koodimonni/composer-dropin-installer":"^1.4","php":">=7.4.0","ext-memcache":"*"}}}' --before wp-packages
```
2. Configure the install location:
```bash
composer config allow-plugins.koodimonni/composer-dropin-installer true
```
```bash
composer config --json extra.dropin-paths '{"web/app/":["type:wordpress-dropin"]}'
```
3. Require the package (omit `--ignore-platform-req=ext-memcache` if already installed):
```bash
composer require automattic/wp-memcached:dev-master --ignore-platform-req=ext-memcache
```
4. Untrack the drop-in because it is now managed by Composer:
```bash
echo 'web/app/object-cache.php' >> .gitignore
```
## Configuration Examples
### Maximum Performance Setup
```yaml
wordpress_sites:
example.com:
cache:
enabled: true # FastCGI page caching
duration: 60s
object_cache:
enabled: true # Object caching
provider: redis # Using Redis
database: 0
```
### Multiple Sites with Isolated Object Caches
```yaml
# Site 1
wordpress_sites:
site1.com:
cache:
enabled: true
object_cache:
enabled: true
provider: redis
database: 0 # Redis DB 0
# Site 2
wordpress_sites:
site2.com:
cache:
enabled: true
object_cache:
enabled: true
provider: redis
database: 1 # Redis DB 1
```
### Mixed Cache Strategies
```yaml
# High-traffic marketing site (page cache only)
wordpress_sites:
marketing.com:
cache:
enabled: true
duration: 300s # 5-minute page cache
# Database-heavy app (both caches)
wordpress_sites:
app.com:
cache:
enabled: true
duration: 30s
object_cache:
enabled: true
provider: redis # Adds object caching
database: 0
```
## Customizing Redis Configuration
### Global Redis Settings
You can customize Redis settings in `group_vars/all/main.yml`:
```yaml
# Increase memory allocation (default: 256mb)
redis_maxmemory: 512mb
# Change eviction policy (default: allkeys-lru)
redis_maxmemory_policy: allkeys-lru
# Enable Redis password
redis_requirepass: your_secure_password
# Persistence settings
redis_appendonly: "yes" # Enable AOF persistence
```
### Advanced Configuration
For more advanced Redis configuration, you can override any setting in `group_vars/all/main.yml`:
```yaml
redis_extra_config:
tcp-backlog: 511
tcp-keepalive: 300
supervised: systemd
```
## Advanced Configurations
### Custom Redis Settings per Site
```yaml
wordpress_sites:
example.com:
cache:
enabled: true
object_cache:
enabled: true
provider: redis
host: 127.0.0.1
port: 6379
database: 0
password: secret_password
prefix: custom_prefix_
```
### Memcached Sessions + Redis Object Cache
```yaml
# In group_vars/all/main.yml
memcached_sessions: true # Use Memcached for PHP sessions
# In wordpress_sites.yml
wordpress_sites:
example.com:
cache:
enabled: true # FastCGI page cache
object_cache:
enabled: true # Redis object cache
provider: redis
database: 0
```
This setup uses three different cache systems:
- **Memcached**: PHP sessions
- **Redis**: WordPress object cache
- **FastCGI**: Page cache
## Monitoring
Monitor Redis usage:
```bash
redis-cli
```
```bash
redis-cli INFO memory
```
```bash
redis-cli INFO stats
```
```bash
redis-cli MONITOR
```
================================================
FILE: trellis/remote-server-setup.md
================================================
---
date_modified: 2024-09-11 10:00
date_published: 2015-10-15 12:27
description: Set up remote servers for Trellis requiring bare Ubuntu 24.04 LTS installation on VPS or dedicated servers. Shared hosting is not supported.
title: Remote Server Setup for WordPress with Trellis
authors:
- ben
- fullyint
- Log1x
- MWDelaney
- nicbovee
- swalkinshaw
- MWDelaney
---
# Remote Server Setup for WordPress with Trellis
Trellis can be used for setting up remote servers (offered by VPS/cloud service providers such as [DigitalOcean](/trellis/docs/deploy-to-digitalocean/) and [Hetzner Cloud](/trellis/docs/deploy-to-hetzner-cloud/)) to host your staging and production environments.
::: tip
ℹ️ Sign up for [Hetzner Cloud](https://console.hetzner.com/refer?pk_campaign=referral-invite&pk_medium=referral-program&pk_source=reflink&pk_content=V6DnI7GDHM4N) through the Roots referral link to receive $20 in cloud credits.
:::
Trellis CLI includes a `trellis server create` command that can automatically create and provision a server on a supported cloud provider:
```shell
$ trellis server create production
```
This command requires a cloud provider API token. If the token environment variable is not set, the command will prompt for one.
| Provider | Environment Variable | Token Link |
| --- | --- | --- |
| DigitalOcean | `DIGITALOCEAN_ACCESS_TOKEN` | [Create a DigitalOcean token](https://cloud.digitalocean.com/account/api/tokens/new) |
| Hetzner Cloud | `HCLOUD_TOKEN` | [Create a Hetzner API token](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/) |
See the [CLI docs](/trellis/docs/cli/) for more details on configuring your cloud provider.
::: warning
**Trellis cannot provision shared or managed hosts.** Trellis requires a bare server if you want to use it for provisioning.
:::
## Server requirements
* Ubuntu 24.04 LTS
* SSH access to the server
You need a server running a bare/stock version of Ubuntu 24.04 LTS. If you're using a host such as DigitalOcean that lets you pick an OS to start with, then select the Ubuntu 24.04 option.
You need to be able to connect to your Ubuntu server from your local computer via SSH. We *highly* suggest doing this via SSH keys so you don't have to specify a password every time. Many hosts offer to automatically add your SSH key when creating a server, so take advantage of that.
Once you have a Ubuntu server up and running, you can provision it.
## Provisioning
Provisioning a server means to set it up with the necessary software and configuration to run a WordPress site. For Trellis this means things like: installing MariaDB, installing Nginx, configuring Nginx, creating a database, etc.
Trellis has two main [playbooks](https://docs.ansible.com/projects/ansible/latest/user_guide/playbooks_intro.html): `dev.yml` and `server.yml`. As mentioned in local development, Trellis automatically runs the `dev.yml` playbook for us.
For remote servers, you provision a server via the `server.yml` playbook. This leaves you with a server *prepared* to run a WordPress site, but without the actual codebase yet.
Before provisioning your server, there's a little more configuration to do.
First determine the _environment_ you want to configure; after development,
you'll likely be creating a `production` or `staging` environment.
### Configuration
1. Copy your `wordpress_sites` from your working development site in `group_vars/development/wordpress_sites.yml` to `group_vars//wordpress_sites.yml`.
2. Modify your site and add the necessary settings for [remote servers](wordpress-sites.md#remote-servers) since they have a few more settings than local development. Also see the [Passwords docs](passwords.md).
3. Add your server hostname to `hosts/` (replacing `your_server_hostname`).
4. Specify public SSH keys for `users` in `group_vars/all/users.yml`. See the [SSH Keys docs](ssh-keys.md).
5. Consider setting `sshd_permit_root_login: false` in `group_vars/all/security.yml`. See the [Security docs](security.md).
Now you're ready to provision your server. Ansible connects to the remote server
via SSH so run the following command from your local machine:
```shell
$ trellis provision
```
### Re-provisioning
Re-provisioning is always assumed to be a safe operation. When you make changes to your Trellis configuration, you should provision your remote servers again to apply the changes:
Run the following from any directory within your project:
```shell
$ trellis provision
```
You can also provision with specific tags to only run the relevant roles:
Run the following from any directory within your project:
```shell
$ trellis provision --tags users
```
================================================
FILE: trellis/sage-integration.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2020-04-07 23:23
description: Use Trellis with Sage themes for complete WordPress development stack. Trellis handles server provisioning and deployment for Sage-based theme development.
title: Sage Theme Integration with Trellis
authors:
- swalkinshaw
---
# Sage Theme Integration with Trellis
Trellis is designed to be theme-agnostic and is not tied to Roots' Sage theme at all.
That doesn't mean it's hard though; all that's needed is a way to compile assets.
## Compiling production theme assets
Sage, like many WordPress themes now, requires a step during deploys to compile production assets.
Trellis includes an *example* `build-before` deploy hook designed to work with Sage. See [`deploy-hooks/build-before.yml`](https://github.com/roots/trellis/blob/master/deploy-hooks/build-before.yml).
For Sage users, simply uncomment that built-in example hook file and deploy away. Assets will be compiled *locally* and then copied to the remote server.
## Other themes
The Sage example deploy hook can also serve as a blueprint for many other themes that need to run an `npm` or `yarn` command for compiling assets.
## NVM
If you use [nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions for running themes locally, the easiest way to use it is via `$NVM_DIR/nvm-exec`.
Example:
```yaml
- name: Install npm dependencies
command: $NVM_DIR/nvm-exec npm install
delegate_to: localhost
args:
chdir: "{{ project_local_path }}/web/app/themes/mytheme"
- name: Compile assets for production
command: $NVM_DIR/nvm-exec npm run build:production
delegate_to: localhost
args:
chdir: "{{ project_local_path }}/web/app/themes/mytheme"
```
================================================
FILE: trellis/security.md
================================================
---
date_modified: 2026-03-05 00:00
date_published: 2015-09-06 07:42
description: Secure Trellis WordPress servers by disabling root SSH login, creating admin users with sudo access, configuring secure password authentication, and hardening WordPress runtime file permissions.
title: WordPress Security Features in Trellis
authors:
- ben
- fullyint
- Log1x
- QWp6t
- swalkinshaw
---
# WordPress Security Features in Trellis
## Locking down root
The `sshd` role heightens your server's security by providing better SSH defaults. SSH password authentication will be disabled. We encourage you to disable SSH `root` login as well. You may adjust these two particular options in `group_vars/all/security.yml`. See the [`sshd` role `README.md`](https://github.com/roots/trellis/tree/master/roles/sshd) for more configuration options.
## Admin user
The first provision via the `server.yml` playbook will create the `admin_user` and set up related [SSH Keys](ssh-keys.md). If you disable `root` login, subsequent connections will be made as the `admin_user`.
## Admin user sudoer password
If `root` login is disabled and the `server.yml` playbook connects as the `admin_user`, it will invoke `sudo` using the password in `vault_users` (`group_vars//vault.yml`). If you run the playbook with `--ask-become-pass`, Trellis will use the password you enter via the CLI. You are strongly encouraged to protect the sensitive `vault_users` information by enabling Ansible [Vault](vault.md).
## WordPress runtime hardening
Trellis supports an opt-in hardening mode that separates the PHP-FPM runtime identity from the deploy user. When enabled, PHP runs as a dedicated user with write access limited to explicitly allowlisted paths. This reduces the impact of a compromised WordPress site by preventing PHP from modifying application code.
By default, hardening is disabled and Trellis behaves as it always has — PHP-FPM runs as the `web_user`.
### Enabling hardening
Add the following to `group_vars/all/main.yml` (or an environment-specific file like `group_vars/production/main.yml`):
```yaml
wordpress_runtime_hardened: true
```
### Configuration options
| Variable | Default | Description |
| --- | --- | --- |
| `wordpress_runtime_hardened` | `false` | Enable runtime hardening mode |
| `wordpress_runtime_user` | `www-data` | OS user that PHP-FPM runs as when hardened |
| `wordpress_runtime_group` | `www-data` | OS group that PHP-FPM runs as when hardened |
| `wordpress_runtime_writable_paths` | `["shared/uploads"]` | Paths the runtime user can write to (relative to the site root) |
| `wordpress_runtime_cron_as_runtime_user` | `false` | Run WP-CLI cron as the runtime user instead of `web_user` |
### Using a custom runtime user
For stronger isolation, use a dedicated user instead of `www-data`. The user and group must exist on the server before hardening is enabled — the playbook will fail fast if they don't.
Define the user in `group_vars/all/users.yml`:
```yaml
users:
- name: php-app
groups:
- php-app
keys: []
```
Then configure the runtime variables:
```yaml
wordpress_runtime_hardened: true
wordpress_runtime_user: php-app
wordpress_runtime_group: php-app
```
### Per-site writable paths
The global `wordpress_runtime_writable_paths` applies to all sites by default. You can override it for individual sites in your [WordPress Sites](/trellis/docs/wordpress-sites/) configuration:
```yaml
wordpress_sites:
example.com:
runtime_writable_paths:
- shared/uploads
- current/web/app/cache
```
### Cron user
By default, WP-CLI cron jobs continue to run as the `web_user` even when hardening is enabled. To run cron as the runtime user instead:
```yaml
wordpress_runtime_cron_as_runtime_user: true
```
This only takes effect when `wordpress_runtime_hardened` is also `true`.
================================================
FILE: trellis/server-logs.md
================================================
---
date_modified: 2023-01-31 17:40
date_published: 2018-04-24 09:59
description: Trellis site logs are located at `/srv/www/example.com/logs/` including Nginx access logs, error logs, and PHP-FPM logs for troubleshooting issues.
title: Accessing Server Logs in Trellis
authors:
- ben
- Log1x
- swalkinshaw
---
# Accessing Server Logs in Trellis
## Accessing logs
Trellis CLI includes a `logs` command for quickly accessing logs. It automatically integrates with [GoAccess](https://goaccess.io/) when the `--goaccess` option is used.
```shell
$ trellis logs [options] ENVIRONMENT [SITE]
```
| Description | Command |
| -------------------------- | ------------------------------------ |
| View production logs | `trellis logs production` |
| View access logs only | `trellis logs --access production` |
| View error logs only | `trellis logs --error production` |
| View logs in GoAccess | `trellis logs --goaccess production` |
| View the last 50 log lines | `trellis logs -n 50 production` |
Run `trellis logs --help` for further information.
## Location of logs
Server logs for Trellis sites can be found at `/srv/www/example.com/logs/`:
- `/srv/www/example.com/logs/access.log`
- `/srv/www/example.com/logs/error.log`
Any server 500 errors or white screen issues should be debugged by viewing the error logs in the `/srv/www/example.com/logs/` directory.
Trellis uses the [ansible-logrotate](https://github.com/nickhammond/ansible-logrotate) role to install and configure logrotate for sites and can be configured by editing [`group_vars/all/logrotate.yml`](https://github.com/roots/trellis/blob/master/group_vars/all/logrotate.yml).
================================================
FILE: trellis/ssh-keys.md
================================================
---
date_modified: 2025-10-16 10:00
date_published: 2015-09-06 07:42
description: Configure SSH keys in Trellis for secure server access. Add keys manually or automatically import SSH keys from GitHub users for team member access.
title: SSH Key Management in Trellis
authors:
- ben
- dalepgrant
- evance
- fullyint
- knowler
- Log1x
- swalkinshaw
- techieshark
---
# SSH Key Management in Trellis
Each Trellis playbook uses a specific SSH user to connect to your remote machines (or virtual machine in development).
| Playbook | Default User | User Variable | Task |
| ------------ | ----------------- | ------------- | ------------------------ |
| `dev.yml` | Your local username | - | create development VMs |
| `server.yml` | `root` or `admin` | `admin_user` | provision remote servers |
| `deploy.yml` | `web` | `web_user` | deploy WordPress sites |
This page reviews how to configure SSH users for the `server.yml` and `deploy.yml` playbooks. If you are looking for general SSH configuration options, see the [`sshd` role `README.md`](https://github.com/roots/trellis/tree/master/roles/sshd) .
If you will be the only person provisioning and deploying, and your SSH public key is available at `~/.ssh/id_ed25519.pub`, you probably won't need to modify the Trellis defaults for `users`.
## The `users` Dictionary
While provisioning, `server.yml` will create the `users` defined in `group_vars/all/users.yml`, assigning their `groups` and public SSH `keys`. The example below defines a single user.
```yaml
users:
- name: username
groups:
- primary_group
- other_group
keys:
- "{{ lookup('file', '/path/to/local/file') }}"
- https://github.com/username.keys
```
Specify the user's primary group first in the list of `groups`. List `keys` for anyone who will need to make an SSH connection as that user. `server.yml` can `lookup` keys in local files or retrieve them from remote host URLs.
::: tip GitHub example
Using `https://github.com/.keys` is a quick way to get all the public SSH keys you have on your GitHub account added onto the server.
:::
If needed, you can define different users *per* environment by redefining `users` in any `group_vars//users.yml` file.
## `server.yml`
Trellis assumes that when you first create your server you've already added your SSH key to the `root` account. How this happens depends on your cloud provider but here's a few common ones:
- *Digital Ocean*: gives you the option to automatically add your SSH key when creating your droplet
- *AWS*: provides a `pem` file which needs to be converted to a `pub` file for use with Ansible (example: `ssh-keygen -y -f private_key1.pem > public_key1.pub `)
`server.yml` will try to connect to your server as `root`. If the connection fails, `server.yml` will try to connect as the `admin_user` defined in `group_vars/all/users.yml` (default `admin`). If `root` login will be disabled on your server, it is critical for the `admin_user` to be defined in your list of `users`, with `sudo` first in this user's list of groups (see the [Security docs](security.md)). The default definition for the `admin_user` is shown below.
```yaml
users:
- name: '{{ admin_user }}'
groups:
- sudo
keys:
- "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
# - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
# - https://github.com/username.keys
admin_user: admin
```
- You may enable colleagues to run `server.yml` by adding their public SSH `keys` to the `admin_user`.
- If your hosting provider disables root but provides a default user such as `ubuntu`, specify `admin_user: ubuntu`.
- If you are trying to override the dynamic selection of `root` or `admin_user`, preferring to manually specify the Ansible remote user, review notes in the section [remote user variable precedence](https://github.com/roots/trellis/pull/274#issuecomment-121455761).
## `deploy.yml`
The `deploy.yml` playbook deploys your site while connecting as the `web_user` (default `web`) because this user owns files in the web root, the deploy destination. The `web_group` must come first in the list of groups for the web_user. The default definition for the `web_user` is shown below.
```yaml
web_user: web
web_group: www-data
users:
- name: "{{ web_user }}"
groups:
- "{{ web_group }}"
keys:
- "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
# - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
# - https://github.com/username.keys
```
You may enable colleagues to run `deploy.yml` by adding their public SSH `keys` to the `web_user`. See the example below.
## Example `users`
The example below adds the SSH keys of GitHub users `swalkinshaw` and `retlehs` to `~/.ssh/authorized_keys` for `admin_user`. This enables `swalkinshaw` and `retlehs` to run `server.yml` to provision the servers. The example also adds their keys, and the keys of GitHub user `austinpray`, to `web_user`. This enables each of them to run `deploy.yml` to deploy sites.
```yaml
users:
- name: '{{ web_user }}'
groups:
- '{{ web_group }}'
keys:
- https://github.com/swalkinshaw.keys
- https://github.com/retlehs.keys
- https://github.com/austinpray.keys
- name: '{{ admin_user }}'
groups:
- sudo
keys:
- https://github.com/swalkinshaw.keys
- https://github.com/retlehs.keys
- name: another_user
groups:
- some_group
- some_other_group
keys:
- https://github.com/swalkinshaw.keys
```
The example above also demonstrates the option of creating `another_user` whose primary group is `some_group`, but who is also in `some_other_group`, and who has public SSH keys for `swalkinshaw`.
## Removing keys
Removing a key from the configuration and re-provisioning the server does not remove the key from the server's `authorized_keys` file by default. To reset SSH keys, Trellis supports an opt-in "single use" `reset_user_ssh_keys` variable (as of [#1576](https://github.com/roots/trellis/pull/1576)):
```shell
$ trellis provision --extra-vars reset_user_ssh_keys=true production
```
::: tip Note
This will first replace all keys on the remote with the _first found_ key from your defined users before re-adding the rest. Make sure you have access with the first key listed in your user dictionary before running this _just in case_ the process is interrupted.
:::
## Cloning remote repo using SSH agent forwarding
All the SSH connections discussed above apply to Trellis connecting from your local machine to your server. It is a different type of connection, however, when Trellis clones a remote private repo during deployment. In this case, your remote server is allowed to forward your local machine's SSH credentials to the remote repo to authorize the connection.
The Trellis `ansible.cfg` file enables this SSH agent forwarding with `ssh_args = -o ForwardAgent=yes`. You should not need auth tokens or private keys for the `web_user`. If you run into trouble cloning a remote repo during deploy, see [Using SSH agent forwarding](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/using-ssh-agent-forwarding) for tips and troubleshooting.
### macOS/OS X users
Remember to import your SSH key password into Keychain. For macOS Monterey (12.0) and later, you should run:
```shell
$ ssh-add --apple-use-keychain
```
For versions prior, you need to run:
```shell
$ ssh-add -K
```
================================================
FILE: trellis/ssl.md
================================================
---
date_modified: 2026-05-03 12:00
date_published: 2015-09-06 07:42
description: Enable HTTPS in Trellis with automatic Let's Encrypt certificates, manually provided SSL certificates, or self-signed certificates for local development.
title: SSL Certificates in Trellis
authors:
- aitor
- ben
- dalepgrant
- fullyint
- joshf
- Log1x
- MWDelaney
- qwatts-dev
- runofthemill
- swalkinshaw
---
# SSL Certificates in Trellis
HTTPS is now more important than ever. Strong encryption through HTTPS creates a safer and more secure web while protecting your site's users.
Roots believes in security so we've always made SSL/HTTPS a priority in Trellis. Our implementation is designed to score an A+ on the [Qualys SSL Labs Test](https://www.ssllabs.com/ssltest/).
In the past many people avoided going HTTPS for technical and convenience reasons:
- Certificates were expensive
- Annoying and complicated web-server configuration
- HTTPS sites were much slower than HTTP
Trellis has features to make it as easy, cheap, and painless as possible to use HTTPS giving you no excuse *not* to use it.
There are three supported certificate *providers* in Trellis:
- [Let's Encrypt](#lets-encrypt)
- [Manual](#manual)
- [Self-signed](#self-signed)
HTTPS can be enabled on a per-site basis. However, by default, enabling SSL on a site will make that site HTTPS **only**. Meaning that all HTTP requests will be redirected to HTTPS with the proper HSTS headers set as well. Unless you have a good reason to change this default, you shouldn't. See the section on [HSTS](#hsts) for more details.
CloudFlare Origin CA support can be added with [trellis-cloudflare-origin-ca](https://github.com/TypistTech/trellis-cloudflare-origin-ca).
## Configuration
Any SSL provider starts with the same basic configuration. Add the following to a WP site:
```yaml
# group_vars/production/wordpress_sites.yml (example)
example.com:
# rest of site config
ssl:
enabled: true
provider:
```
### Let's Encrypt
[Let's Encrypt](https://letsencrypt.org/) (LE) is a new Certificate Authority that is free, automated, and open.
Unless you already have an SSL certificate purchased, Let's Encrypt should be your provider choice. Let's Encrypt is appropriate for your production and staging environments, but not for development (see [DNS records](#dns-records)).
Trellis has complete automated integration. The only required setting is the `provider` itself:
```yaml
# group_vars/production/wordpress_sites.yml (example)
example.com:
# rest of site config
ssl:
enabled: true
provider: letsencrypt
```
There is one main difference between LE and other certificate authorities: their certificates expire every *90 days*. Trellis automates by running a cron-job so you never have to manually renew them or worry about them expiring like a paid certificate.
::: warning Note
Let's Encrypt is ending support for v1 of their ACME protocol. If you are using a Trellis version older than `v1.2.0`, please see [here](https://discourse.roots.io/t/trellis-and-lets-encrypt-v1-end-of-life/16816) for more details.
:::
#### DNS records
Let's Encrypt verifies and creates certificates through a publicly accessible web server for *every* domain you want on the certificate.
This means you need valid and working DNS records for every site host/domain you have configured for your WP site.
```yaml
# group_vars/production/wordpress_sites.yml (example)
mydomain.com:
site_hosts:
- canonical: mydomain.com
redirects:
- www.mydomain.com
- mydomaintoredirect.com
- www.mydomaintoredirect.com
ssl:
enabled: true
provider: letsencrypt
```
In the example above, Trellis will try to automatically create 1 certificate with the following hosts: `mydomain.com`, `www.mydomain.com`, `mydomaintoredirect.com` and `www.mydomaintoredirect.com`.
All you need to do is make sure those DNS records exist and point to the web server's IP. Trellis takes care of the rest.
If you want "www" subdomains to redirect to your canonical domain, they MUST be included in redirects.
#### Challenges
Let's Encrypt certificate process looks roughly like:
1. Generate private account key
2. Generate private key for each site (could have multiple domains)
3. Generate CSR (Certificate Signing Request) for each site (single/multiple domains)
4. Request certificate from LE by sending them the account key and CSR
5. LE client creates a "challenge" file in the web root of your site
6. LE server verifies it can access the challenge file
7. LE server sends the certificate if the challenge succeeds
The above steps is what Trellis handles automatically.
#### Multiple servers
Trellis' LE integration is designed by default for a single server. If you have multiple web servers behind a load balancer, you will *not* want this role/process running on all of them since it would generate different private and account keys for each one.
This process is beyond the scope of the documentation right now. However, there are two variables which help for this process:
- `letsencrypt_account_key_source_content`
- `letsencrypt_account_key_source_file`
You can use either of these to manually define an account key's contents or file. If one of these is set, it will be used and none will be automatically generated.
It's also up to you to make sure you've manually registered your account key. See [https://gethttpsforfree.com/](https://gethttpsforfree.com/) for a simple site to do this.
#### Staging
Let's Encrypt has rate limits for their production/real certificates. While Trellis will prevent these rate limits from being hit, if you want to test out LE integration, you can use their staging server to get a "fake" certificate.
::: warning Note
Note that browsers will display an error/warning that they don't recognize the Certificate Authority so this should only be used for testing purposes.
:::
Just set the following variable:
```yaml
# in a group_vars file
letsencrypt_ca: "https://acme-staging-v02.api.letsencrypt.org"
```
#### Troubleshooting Let's Encrypt
Trellis versions prior to [Jan 2017](https://github.com/roots/trellis/pull/630) did not detect some changes that should have triggered Let's Encrypt certificate regeneration. The most common example was users adding domain(s) to `site_hosts` (in `wordpress_sites`) and reporting that browsers gave privacy warnings for the new domains. Similar problems occurred for users switching from manual certificates to Let's Encrypt certificates.
If you see similar privacy warnings after adjusting your SSL configuration in some way, these troubleshooting steps may help.
1. Update trellis to include [`roots/trellis#630`](https://github.com/roots/trellis/pull/630)
2. Set ssl `enabled: false` for affected sites in `group_vars//wordpress_sites.yml`
3. Run `ansible-playbook server.yml -e env= --tags wordpress`
4. Reset ssl `enabled: true` for applicable sites in `group_vars//wordpress_sites.yml`
5. Run `ansible-playbook server.yml -e env= --tags letsencrypt`
### Manual
This provider means you're providing both the SSL certificate and private key. This was the original method included in Trellis.
```yaml
# group_vars/production/wordpress_sites.yml (example)
example.com:
# rest of site config
ssl:
enabled: true
provider: manual
cert: ~/ssl/example.com.crt
key: ~/ssl/example.com.key
```
`cert` and `key` are **local** relative paths to those files. They will be copied to the remote servers. This is done so your private key does not need to be stored in your Git repository for security reasons.
### Self-signed
The self-signed provider **should only be used for development or internal server purposes**. Trellis will generate a "fake" (or "snake-oil") certificate which is not recognized by browsers.
Browsers will prompt you with an error/warning that they don't recognize the Certificate Authority (which is yourself in this case).
```yaml
# group_vars/development/wordpress_sites.yml (example)
example.com:
# rest of site config
ssl:
enabled: true
provider: self-signed
```
#### Lima
Trust the Lima VM's self-signed certificate so browsers and host-side tooling stop showing warnings:
```shell
$ trellis vm trust
```
This pulls the cert and key out of the VM, exports them to `~/.local/share/trellis/ssl/-/`, and trusts the cert in the macOS login keychain and every Firefox profile. Re-runs are safe: if the cert is already trusted, the command reports it and does nothing.
Firefox support requires `certutil` (install via `brew install nss` on macOS or `apt install libnss3-tools` on Linux). On Linux, pass `--trust-system` to also add the cert to the system trust store.
Available flags:
- `--site` — only trust the cert for the named site
- `--no-export-key` — skip exporting the private key to the host
To reverse trust entries added by this project:
```shell
$ trellis vm untrust
```
To print the host paths of the exported cert and key per site:
```shell
$ trellis vm trust paths
```
## HSTS
Trellis sets [HSTS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security) headers for better security. HSTS will ensure all traffic to your site is being served over HTTPS automatically.
There are a few defaults set which you can override if need be:
- `hsts_max_age` - how long the header lasts (default: `31536000` (1 year))
- `hsts_include_subdomains` - also make *all* subdomains be served over HTTPS (default: `false`)
- `hsts_preload` - indicates the site owner's consent to have their domain preloaded (default: `false`)
These variables are configured on a site's `ssl` object:
```yaml
# group_vars/production/wordpress_sites.yml (example)
example.com:
# rest of site config
ssl:
enabled: true
provider: letsencrypt
hsts_max_age: 31536000
hsts_include_subdomains: true
hsts_preload: true
```
### Preload lists
What is HSTS Preloading?
> HSTS Preloading is a mechanism whereby a list of hosts that wish to enforce the use of SSL/TLS on their site is built into a browser. This list is compiled by Google and is utilised by Chrome, Firefox, Opera, Safari, IE11 and Edge. These sites do not depend on the issuing of the HSTS response header to enforce the policy, instead the browser is aleady aware that the host requires the use of SSL/TLS before any connection or communication even takes place. This removes the opportunity an attacker has to intercept and tamper with redirects that take place over HTTP. This isn't to say that the host needs to stop issuing the HSTS response header, this must be left in place for those browsers that don't use preloaded HSTS lists.
>
> - https://scotthelme.co.uk/hsts-preloading/
Using preloading is a two-step process:
1. Enable the `preload` option shown above by setting `hsts_preload: true`
2. Submit your site/domain to the official browser preload list: [https://hstspreload.org/](https://hstspreload.org/)
More information:
- [https://hstspreload.org/](https://hstspreload.org/)
- [HSTS Preloading](https://scotthelme.co.uk/hsts-preloading/)
### `max-age`
Trellis defaults to a long `max-age` of `31536000` seconds (1 year).
You may want to test out HSTS with much shorter max-ages and then ramp up the value in stages until you're confident everything works.
This deployment ramp up process is detailed here: [https://hstspreload.org/#deployment-recommendations](https://hstspreload.org/#deployment-recommendations)
### Disabling HSTS
The only way to disable HSTS is to set the `max-age` header to `0`:
```yaml
# group_vars/production/wordpress_sites.yml (example)
example.com:
# rest of site config
ssl:
enabled: true
provider: letsencrypt
hsts_max_age: 0
```
### `hsts_include_subdomains`
HSTS should ideally be applied to all subdomains as well when possible. This means that if you have HSTS enabled on `example.com`, then *all- its subdomains (`*.example.com`) will also be forced over HTTPS.
However, it's a common enough scenario for a subdomain to host another non-HTTPS
site for various reasons (maybe it's externally managed and out of your
control). For example, you might deploy a Trellis based WordPress site to `example.com` but also host another application from a subdomain such as `internalapp.example.com` which isn't secured by HTTPS.
Since HSTS' `includeSubdomains` option would break any subdomains in those
situations, Trellis _disables_ the `hsts_include_subdomains` option by default.
::: tip
If you are in control of your domain and all its subdomains, we **highly
recommend** you consider enabling the `includeSubdomains` option since it does
provide stricter guarantees and security for your users.
:::
This can be done by setting the `hsts_include_subdomains` option to `true`
(either globally or a per-site basis).
Per-site:
```yaml
# group_vars/production/wordpress_sites.yml (example)
example.com:
# rest of site config
ssl:
enabled: true
provider: letsencrypt
hsts_include_subdomains: true
```
Globally:
```yaml
# group_vars/production/main.yml
nginx_hsts_include_subdomains: true
```
## Client certificates
You may want to set up TLS Client Authentication, especially when using [Cloudflare Authenticated Origin Pulls](https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/). To enable, simply set the `client_cert_url` variable to the URL of the certificate authority (CA) that will be used to verify clients with.
```yaml
# group_vars/production/wordpress_sites.yml (example)
example.com:
# rest of site config
ssl:
# rest of ssl config
client_cert_url: https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem
```
## Performance
Our HTTPS implementation uses all performance optimizations possible to ensure your sites remain fast despite the small overhead of SSL. This includes the following features:
- HTTP/3 support with QUIC (fallback to HTTP/2 and HTTP/1.1 for older browsers)
- SSL session cache
- OCSP stapling
- 1400 byte TLS records
- Longer keepalives
HTTP/3 requires UDP port 443 to be open. If you have a cloud or hardware firewall in front of your server (eg: AWS security groups, DigitalOcean cloud firewalls), ensure it allows UDP/443 inbound traffic.
See [Is TLS Fast Yet?](https://istlsfastyet.com/) for more information on fast TLS/SSL.
## Browser support
Since our implementation is designed to be modern and score an A+ on the [Qualys SSL Labs Test](https://www.ssllabs.com/ssltest/), this does mean that a few older browsers such as IE6 won't be able to access your site due to the cipher suites used.
================================================
FILE: trellis/troubleshooting.md
================================================
---
date_modified: 2023-01-27 13:17
date_published: 2015-09-06 07:42
description: Troubleshoot Trellis installations with debugging tips for Ansible errors, solutions for unresponsive machines, and fixes for common provisioning problems.
title: Troubleshooting Common Trellis Issues
authors:
- ben
- fullyint
- Log1x
- swalkinshaw
- dalepgrant
---
# Troubleshooting Common Trellis Issues
## Debugging
Golden rule to debugging any failed command with Ansible:
1. Read the output logs and find the failed task.
2. Read through error message for the exact issue.
3. Re-run the command in `verbose` mode `ansible-playbook deploy.yml -vvvv -e "site= env="` if necessary to get more details.
4. SSH into your server and manually run the command where Ansible failed.
Example: if a Git clone task failed during deploys, then SSH into the server as the `web` user (which is what deploys use) and run the manual command such as `git clone `. This will give you a much better clue as to what's going wrong.
## Let's Encrypt SSL certificates
See [Troubleshooting Let's Encrypt](ssl.md#troubleshooting-let-s-encrypt).
## Composer install: host key verification failed
Sometimes a task that installs Composer dependencies gives an error `host key verification failed`. This can happen when the `known_hosts` file on your Lima VM or remote host is missing a key for one of the host `repositories` in the related `composer.json` file. Ensure that each host from `composer.json` has a key listed in `group_vars/all/known_hosts.yml` then try your `trellis provision development` or `trellis deploy` command again.
## SSH connections
If you have trouble with SSH connections to your server, consider the tips below. You may also want to review information about [disabling `root` login](security.md#locking-down-root) and how to configure your server's SSH settings via the [`sshd` role](https://github.com/roots/trellis/tree/master/roles/sshd).
### SSH keys
- [Generating a new SSH key and adding it to the ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)
- [Testing your SSH connection](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection)
- [Your local `ssh-agent` must be running](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/using-ssh-agent-forwarding#your-local-ssh-agent-must-be-running) (macOS users: remember to run `ssh-add -K`)
- How to designate [SSH keys](ssh-keys.md) in Trellis
SSH will automatically look for and try a default set of SSH keys, along with keys loaded in your `ssh-agent`. However, the SSH server will only let your SSH client try a limited number of keys before disconnecting (default: 6). If you have many SSH keys and the correct key is not being selected, you can force your SSH client to try only the correct key. Add this to your `~/.ssh/config` (with the correct path to your key):
```plaintext
Host example.com
IdentitiesOnly yes
IdentityFile /users/username/.ssh/id_ed25519
```
### Host key change
Your server may occasionally offer a different host key than what your local machine has on record in `known_hosts`. This could happen if you rebuild your server or if the `sshd` role configures your server to offer a stronger key.
**Example 1**
```plaintext
TASK [setup] *******************************************************************
System info:
Ansible 2.2.1.0; Darwin
Trellis at "Add `apt_packages_custom` to customize Apt packages"
---------------------------------------------------
SSH Error: data could not be sent to the remote host. Make sure this host can
be reached over ssh
fatal: [xxx.xxx.xxx.xxx]: UNREACHABLE! => {"changed": false, "unreachable": true}
to retry, use: --limit @/Users/yourname/sites/example.com/trellis/deploy.retry
```
**Example 2**
```plaintext
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:lv86hFykjn8pnOWE2WDWJo8Mzf6FTDMx/yWXOqzK5PU.
```
If this change in host keys is expected, then clear the old host key from your `known_hosts` by running the following command (with your real IP or host name).
```shell
$ ssh-keygen -R 12.34.56.78
```
Then try your Trellis playbook or SSH connection again.
If the host key change is unexpected, cautiously consider why the host identification may have changed and whether you may be victim to a man-in-the-middle attack.
### `git clone` or `composer install` task hangs or fails
The `sshd` role may cause your server's SSH client to request stronger host keys from hosts of git repos or composer packages. This could create the [host-key-change](#host-key-change) problem, but this time on your server instead of your local machine. Follow the same remediation steps, but on the server.
Similarly, the `sshd` role may cause your server's SSH client to require stronger [ciphers, kex algorithms, and MACs](https://github.com/roots/trellis/tree/master/roles/sshd#ciphers-kexalgorithms-and-macs) than previously. If your `git clone` or `composer install` connections involve older systems that do not support the stronger protocols, you may need to add more options to `ssh_ciphers_extra`, `ssh_kex_algorithms_extra`, or `ssh_macs_extra`.
### Verbose output
SSH connection issues are often difficult to resolve without verbose output. Use the `-vvvv` option with your `ansible-playbook` command:
```shell
$ ansible-playbook server.yml -e env=production -vvvv
```
You may also use `-v`, `-vv`, and `-vvv` with manual SSH connections:
```shell
$ ssh -v root@12.34.56.78
```
### Manual SSH
If your `ansible-playbook` command is failing its SSH connection, it can be helpful to try a manual SSH connection to narrow down the problem. If manual SSH fails, try again with `-v` for [verbose output](#verbose-output).
```shell
$ ssh -v root@12.34.56.78
```
### `Ciphers`, `KexAlgorithms`, or `MACs`
The `sshd` role will most likely cause your SSH server to discontinue using some older and weaker protocols. If your connections involve older systems that do not support the stronger protocols configured by the `sshd` role, see [`Ciphers`, `KexAlgorithms`, and `MACs`](https://github.com/roots/trellis/tree/master/roles/sshd#ciphers-kexalgorithms-and-macs) for how to add back in any protocols you need.
## APT sources
You may need to clean the APT sources to update a package, for example when [updating MariaDB mirrors](https://github.com/roots/trellis/issues/1575). You can set `apt_clean_sources: true` in `group_vars/all/main.yml` to run every provision, or to run this for one provision only, use:
```shell
$ trellis provision --extra-vars apt_clean_sources=true production
```
================================================
FILE: trellis/user-contributed-extensions.md
================================================
---
date_modified: 2024-10-12 16:15
date_published: 2015-09-06 07:42
description: Explore community-developed Ansible roles and extensions for Trellis that add functionality and features beyond the core WordPress server management.
title: User Contributed Extensions for Trellis
authors:
- ben
- Log1x
- MWDelaney
- strarsis
- swalkinshaw
- TangRufus
- Xilonz
---
# User Contributed Extensions for Trellis
Extensions (or roles), developed by the community, that complement Trellis.
Issues with extensions should be opened in their respective repositories.
- [bedrock-site-protect](https://github.com/louim/bedrock-site-protect) — Add or remove htpasswd protection to your websites
- [trellis-backup](https://discourse.roots.io/t/trellis-backup-an-ansible-role-for-local-backups/6497) — Set up automated backups to various locations using duplicity.
- [trellis-backup](https://github.com/Xilonz/trellis-backup-role) — Set up automated backups to various locations using duply.
- [trellis-cloudflare-origin-ca](https://typist.tech/portfolio-item/trellis-cloudflare-origin-ca/) — Add Cloudflare Origin CA to Trellis as SSL provider.
- [trellis-newrelic-php](https://typist.tech/portfolio-item/trellis-newrelic-php/) — Install New Relic PHP agent on Trellis servers
- [trellis-nixstats](https://github.com/Xilonz/trellis-nixstats/) — Install NIXStats agent on Trellis servers
- [trellis-database-uploads-migration](https://github.com/valentinocossar/trellis-database-uploads-migration) — Ansible playbook for Trellis that manages database and uploads migration
- [trellis-db-push-and-pull](https://github.com/hamedb89/trellis-db-push-and-pull) — Push and pull databases with Trellis and Ansible playbooks
- [trellis-backup-during-deploy](https://github.com/ItinerisLtd/trellis-backup-during-deploy) - Backup WordPress database during Trellis deploys
- [trellis-purge-kinsta-cache-during-deploy](https://github.com/ItinerisLtd/trellis-purge-kinsta-cache-during-deploy) - Purge Kinsta cache when Trellis deploys Bedrock
- [trellis-cve-2018-6389](https://github.com/ItinerisLtd/trellis-cve-2018-6389) - Mitigate CVE-2018-6389 WordPress load-scripts / load-styles attacks
- [trellis-disable-xml-rpc](https://github.com/ItinerisLtd/trellis-disable-xml-rpc) - Disable WordPress XML RPC on Trellis sites
- [trellis-purge-wp-rocket-cache-during-deploy](https://github.com/ItinerisLtd/trellis-purge-wp-rocket-cache-during-deploy) - Purge WP Rocket cache when Trellis deploys Bedrock
- [trellis_flush_rewrite_rules_during_deploy](https://github.com/ItinerisLtd/trellis_flush_rewrite_rules_during_deploy) - Resets WordPress' rewrite rules (based on registered post types, etc) during Trellis deploys
- [trellis-slack-webhook-notify-during-deploy
](https://github.com/ItinerisLtd/trellis-slack-webhook-notify-during-deploy) - Sends a deployment complete message to a Slack channel when Trellis deploys Bedrock
- [trellis_install_wp_cli_via_composer](https://github.com/ItinerisLtd/trellis_install_wp_cli_via_composer) - Install WP-CLI via composer on Trellis servers
- [tiller-circleci-orb](https://github.com/ItinerisLtd/tiller-circleci-orb/) - Deploy Trellis, Bedrock and Sage(optional) via CircleCI
- [trellis-cyberduck](https://github.com/ItinerisLtd/trellis-cyberduck) - Trellis commands for Cyberduck
- [trellis-matomo](https://github.com/E-VANCE/trellis-matomo) - Install the latest on-premise version of Matomo with Trellis
================================================
FILE: trellis/vault.md
================================================
---
date_modified: 2026-03-10 17:00
date_published: 2015-11-01 14:32
description: Enable Ansible Vault in Trellis to encrypt sensitive data in `vault.yml`. Store passwords, API keys, and confidential variables securely in version control.
title: Ansible Vault for Encrypting Secrets in Trellis
authors:
- ben
- fullyint
- Log1x
- MWDelaney
- mZoo
- swalkinshaw
- TangRufus
---
# Ansible Vault for Encrypting Secrets in Trellis
Some project variables contain sensitive data like passwords. Trellis keeps these variable definitions in separate files named `vault.yml`. We strongly recommend that you encrypt these `vault.yml` files using to avoid exposing sensitive data in your project repository.
vault.yml example
To briefly demonstrate what vault does, consider this example `vault.yml` file.
```yaml
# example vault.yml file -- unencrypted plain text
my_password: example_password
```
You should replace the `example_password` then encrypt the file with Ansible Vault before committing it to your repo. The data would be safe in your repo because the encrypted file would look like this:
```yaml
# example vault.yml file -- encrypted
$ANSIBLE_VAULT;1.1;AES256
343163646662643438323831343332626234333233386666333162383265663
3132306538383762336332376165383530633838643937320a6363343238643
363065366664316364646561613163653866623566303235666537343437643
6638363265383831390a6631663239373833636133623333666363643166383
6237663637353638653266616562616535623465636265316231613331 etc.
```
## Encrypt your vault files
```shell
$ trellis vault encrypt
```
::: danger
If you have unencrypted `vault.yml` files in your project's git history (e.g., passwords in plain text), you will most likely want to change the variable values in your `vault.yml` files before encrypting them and committing them to your repo.
:::
::: warning Don't forget your vault password
Trellis automatically generates a vault password for you at `trellis/.vault_pass` (this file **will not** be added to your Git repository), and adds a reference to it to the `ansible.cfg` file.
:::
Your Trellis commands will be exactly the same as before enabling vault, not requiring any extra flags.
### Adding additional vault files for encryption
```shell
$ trellis vault encrypt -f path/to/file.yml
```
## View an encrypted vault file
You can view a vault file in your terminal with the following command:
```shell
$ trellis vault view
```
## Edit an encrypted vault file
You can edit a vault file in your terminal with the following command:
```shell
$ trellis vault edit group_vars//vault.yml
```
## Other vault commands
`trellis-cli` provides a few basic commands that mirror with the official [Ansible Vault](https://docs.ansible.com/projects/ansible/latest/user_guide/vault.html) ones.
- `trellis vault encrypt `
- `trellis vault view `
- `trellis vault edit `
- `trellis vault decrypt ` -- Avoid using the `decrypt` command. If your intention is to view or edit an encrypted file, use the `view` or `edit` commands instead. Any time you decrypt a file, you risk forgetting to re-encrypt the file before committing changes to your repo.
Run `trellis vault` to see usage details.
## Working with vault variables
Here are a few tips for working with [variables and vault](https://docs.ansible.com/projects/ansible/latest/tips_tricks/ansible_tips_tricks.html#keep-vaulted-variables-safely-visible) in Trellis.
- Variables with sensitive data such as passwords are defined in files named `vault.yml`.
- Each environment has its own `vault.yml` file: `group_vars//vault.yml`.
- There is also one `vault.yml` file applicable to all environments: `group_vars/all/vault.yml`.
- Variables named with the `vault_` prefix are defined in the `vault.yml` files.
- To view or edit an encrypted `vault.yml` file, use either `trellis vault view ` or `trellis vault edit `. Avoid using the `decrypt` command. Any time you decrypt a file, you risk forgetting to re-encrypt the file before committing changes to your repo. You may want to employ a pre-commit hook ([example](https://reinteractive.com/articles/ansible-best-practices)) for added prevention.
## Sharing a project with vault-encrypted files
Your repo with vault-encrypted files is secure from anyone being able to see or use the sensitive data in the `vault.yml` files. To grant a colleague access to the data, you will need to give your colleague your vault password to use in repeating the two password steps in the [Steps to Enable Ansible Vault](#encrypt-your-vault-files) above. It is still recommended to always keep your project in a private repo.
## Disabling Ansible Vault
It is not recommended to disable Ansible Vault but you can disable it at any time. Simply run `ansible-vault decrypt `. If you then commit the unencrypted files to your repo, the sensitive data will be in your repo in plain text and will be difficult to remove from the git history. If you re-enable vault in the future, you may want to change all the sensitive data, encrypt with vault, then commit the revised and encrypted `vault.yml` files to your repo.
## Storing your password
Without your password, either entered as a string or stored in your `vault_password_file` file (usually `.vault_pass` and configured in the `ansible.cfg` file), you will not be able to access the encrypted files. The `vault_password_file` should not ever be publicly accessible, or committed to version control. It's a good practice to backup this file on another physical or virtual drive, ideally also encrypted.
## Access recovery
Should you lose access to your vault password, you you can either spin up a new server, or recreate or regenerate the `group_vars/(environment)/vault.yml` files and, on the servers, manually update the following to match new vault strings:
### admin root (sudo) password
```shell
$ sudo passwd admin
```
### root mysql password
```sql
UPDATE mysql.user SET Password=PASSWORD('password_in_vault_file') WHERE USER='root' AND Host='localhost';
flush privileges;
```
### WordPress database passwords
```sql
UPDATE mysql.user SET Password=PASSWORD('password_in_vault_file') WHERE USER='example_com' AND Host='localhost';
flush privileges;
```
## Additional resources
[ansible-toolkit](https://github.com/dellis23/ansible-toolkit#atk-git-diff) provides a `atk-git-diff` command that allows you to do a `git diff` on encrypted files.
================================================
FILE: trellis/wordpress-sites.md
================================================
---
date_modified: 2024-06-19 13:17
date_published: 2016-03-28 21:10
description: Configure WordPress sites in Trellis through `wordpress_sites.yml`. Define domains, SSL certificates, cache configuration, and host multiple WordPress sites.
title: Configuring WordPress Sites in Trellis
authors:
- ben
- dalepgrant
- fullyint
- Log1x
- mockey
- MWDelaney
- nathanielks
- nlemoine
- swalkinshaw
- TangRufus
---
# Configuring WordPress Sites in Trellis
Everything in Trellis is built around the concept of "sites". Each Trellis managed server (local virtual machine or remote server) can support one or more WordPress sites. Trellis will automatically configure everything needed to host a WordPress site such as databases, Nginx confs, folder directories, etc based on the site's configuration.
These sites are configured in YAML files for each environment such as `group_vars/development/wordpress_sites.yml`.
There are two components and places to configure sites:
- Basic settings in `group_vars/development/wordpress_sites.yml`
- Passwords/secrets in `group_vars/development/vault.yml`
::: tip Note
If you used Trellis CLI to create your project, the basic configuration settings
will already be set for your main site.
:::
## Site configuration
`wordpress_sites` is a top-level dictionary (object/hash) used to define all the sites you want. Here's an absolute bare-minimum site as an example for development:
```yaml
# group_vars/development/wordpress_sites.yml
wordpress_sites:
example.com:
site_hosts:
- canonical: example.test
local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
admin_email: admin@example.test
multisite:
enabled: false
ssl:
enabled: false
cache:
enabled: false
```
Each site is defined by a "key" (`example.com` in this case). Trellis uses the key internally as the name of the site and as a default value in a lot of variables. We recommend naming your sites after their domain so it's descriptive.
Nested under the name/key are the site specific configuration settings. You only need to define a variable/setting if you want to overwrite the default value which can be found below.
## Passwords/secrets
When you add or edit a site in `wordpress_sites.yml`, you also need to edit `vault.yml` for the accompanying site/key. `vault.yml` simplifies the use of the Ansible Vault encryption feature for specific files. You never want to include plain-text passwords in a Git repository so we make it easier to optionally encrypt the `vault.yml` file while leaving the normal settings separate. See [Vault](vault.md) for more information on this.
```yaml
# group_vars/development/vault.yml
vault_wordpress_sites:
example.com:
admin_password: admin
env:
db_password: example_dbpassword
```
Notice the matching site keys in both `wordpress_sites` and `vault_wordpress_sites` for `example.com` which ties together these site settings.
## Options
### Common
- `site_hosts` - List of hosts that Nginx will listen on. At least one is required. Each host item must specify a `canonical` host and may optionally specify a list of corresponding `redirects` (hosts). **Remember to set up DNS for every host listed.** You cannot use just an IP address.
```yaml
# minimum required
example.com:
site_hosts:
- canonical: example.com
# multiple hosts and redirects are possible
example.com:
site_hosts:
- canonical: example.com
redirects:
- www.example.com
- site.com
- canonical: example.co.uk
redirects:
- www.example.co.uk
```
- `local_path` - path targeting Bedrock-based site directory (*required*)
- `current_path` - symlink to latest release (default: `current`)
- `db_create` - whether to auto create a database or not (default: `true`)
- `composer_authentications` - Composer auth setup. Useful for configuring access to private repositories. See the [Composer Authentication docs](/trellis/docs/composer-authentication/) (optional)
- `ssl` - SSL options. See the [SSL docs](ssl.md)
- `multisite` - Multisite options. See the [Multisite docs](multisite.md)
- `cache` - Nginx FastCGI cache options. See the [Cache docs](fastcgi-caching.md)
- `h5bp` - Nginx config files from [h5bp server config](https://github.com/h5bp/server-configs-nginx) to include
- `cache_file_descriptors` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/directive-only/cache-file-descriptors.conf) (default: `not_dev`)
- `extra_security` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/directive-only/extra-security.conf) (default: `true`)
- `no_transform` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/directive-only/no-transform.conf) (default: `false`)
- `x_ua_compatible` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/directive-only/x-ua-compatible.conf) (default: `true`)
- `cache_busting` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/location/cache-busting.conf) (default: `false`)
- `cross_domain_fonts` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/location/cross-domain-fonts.conf) (default: `true`)
- `expires` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/location/expires.conf) (default: `false`)
- `protect_system_files` - See [h5bp server config](https://github.com/h5bp/server-configs-nginx/blob/2.0.0/h5bp/location/protect-system-files.conf) (default: `true`)
- `env` - environment variables
- `disable_wp_cron` - Disable WP cron and use system's (default: `true`)
- `wp_home` - `WP_HOME` constant (default: `://${HTTP_HOST}`)
- `wp_siteurl` - `WP_SITEURL` constant (default: `${WP_HOME}/wp`)
- `wp_env` - environment (default: `env` via Ansible)
- `db_name` - database name (default: `_`)
- `db_user` - database username (default: ``)
- `db_password` - database password (*required*, in `vault.yml`)
- `db_host` - database hostname (default: `localhost`)
- `db_prefix` - database table prefix (defaults to `wp_` if not set)
- `db_user_host` - hostname or ip range used to restrict connections to database (default: `localhost`)
### Development
- `site_install` - whether to install WordPress or not (default: `true`)
- `site_title` - WP site title (default: site name)
- `admin_user` - WP admin user name (default: `admin`)
- `admin_email` - WP admin email address (*required*)
- `admin_password` - WP admin user password (*required* in `vault.yml`)
- `initial_permalink_structure` - permalink structure applied at time of WP install (default: `/%postname%/`)
### Remote servers
- `repo` - URL of the Git repo of your Bedrock project (*required*)
- `repo_subtree_path` - relative path to your Bedrock/WP directory in your repo (above) if its not the root (like site/ in roots-example-project)
- `branch` - the branch name, tag name, or commit SHA1 you want to deploy (default: `master`)
- `env` - environment variables
- `auth_key` - Generate (*required* in `vault.yml`)
- `secure_auth_key` - Generate (*required* in `vault.yml`)
- `logged_in_key` - Generate (*required* in `vault.yml`)
- `nonce_key` - Generate (*required* in `vault.yml`)
- `auth_salt` - Generate (*required* in `vault.yml`)
- `secure_auth_salt` - Generate (*required* in `vault.yml`)
- `logged_in_salt` - Generate (*required* in `vault.yml`)
- `nonce_salt` - Generate (*required* in `vault.yml`)
- `runtime_writable_paths` - list of paths (relative to the site root) the PHP-FPM runtime user can write to when [runtime hardening](/trellis/docs/security/#wordpress-runtime-hardening) is enabled (default: global `wordpress_runtime_writable_paths`)
- `deploy_keep_releases` - number of releases to keep for rollbacks (default: 5)