5 min read

WordPress Bedrock Setup: Production-Ready WordPress with Roots.io

Implementing twelve-factor app methodology with Bedrock for secure, scalable WordPress deployments using environment variables and modern folder structure

WordPress Bedrock Setup: Production-Ready WordPress with Roots.io

If you’ve ever deployed a traditional WordPress site and felt a nagging sense of unease — config files with hardcoded database credentials, plugins committed directly to your repo, no clear separation between environments — you’re not alone. WordPress is powerful, but its default project structure wasn’t designed with modern deployment workflows in mind.

Enter Bedrock by Roots.io. Bedrock is a WordPress boilerplate that applies the twelve-factor app methodology to WordPress, giving you a clean folder structure, dependency management via Composer, and proper use of environment variables for configuration. It’s, quite simply, how WordPress should be set up for any project you care about.

In this post, we’ll walk through setting up a Bedrock project from scratch, explore the folder structure that makes it special, and show you how to configure it for production-ready deployments.


Why Bedrock? The Problems It Solves

Traditional WordPress lumps everything together. Your application code, your content, your configuration, and your dependencies all live in one tangled directory. This creates real problems:

  • Security risks: The wp-config.php file (with your database password) sits in the web root.
  • No dependency management: Plugins and themes are often committed directly to version control or installed manually via the admin panel.
  • Environment coupling: Switching between local, staging, and production means editing config files or maintaining multiple config branches.
  • Deployment headaches: There’s no clean way to do automated, repeatable deployments.

Bedrock addresses every single one of these issues. It restructures WordPress so the web root is isolated, configuration is driven by environment variables, and all dependencies — including WordPress core itself — are managed through Composer. It’s a proper twelve-factor app approach applied to a CMS that predates the concept by years.


Setting Up Bedrock From Scratch

Prerequisites

You’ll need PHP 8.0+, Composer, and a local development environment (Local by Flywheel, DDEV, Lando, or even a simple php -S setup will work for testing).

Creating Your Project

Getting started with Bedrock is a single Composer command:

composer create-project roots/bedrock my-wordpress-site
cd my-wordpress-site

That’s it. You now have a fully structured WordPress project. Let’s look at what Composer just gave you:

my-wordpress-site/
├── config/
│   ├── application.php    # Main config (replaces wp-config.php)
│   └── environments/
│       ├── development.php
│       ├── staging.php
│       └── production.php
├── web/
│   ├── app/               # wp-content equivalent
│   │   ├── mu-plugins/
│   │   ├── plugins/
│   │   ├── themes/
│   │   └── uploads/
│   ├── wp/                # WordPress core (managed by Composer)
│   ├── index.php
│   └── wp-config.php      # Minimal bootstrap, points to config/
├── vendor/
├── .env                   # Your environment variables
├── .env.example
├── composer.json
└── wp-cli.yml

Notice something crucial: the web/ directory is your document root, not the project root. This means files like .env, composer.json, and your config/ directory are never accessible from the browser. That alone is a massive security improvement.

WordPress core lives inside web/wp/ and is treated as a dependency. You never edit it. You never commit it. Composer manages it.

Configuring Environment Variables

Here’s where the twelve-factor app philosophy really shines. Open up the .env file:

# .env - This file should NEVER be committed to version control

DB_NAME='my_wordpress_db'
DB_USER='root'
DB_PASSWORD='secret'
DB_HOST='127.0.0.1'
DB_PREFIX='wp_'

WP_ENV='development'
WP_HOME='https://my-wordpress-site.test'
WP_SITEURL="${WP_HOME}/wp"

# Generate these at https://roots.io/salts.html
AUTH_KEY='generateme'
SECURE_AUTH_KEY='generateme'
LOGGED_IN_KEY='generateme'
NONCE_KEY='generateme'
AUTH_SALT='generateme'
SECURE_AUTH_SALT='generateme'
LOGGED_IN_SALT='generateme'
NONCE_SALT='generateme'

Every secret, every environment-specific value, lives here. Your config/application.php reads from these variables using the env() helper. Let’s look at a snippet of how that works:

// config/application.php

<?php

use Roots\WPConfig\Config;
use function Roots\env;

/**
 * Directory containing all of the site's files
 */
$root_dir = dirname(__DIR__);

/**
 * Document Root
 */
$webroot_dir = $root_dir . '/web';

/**
 * Database settings
 */
Config::define('DB_NAME', env('DB_NAME'));
Config::define('DB_USER', env('DB_USER'));
Config::define('DB_PASSWORD', env('DB_PASSWORD'));
Config::define('DB_HOST', env('DB_HOST') ?: 'localhost');
Config::define('DB_CHARSET', 'utf8mb4');
Config::define('DB_COLLATE', '');
$table_prefix = env('DB_PREFIX') ?: 'wp_';

/**
 * URLs
 */
Config::define('WP_HOME', env('WP_HOME'));
Config::define('WP_SITEURL', env('WP_SITEURL'));

/**
 * Custom content directory
 */
Config::define('CONTENT_DIR', '/app');
Config::define('WP_CONTENT_DIR', $webroot_dir . Config::get('CONTENT_DIR'));
Config::define('WP_CONTENT_URL', Config::get('WP_HOME') . Config::get('CONTENT_DIR'));

The per-environment config files let you layer on environment-specific settings. For example, your production config tightens things down:

// config/environments/production.php

<?php

use Roots\WPConfig\Config;

Config::define('WP_DEBUG', false);
Config::define('WP_DEBUG_DISPLAY', false);
Config::define('WP_DEBUG_LOG', false);
Config::define('SCRIPT_DEBUG', false);
Config::define('DISALLOW_FILE_MODS', true);  // No plugin installs from admin
Config::define('DISALLOW_FILE_EDIT', true);   // No theme editor

That DISALLOW_FILE_MODS setting is critical for production. It prevents anyone from installing or updating plugins through the WordPress admin. All changes go through Composer and your deployment pipeline. This is proper, disciplined release management.


Managing Dependencies with Composer

This is where Bedrock transforms the WordPress development experience. Want to install a plugin? Don’t download a ZIP file. Don’t click around the admin dashboard. Use Composer:

# Install plugins from WordPress Packagist (wpackagist)
composer require wpackagist-plugin/advanced-custom-fields
composer require wpackagist-plugin/wp-mail-smtp
composer require wpackagist-plugin/redis-cache

# Install a specific theme
composer require wpackagist-theme/flavor

# Install premium plugins from private repositories
# (after adding the repo to composer.json)
composer require advanced-custom-fields/advanced-custom-fields-pro

Bedrock includes WordPress Packagist as a Composer repository by default. This mirrors the entire WordPress.org plugin and theme directories as Composer packages.

Your composer.json becomes the single source of truth for your project’s dependencies. Every plugin, every theme, and WordPress core itself are versioned, locked, and reproducible. Run composer install on any machine and you get an identical setup. This is fundamental to the twelve-factor app principle of maintaining strict separation between your build, release, and run stages.


Deploying to Production

Because Bedrock is structured around environment variables and Composer, deployment becomes predictable. Whether you’re using a platform like Laravel Forge, a CI/CD pipeline with GitHub Actions, or a tool like Trellis (also from Roots.io), the process is straightforward:

  1. Push your code to your repository (no secrets, no dependencies committed).
  2. On the server, composer install --no-dev --optimize-autoloader.
  3. Set your environment variables on the server (via .env file, server config, or secrets manager).
  4. Point your web server’s document root to the web/ directory.

Here’s a minimal Nginx configuration for Bedrock:

server {
    listen 443 ssl http2;
    server_name example.com;

    # Document root points to web/ — not the project root
    root /srv/my-wordpress-site/web;
    index index.php index.html;

    # Deny access to sensitive files that shouldn't be
    # reachable even if they somehow end up in web/
    location ~ /\. {
        deny all;
    }

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to WordPress core files you don't want exposed
    location ~* /(?:wp-config\.php|readme\.html|license\.txt) {
        deny all;
    }
}

Because the document root is web/, your .env file, composer.json, vendor/ directory, and config/ directory are physically outside the web-accessible path. Even if something goes wrong with your PHP processing, those files can’t be served to a browser.


Conclusion: The WordPress Setup You’ll Never Go Back From

Once you’ve built a project with Bedrock, traditional WordPress setups feel reckless. The isolated web root, Composer-managed dependencies, environment-driven configuration, and per-environment settings give you a foundation that’s secure, reproducible, and ready for modern deployment workflows.

Here’s what I’d recommend as your next steps:

  1. Spin up a local Bedrock project using the commands above and explore the folder structure.
  2. Explore Trellis — the companion tool from Roots.io that handles server provisioning and deployment for Bedrock projects.
  3. Look into Sage, the Roots.io starter theme, which brings a modern front-end build pipeline (with Bud, Blade templates, and Tailwind CSS) to complement Bedrock’s back-end improvements.
  4. Set up a CI/CD pipeline that runs composer install and deploys automatically on merge — you have the structure for it now.

The twelve-factor app methodology exists because decades of hard-won deployment experience taught us that configuration belongs in the environment, dependencies should be explicitly declared, and your codebase should be deployable anywhere without modification. Bedrock brings all of that to WordPress. Use it.