WordPress Development Isn’t What It Used to Be — Here’s My Stack

Over the years, I’ve dialed in a WordPress stack that’s modern, fast, and production-ready — designed for flexibility, easy collaboration, and repeatable success across every project. Whether I’m working on a small business site or a complex custom build, this setup helps me stay efficient and focused on performance.
🧱 Foundation: Bedrock
I start every site with Bedrock, which brings a modern structure to WordPress with:
- Environment-based configuration
- Composer-based dependency management
- Cleaner folder organization and better security
It makes managing code and deployments way more predictable and scalable.
💻 Local Development with DDEV
All development starts locally in DDEV, which gives me a consistent, containerized environment across projects.
Once a site is live, I can pull the latest database and uploads from production with a single command:
ddev pull prod
That command is configured in .ddev/providers/prod.yaml
, allowing me to automate syncing without manual SSH work. Here’s what it looks like — but for this to work, you’ll also need to create a .env
file at .ddev/.env.web
with the following variables:
PROD_SSH_HOST=
PROD_SSH_USER=
PROD_FILES_DIR=
PROD_BACKUP_DIR=
PROD_DB_NAME=
PROD_DB_HOST=
PROD_DB_PORT=3306
PROD_DB_USER=
PROD_DB_PASSWORD=
And here's the contents of the prod.yaml
config:
environment_variables:
dburl: ${PROD_SSH_USER}@${PROD_SSH_HOST}:${PROD_BACKUP_DIR}/${PROD_DB_NAME}.sql.gz
filesurl: ${PROD_SSH_USER}@${PROD_SSH_HOST}:${PROD_BACKUP_DIR}/uploads.tar.gz
auth_command:
command: |
set -eu -o pipefail
ssh-add -l >/dev/null || ( echo "Please 'ddev auth ssh' before running this command." && exit 1 )
service: web
db_pull_command:
command: |
set -eu -o pipefail
ssh ${PROD_SSH_USER}@${PROD_SSH_HOST} "mariadb-dump -u ${PROD_DB_USER} -p${PROD_DB_PASSWORD} ${PROD_DB_NAME} | gzip > ${PROD_BACKUP_DIR}/${PROD_DB_NAME}.sql.gz"
rsync -az "${dburl}" /var/www/html/.ddev/.downloads/db.sql.gz
ddev import-db --src=/var/www/html/.ddev/.downloads/db.sql.gz
ssh ${PROD_SSH_USER}@${PROD_SSH_HOST} "rm -f ${PROD_BACKUP_DIR}/${PROD_DB_NAME}.sql.gz"
service: web
files_pull_command:
command: |
set -eu -o pipefail
echo "Syncing files from remote server..."
rsync -avz --delete --exclude=".gitkeep" ${PROD_SSH_USER}@${PROD_SSH_HOST}:${PROD_FILES_DIR}/ /var/www/html/web/app/uploads/ || { echo 'Failed to sync files'; exit 1; }
service: web
This gives me a painless way to pull fresh production data into my local dev environment.
✨ Hosting & Deployments
- Hosting: DigitalOcean droplets
- Server Management: RunCloud
- Deployments: Atomic deployments triggered by pushes to GitLab — I use
main
as the production branch anddevelop
for active development - DNS & Security: Managed through Cloudflare
- Media Clouding: DigitalOcean Spaces
- Caching: Redis, for fast and efficient object caching
- Monitoring: Uptime Kuma (self-hosted on a 2011 Mac mini at my house) connected to Pushover for instant downtime alerts
Everything is optimized for performance, scalability, and ease of use.
🔌 Core Plugin Stack
These are my must-haves for nearly every project:
- Advanced Custom Fields Pro — for flexible content modeling and block editor enhancements
- Gravity Forms — powerful and extensible form builder
- WP Environments — environment-aware development inside the WP admin
- The SEO Framework — lightweight SEO that doesn’t get in the way
- Media Offload — automatically pushes uploads to DigitalOcean Spaces (works great alongside EWWW Image Optimizer with built-in compatibility settings)
- EWWW Image Optimizer — compresses and converts images to WebP for faster load times
- Flying Scripts — delays JavaScript execution for better performance scores
- Mailgun — transactional email delivery
- Google Site Kit — quick integration for Analytics, Search Console, and more
All plugins sourced from the WPackagist repository are managed via Composer. For custom or premium plugins, I include them directly in the repository so everything is version-controlled and deployable.
🧩 MU Plugins & Customizations
I use the default MU plugins that come with Bedrock, along with a few custom ones I’ve built for:
- Branding the login screen
- Disallowing specific plugins
- Disabling email notifications for core/plugin/theme updates
These help enforce consistency and reduce client-side clutter.
🔐 Media Offload Configuration
Here’s an example of how I configure Media Cloud credentials within config/application.php
:
/** DigitalOcean Spaces */
// @link https://support.mediacloud.press/articles/advanced-usage/environment-variables/cloud-storage-variables/
Config::define( 'MCLOUD_STORAGE_PROVIDER', 'do' );
Config::define( 'MCLOUD_STORAGE_S3_ACCESS_KEY', '' );
Config::define( 'MCLOUD_STORAGE_S3_SECRET', '' );
Config::define( 'MCLOUD_STORAGE_S3_BUCKET', '' );
Config::define( 'MCLOUD_STORAGE_S3_ENDPOINT', 'nyc3.digitaloceanspaces.com' );
Config::define( 'MCLOUD_STORAGE_PREFIX', '@{date:Y/m}' );
Config::define( 'MCLOUD_STORAGE_UPLOAD_DOCUMENTS', false );
Config::define( 'MCLOUD_STORAGE_CACHE_CONTROL', 'public,max-age=31536000' );
//Config::define( 'MCLOUD_STORAGE_CDN_BASE', '' );
EWWW and Media Cloud pair well together — I fine-tune settings in Media Offload to ensure that images are properly optimized, offloaded, and cached.
This workflow keeps things fast, clean, and scalable. Whether it's performance tuning, building custom themes and plugins, or maintaining long-term client sites, this stack helps me ship high-quality WordPress work without friction.
If you have questions about any part of the setup — or want a deeper dive into how I structure deployments, manage local dev, or streamline performance — feel free to reach out!