Skip to content

Bedrock: The Complete Guide to Composer-Based WordPress Development

Emnes
Illustration of the Bedrock directory structure showing composer, config, .env, and web folders

For most WordPress developers, the first exposure to the Roots.io stack is Bedrock — a boilerplate that restructures a WordPress project so it behaves like a modern PHP application. Dependencies are managed with Composer, secrets live in a .env file that never touches version control, configuration is per-environment, and WordPress core is a composable dependency rather than a pile of files committed to Git.

The payoff for adopting Bedrock is enormous — if you’re working in a team, shipping regularly, or want a sensible security posture. The cost is a real learning curve, particularly if you’ve never used Composer or dotenv before. This guide walks you through everything you need to know about Bedrock — what it is, how it’s structured, how to install it, how the Composer workflow replaces the WordPress admin “Add Plugin” button, and how to deploy Bedrock projects to production.

By the end of this guide, you’ll be able to start a new Bedrock project, add plugins and themes via Composer, configure per-environment settings correctly, and understand the security and deployment implications of the Bedrock workflow. If you’re new to the Roots stack entirely, start with The Roots.io Stack Explained first; this post assumes that background.

What Bedrock Actually Is

Bedrock is an MIT-licensed WordPress boilerplate maintained by Roots.io. The current stable version is 1.30.1, released in March 2026. It’s not a framework, not a plugin, and not a replacement for WordPress — it’s a project template that applies Twelve-Factor App principles to WordPress.

The four principles Bedrock enforces:

  • Explicit dependencies. Every piece of third-party code — WordPress core, every plugin, every theme — is declared in composer.json. No drag-and-drop. No “I think we installed that last year.”
  • Separation of config from code. Secrets live in .env, not in wp-config.php. The same codebase can run on dev, staging, and production without modification — only the .env changes.
  • Production-safe defaults. File editing from the WordPress admin is disabled outside development. Auto-updates are off (you update through Composer). Debug output never leaks to visitors.
  • Improved directory structure. WordPress core lives in web/wp/ (its own subdirectory, never edited). Custom content — plugins, themes, mu-plugins, uploads — lives in web/app/, cleanly separated from core.

None of these are Bedrock-specific inventions. They’re how modern PHP applications have worked for a decade. Bedrock just applies them to WordPress without asking you to rewrite WordPress itself.

The Bedrock Directory Structure

Here’s what a fresh Bedrock project looks like after composer create-project:

my-site/
├── .env # per-environment secrets, git-ignored
├── .env.example # template committed to git
├── composer.json # dependency manifest
├── composer.lock # resolved versions, committed to git
├── config/
│ ├── application.php # main WP config (loaded by wp-config.php)
│ └── environments/
│ ├── development.php # dev-only overrides
│ ├── staging.php
│ └── production.php
├── vendor/ # composer-managed PHP dependencies
└── web/ # document root — point nginx here
    ├── app/ # custom wp-content replacement
    │ ├── mu-plugins/ # must-use plugins
    │ ├── plugins/ # standard plugins (composer-installed)
    │ ├── themes/ # themes (composer-installed or custom)
    │ └── uploads/ # media library (git-ignored)
    ├── wp-config.php # thin file that loads config/application.php
    ├── index.php # entry point
    └── wp/ # WordPress core, composer-managed

Three things to notice immediately:

  • The web root is web/, not the project root. This is a major security improvement — composer.json, .env, and vendor/ are above the web root and cannot be accessed via HTTP even if nginx misbehaves.
  • WordPress core is in web/wp/. You never edit files there. composer update roots/wordpress replaces it cleanly.
  • Custom content is in web/app/, not wp-content/. Bedrock redirects WordPress’s content constants so everything works, but plugins that hardcode wp-content/ paths can break (more on this below).

Installing Bedrock

You need Composer and PHP 8.3 or newer installed locally. With those in place:

composer create-project roots/bedrock my-site

That one command downloads Bedrock, installs the WordPress core dependency, pulls in the default theme (Twenty Twenty-Five as of April 2026), and scaffolds the project. After it finishes:

  • Copy .env.example to .env and fill in your database credentials, site URL (WP_HOME), and WordPress auth keys. The Bedrock README links to a salt generator; any cryptographically-random 64-character strings work.
  • Point nginx (or Apache) at web/, not the project root.
  • Create the database specified in .env.
  • Visit the site URL — WordPress runs the standard installer.

Total time from zero to working WordPress on a fresh machine: about 10 minutes, most of it waiting for Composer to download dependencies.

The .env File — Where Your Secrets Live

.env is a simple key=value file loaded by vlucas/phpdotenv before WordPress bootstraps. A typical Bedrock .env contains:

DB_NAME=example_db
DB_USER=example_user
DB_PASSWORD=your_db_password
DB_HOST=localhost

WP_ENV=development
WP_HOME=http://example.test
WP_SITEURL=${WP_HOME}/wp

AUTH_KEY='...'
SECURE_AUTH_KEY='...'
LOGGED_IN_KEY='...'
NONCE_KEY='...'
AUTH_SALT='...'
SECURE_AUTH_SALT='...'
LOGGED_IN_SALT='...'
NONCE_SALT='...'

Critical rules:

  • .env is never committed to git. The Bedrock .gitignore excludes it by default. If you accidentally commit one, rotate every secret immediately — assume the entire secret surface is compromised.
  • .env.example is committed and serves as a template. New developers copy it to .env and fill in values.
  • Different environments get different .env files. Your laptop has one; staging has another; production has yet another. None of them touch each other.
  • WP_ENV drives the environment-specific config. Set it to development, staging, or production and Bedrock loads the matching file from config/environments/.

Per-Environment Configuration

Inside config/environments/, each environment has its own PHP file that overrides settings. A typical development.php:

<?php
Config::define('WP_DEBUG', true);
Config::define('WP_DEBUG_DISPLAY', true);
Config::define('WP_DEBUG_LOG', true);
Config::define('SAVEQUERIES', true);
Config::define('DISALLOW_FILE_MODS', false);

And the opposite in production.php:

<?php
Config::define('WP_DEBUG', false);
Config::define('WP_DEBUG_DISPLAY', false);
Config::define('DISALLOW_FILE_MODS', true);
Config::define('DISALLOW_FILE_EDIT', true);
Config::define('AUTOMATIC_UPDATER_DISABLED', true);

These are sensible defaults for each environment. Development gets debug output and file modifications enabled (so you can install plugins from the admin UI while prototyping); production disables everything that could leak information or let an attacker modify code through a compromised account.

You can add any WordPress constant to these files. Common additions: WP_MEMORY_LIMIT, WP_POST_REVISIONS, EMPTY_TRASH_DAYS, cache driver settings, multisite configuration.

Installing Plugins and Themes with Composer

This is the biggest workflow shift for most new Bedrock users. Instead of installing plugins via Plugins → Add New in the WordPress admin, you install them via Composer.

Free Plugins from WordPress.org

Bedrock 1.30+ ships with WP Packages as the default Composer repository for WordPress.org plugins and themes. To install a plugin:

composer require wp-plugin/wordpress-seo
composer require wp-plugin/wp-mail-smtp:^4.3
composer require wp-theme/twentytwentyfive

Each command adds the package to composer.json, resolves its version, downloads the files into web/app/plugins/, and pins the exact version in composer.lock. Committing composer.json and composer.lock to git gives every developer and every environment the same version of every plugin.

Older projects may still use WPackagist (wpackagist-plugin/*, wpackagist-theme/*). Both work; WP Packages is the newer, more actively maintained option.

Premium and Private Plugins

Premium plugins — ACF Pro, Gravity Forms, WooCommerce premium extensions — aren’t on WordPress.org, so they need their own Composer repositories. The four common options:

  • Vendor-provided Composer repositories, like ACF Pro’s composer.advancedcustomfields.com — point composer.json at the vendor’s URL, add a license key, and composer require just works.
  • Paid aggregators like Deliciousbrains or Packagist premium mirrors.
  • Private Satis repositories you host yourself, serving your agency’s purchased plugins.
  • Raw zip URLs via composer/installers and a package-type repository entry — fragile but works for one-off plugins.

Post 5 in this series goes deep on managing premium plugins with Bedrock.

The Must-Use Plugin Autoloader

Bedrock ships with roots/bedrock-autoloader, a must-use plugin that lets regular plugins behave as must-use plugins without being in the mu-plugins/ folder. Drop a file in web/app/mu-plugins/ and its main plugin file gets auto-loaded on every request, skipping the admin UI.

Version 1.1.0 (shipped with Bedrock 1.30) added two features worth knowing:

  • Plugin filtering — exclude specific plugins from auto-loading even if they’re in mu-plugins/.
  • Symlink handling — Docker volumes and local development setups that use symlinks no longer confuse the autoloader.

Deploying a Bedrock Site to Production

A production Bedrock deploy is just three ingredients: the code, a .env file with production secrets, and composer install run on the server.

The canonical deploy workflow:

  • Clone the git repository to a release directory.
  • Run composer install --no-dev --optimize-autoloader to resolve dependencies without dev tools (Pint, Pest) and with optimized autoloading.
  • Symlink the shared .env file — production .env lives outside the release directory and is symlinked in, so deploys don’t overwrite secrets.
  • Symlink the shared uploads/ directory — media doesn’t belong in git, so it’s stored once and symlinked into each release.
  • Flip the current/ symlink to point at the new release directory.
  • Optionally, flush caches (Redis, OPcache, page cache, CDN).

This is exactly what Trellis does automatically. You can also do it by hand, with Deployer, GitHub Actions, or any other CI/CD tool.

Multisite, Pressed, and Other Advanced Configurations

Bedrock supports WordPress multisite out of the box. Enable it by adding to .env:

WP_ALLOW_MULTISITE=true
MULTISITE=true
SUBDOMAIN_INSTALL=false
DOMAIN_CURRENT_SITE=example.com
PATH_CURRENT_SITE=/

Bedrock’s application.php reads these from .env and defines the corresponding constants. For background on when multisite is the right choice, see our WordPress Multisite guide.

Common Pitfalls and How to Avoid Them

1. Clients Installing Plugins via Admin

The single most common Bedrock problem: a client (or well-meaning developer) installs a plugin via the WordPress admin, it works locally, and then the next deploy wipes it because it’s not in composer.json.

Solutions:

  • Enable DISALLOW_FILE_MODS in production (default in Bedrock production config). This hides the “Add Plugin” button entirely.
  • Train the team. Everyone with admin access needs to know “plugin installs go through the developer.”
  • Add a pre-deploy hook that warns if web/app/plugins/ on the server contains plugins not in composer.json.

2. Plugins That Hardcode wp-content/

A minority of plugins hardcode the string wp-content instead of using the WP_CONTENT_DIR constant. These break under Bedrock’s web/app/ structure. The Roots blog post “WordPress Plugins That Assume Your Directory Structure” (March 2026) covers this in detail. Typical fix: file an issue with the plugin vendor, or patch the plugin with a Composer post-install script.

3. Forgetting to Run composer install on the Server

A deploy that just rsyncs code without running Composer leaves the vendor/ directory stale and misses plugin updates. Always run composer install as part of every deploy.

4. PHP Version Drift

Local development on PHP 8.3 and production on PHP 8.1 means Composer will resolve dependencies that crash on the server. Bedrock 1.29+ requires PHP 8.3 minimum — if your production server is older, upgrade before upgrading Bedrock.

5. Committing .env

It happens. A developer copies .env.example to .env, fills it in, and accidentally stages it. If you catch it before pushing, git reset is fine; if it’s on a remote, assume every secret is compromised and rotate everything.

Bedrock Security Posture

Bedrock gives you a significantly stronger security foundation than vanilla WordPress:

  • Secrets above the web root. .env, vendor/, and config files cannot be served via HTTP even if nginx is misconfigured.
  • WordPress core in a subdirectory, never edited. Core updates are a single composer update.
  • File modifications disabled in production. An attacker who compromises an admin account cannot edit plugin or theme files.
  • Auto-updates disabled. The 2025 GiveWP incident, where an auto-updated plugin release broke thousands of sites, is exactly why this is a default. Update through Composer on your own schedule.
  • Automatic index-of-directory prevention. bedrock-disallow-indexing adds X-Robots-Tag headers and an .htaccess equivalent to prevent directory listings.
  • Composer-native security advisories. With WP Sec Adv installed, composer audit flags known-vulnerable plugins and themes.

None of this replaces a real security practice (2FA, rate limiting, WAF — see our login security guide), but it raises the floor significantly.

Is Bedrock Right for Your Project?

Bedrock is the right choice when:

  • You have more than one developer on the project.
  • You deploy more than once a month.
  • You need staging and production parity.
  • You’re comfortable with Composer and command-line workflows.
  • You control the production server (VPS or managed host that supports Composer).

Skip Bedrock when:

  • The site is a five-page brochure that won’t change.
  • Your client installs plugins themselves and can’t be trained out of it.
  • Your host doesn’t support Composer or SSH (cheap shared hosting).
  • Your team has no Composer experience and no time to learn.

Frequently Asked Questions

Does Bedrock work with managed WordPress hosts?

Yes, on several major hosts. Kinsta officially supports Bedrock with a documented deploy workflow. WP Engine supports it with some configuration. Pantheon has historically been harder. Cheap shared hosts (GoDaddy, Bluehost default plans) often can’t run Composer server-side and aren’t a good fit.

Can I migrate an existing WordPress site to Bedrock?

Yes. Post 4 in this series covers the migration process in detail. Briefly: start a fresh Bedrock project, port your theme and custom code, add every plugin via Composer, import the database, migrate media files, and switch DNS.

What PHP version does Bedrock require?

Bedrock 1.29+ requires PHP 8.3 or higher. Older versions support down to PHP 8.1, but you should not start a new project on an EOL PHP version.

How does Bedrock handle WordPress core updates?

WordPress core is a Composer package (roots/wordpress). Update with composer update roots/wordpress, test locally, commit the resulting composer.lock, and deploy. No “click update in the admin” workflow — that’s disabled in production.

Can I use Bedrock without Sage or Trellis?

Absolutely. Bedrock is standalone. You can run Bedrock with any theme (including block themes and Underscores-based themes) and deploy it with any tool. Sage and Trellis are complementary but not required.

What’s the difference between Bedrock and Radicle?

Bedrock is a lightweight, unopinionated boilerplate. Radicle is a paid, opinionated starter that combines Bedrock + Sage + Acorn + curated mu-plugins in a Laravel-style layout. Use Bedrock when you want to pick every piece yourself; use Radicle when you want everything pre-wired.

Start a Bedrock Project Today

If you’ve never worked with Bedrock before, spin up a throwaway project to get a feel for the workflow:

composer create-project roots/bedrock /tmp/bedrock-test
cd /tmp/bedrock-test
cp .env.example .env
# edit .env with test DB credentials
composer require wp-plugin/query-monitor
php -S localhost:8000 -t web

Ten minutes end-to-end, and you’ll have working Composer-managed WordPress with a plugin installed and locked to a specific version.

At Emnes, every production WordPress site we run — including this one — is built on Bedrock. If you’re evaluating Bedrock for your team, get in touch and we can share how we handle deploys, CI, and premium plugin management across 18 sites.

Next in this series: The Roots.io Stack Explained, what’s new in the ecosystem in 2025–2026, and upcoming posts on migrating existing sites to Bedrock and managing premium plugins with Composer.