Introduction
WordPress powers nearly half the web, yet most teams deploy it as a tangle of loose files scattered across web roots, database credentials buried in wp-config.php, and plugin versions that drift across environments. When you scale beyond a single site or move beyond hobby projects, that friction compounds: deployments become risky, onboarding takes hours, and separating code from configuration becomes nearly impossible. Bedrock and modern WordPress stacks: a step-by-step guide shows you how to reclaim control by treating WordPress like a professional application.
Bedrock is a roots-managed WordPress boilerplate that gives you Composer-based dependency management, environment-driven configuration, and a sensible folder structure—the foundations that mature development teams expect. Paired with infrastructure tools like Trellis or containerized setups, it bridges the gap between WordPress's conventional simplicity and the rigor needed for production systems handling real traffic and real stakes.
If you've spent evenings SSH-ing into servers to manually update plugins or debugging why staging behaves differently than production, this is the article that changes your workflow.
Composer-managed dependencies
The traditional WordPress setup mixes third-party code with first-party code: plugins and themes live as separate file trees, versions are tracked inconsistently, and there's no reliable way to declare "this site needs plugin X at version 2.5.1 and theme Y at 1.8." You end up with git history that's either bloated with vendor files or missing critical dependencies entirely.
Bedrock flips that model by treating WordPress, plugins, and themes as Composer packages. Your entire project—core, extensions, and dependencies—lives in a single composer.json file that acts as a source of truth.
{
"name": "myteam/client-site",
"type": "project",
"require": {
"php": ">=8.0",
"roots/wordpress": "6.4.1",
"wpackagist-plugin/akismet": "5.2",
"wpackagist-plugin/wpforms-lite": "1.8.5",
"wpackagist-theme/twentytwentyfour": "1.0"
},
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org"
}
],
"extra": {
"wordpress-install-dir": "web/wp",
"wordpress-content-dir": "web/app"
}
}
When a team member clones the repo and runs composer install, they get the exact same WordPress version, the exact same plugins, the exact same theme. No surprises. No "works on my machine" excuses. Deployments become deterministic: pull, run Composer, database migrations if needed, done.
For commercial plugins (those you purchase), you add a separate Composer repository—often a private Packagist instance or a custom VCS repository—so licensing and version control remain intact without committing licensed code to git.
Environment variables via .env
Hardcoding database credentials, API keys, or environment-specific settings into wp-config.php is a security liability and a deployment nightmare. Different environments (local, staging, production) need different values, but your code repository should be identical across all of them.
Bedrock ships with support for PHP dotenv, a library that reads a .env file at the project root and hydrates $_ENV, $_SERVER, and getenv() calls. Your wp-config.php becomes a simple, environment-agnostic template:
<?php
/**
* Configuration managed via environment variables
*/
$root_dir = dirname(__DIR__);
$webroot_dir = $root_dir . '/web';
require_once $root_dir . '/vendor/autoload.php';
require_once $root_dir . '/vendor/roots/wordpress-core-installer/src/Installer.php';
Dotenv\Dotenv::createImmutable($root_dir)->load();
// Database settings read from .env
define('DB_NAME', $_ENV['DB_NAME'] ?? '');
define('DB_USER', $_ENV['DB_USER'] ?? '');
define('DB_PASSWORD', $_ENV['DB_PASSWORD'] ?? '');
define('DB_HOST', $_ENV['DB_HOST'] ?? 'localhost');
// Disable file editing in production
define('DISALLOW_FILE_EDIT', $_ENV['ENVIRONMENT'] === 'production');
// Debug settings per environment
define('WP_DEBUG', $_ENV['WP_DEBUG'] === 'true');
define('WP_DEBUG_LOG', $root_dir . '/logs/debug.log');
// Security salts and keys
define('AUTH_KEY', $_ENV['AUTH_KEY'] ?? 'put your unique phrase here');
define('SECURE_AUTH_KEY', $_ENV['SECURE_AUTH_KEY'] ?? 'put your unique phrase here');
// ... more defines
$table_prefix = $_ENV['TABLE_PREFIX'] ?? 'wp_';
// Standard WordPress setup
if (!defined('ABSPATH')) {
define('ABSPATH', $webroot_dir . '/wp/');
}
require_once ABSPATH . 'wp-settings.php';
Your .env file—which never goes into version control—looks like:
DB_NAME=mysite_prod
DB_USER=prod_user
DB_PASSWORD=<strong-password>
DB_HOST=10.0.1.42
ENVIRONMENT=production
WP_DEBUG=false
TABLE_PREFIX=wp_
AUTH_KEY=your-random-key-here
SECURE_AUTH_KEY=another-random-key
// ... etc
On local, you have a different .env with localhost credentials. On staging, another. The code is identical; only the environment varies. This practice becomes critical when you add CI/CD pipelines—your deploy script injects the right .env per environment without touching code.
Better folder structure
Standard WordPress places everything under a single web root: plugins at wp-content/plugins, themes at wp-content/themes, uploads at wp-content/uploads. There's no separation between managed code and generated artifacts, and your entire codebase sprawls horizontally.
Bedrock reorganizes the layout for clarity and security:
project-root/
├── composer.json
├── .env
├── .env.example
├── .gitignore
├── web/ # Web-accessible directory
│ ├── app/ # Equivalent to wp-content
│ │ ├── plugins/
│ │ ├── themes/
│ │ └── mu-plugins/ # Must-use plugins
│ ├── wp/ # WordPress core (managed by Composer)
│ ├── .htaccess
│ └── index.php
├── vendor/ # Composer dependencies
├── config/
│ ├── application.php # Custom config hooks
│ └── environments/
│ ├── development.php
│ ├── staging.php
│ └── production.php
├── logs/
└── storage/ # Writable directory for backups, cache
The advantages compound quickly. Your WordPress core lives in web/wp and is entirely managed by Composer—you never manually touch it. Your own code (custom themes, must-use plugins, application-specific logic) sits separately in web/app or alongside the project root. Generated uploads go to web/app/uploads, which your deploy process can exclude from version control and back up separately.
Most importantly, only the web/ directory is exposed to the web server. The project root, config files, vendor, and logs remain outside the web root—safer by default. If someone compromises the web server, they can't access your Composer lock file or configuration.
Pair with Trellis or Docker
Bedrock is the codebase foundation, but modern workflows need infrastructure-as-code too. Two popular approaches complement Bedrock: Trellis (an Ansible playbook for servers) and Docker (containerized local/CI environments).
Trellis approach
Trellis is a Roots-maintained Ansible provisioning framework that orchestrates Ubuntu servers for Bedrock sites. You define your server configuration, SSL certificates, database setup, and deployment strategy in YAML:
---
- name: Configure web server for example.com
hosts: web
vars:
site_env: production
site_repos:
- repo: git@github.com:myteam/bedrock-site.git
branch: main
sites:
example.com:
ssl: letsencrypt
multisite: false
roles:
- wordpress
- certbot
- deploy
When you push code to your main branch, Trellis automatically runs composer install, clears caches, runs database migrations, and restarts services—all defined in code, auditable, reproducible across team members and deployments.
Docker approach
For teams already invested in Docker, Bedrock works seamlessly in containers. A local development setup might use Docker Compose with separate services for PHP, MySQL, Redis, and Nginx:
version: '3.8'
services:
php:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/app
environment:
- DB_HOST=mysql
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
volumes:
- db_data:/var/lib/mysql
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./web:/app/web
- ./nginx.conf:/etc/nginx/nginx.conf:ro
volumes:
db_data:
Developers run docker-compose up, wait for services to stabilize, and they have a full local environment matching production topology. CI/CD pipelines run the same containers, ensuring that what passes tests locally will behave identically in CI. Deployments can push pre-built images, reducing runtime surprises.
Common mistakes
Committing .env to version control. It's the most common error and the most dangerous. Your .env file contains secrets. Always add it to .gitignore, and commit a .env.example with placeholder values so new team members know what variables to set.
Ignoring composer.lock in version control. The opposite mistake: while .env must never be committed, composer.lock must always be committed. It locks every dependency to an exact version, ensuring deterministic installs. Without it, composer install might fetch different versions at different times, breaking reproducibility.
Mixing Bedrock structure with loose plugins. It's tempting to manually clone a plugin from GitHub into web/app/plugins and commit it. Resist that urge. Add it to composer.json, or if it's a custom private plugin, create a VCS repository and add that. Loose files fragment your dependency story.
Not automating deployments. Setting up Bedrock, environment variables, and Composer only pays off if you automate the deployment pipeline. Manual SSH commands defeat the entire purpose. Invest in a basic CI/CD workflow (GitHub Actions, GitLab CI, even a simple shell script) that runs composer install, clears caches, and verifies the site is up before calling the deploy successful.
Overlooking security in .env injection. When you deploy, ensure your CI/CD platform securely injects the .env file from encrypted secrets, and never log its contents. If you use Trellis or Docker, keep your secrets in encrypted CI/CD provider vaults, not hardcoded in playbooks or Dockerfiles.
Key takeaways
- Bedrock replaces WordPress's ad-hoc folder structure with a professional layout: WordPress core in
web/wp(Composer-managed), content inweb/app, and configuration at the project root. - Composer-managed dependencies—via composer.json and wpackagist.org—ensure every team member and every environment uses identical versions of WordPress, plugins, and themes.
- Environment variables via .env files separate configuration from code, enabling secure multi-environment deployments without touching your repository.
- Pair Bedrock with Trellis (for managed servers) or Docker (for containerized development and CI) to automate infrastructure, making deployments reproducible and auditable.
- The biggest wins come when you combine all four: Bedrock's structure, Composer's determinism, environment-driven config, and automated deployment pipelines. Partial adoption leaves friction in place.
- Commit composer.lock and .env.example, never commit .env or vendor/ (it's regenerated during deploy).
- The upfront investment in this setup (a few hours to scaffold a project, integrate CI/CD, document local setup) pays dividends immediately: deployments stop being nerve-wracking, onboarding new developers drops from hours to minutes, and debugging environment-specific issues becomes rare.






Comments
Loading comments…