Many businesses start on low-cost shared WordPress hosting because it is quick, familiar and good enough at the beginning. The difficulty comes later. Traffic grows, the website becomes more important to the business, and teams need custom functionality that a generic theme or another large plugin cannot provide cleanly.
At that point, businesses are often presented with two unsatisfying choices: keep adding plugins and paying more for a constrained hosting plan, or replace WordPress and retrain the people who already know how to manage the site.
I do not think the content-management system is the problem. The business has already built processes around WordPress. Editors know how to create pages, publish articles, manage media and make routine changes without waiting for a developer. Replacing that workflow introduces cost and disruption without necessarily solving the underlying hosting and engineering problems.
The better option is to preserve WordPress for content management while replacing the parts that create the ceiling around it.
For the Katalyst Technologies site, that meant moving away from the assumptions of shared hosting and building a leaner setup around Docker, a custom Gutenberg-first theme, small purpose-built plugins, modern frontend tooling and controlled deployments to an Azure virtual machine.
The problem: growth without a disruptive replatform
Shared hosting is useful for many websites, and moving away from it is not automatically an improvement. The change becomes valuable when a business needs more control over performance, deployment, security or custom functionality than a shared environment is designed to provide.
The aim was not to make WordPress complicated. It was to create a clearer path for a growing, higher-traffic site:
- Keep the WordPress administration experience familiar to the content team.
- Avoid a costly migration to a new CMS and the process changes that come with it.
- Replace a bloated multipurpose theme with code built for the actual website.
- Add business functionality without accumulating overlapping plugins.
- Control the server and scale its resources as demand grows.
- Release updates without editing production files or interrupting publishing.
This separates two concerns that are too often treated as one. WordPress can remain the editorial system while the infrastructure and custom application code mature around it.
The principles behind the setup
Before choosing tools, I settled on a few constraints:
- WordPress should remain easy for editors to use.
- Only code we own should live in Git.
- Local development should be reproducible on a new machine.
- Production deployments should never overwrite the database, uploads, configuration, or WordPress core.
- A failed release should be easy to reverse.
- The stack should stay as small as the site actually needs.
That last point matters. This is a business website, not a globally distributed application processing millions of events. A well-configured single VM is easier to understand and operate than a collection of services added only because they are fashionable.
It also lets the business spend money where it creates value. Instead of paying for layers of bundled hosting features or compensating for a heavy theme with a larger plan, the site can use right-sized compute and application code that is easier to profile and improve.
Treating wp-content as the application boundary
The most important architectural decision was deciding what belongs in source control.
In this project, Git owns the custom parts of the application:
wp-content/
├── themes/custom-theme/
├── plugins/
└── mu-plugins/
WordPress core, the database, generated uploads, caches, secrets, and installed dependencies stay outside the repository. This creates a clear boundary between application code and runtime state.
It also makes deployment safer. A theme release cannot accidentally replace wp-config.php, delete uploaded media, or modify the database. WordPress can be maintained as the platform it is, while the custom theme and plugins follow a normal software development workflow.
Reproducible local development with Docker
The local environment runs through Docker Compose. It contains three focused services:
- WordPress with PHP 8.3 and Apache.
- MySQL 8.4 with a persistent data volume and health check.
- WP-CLI, available through an optional tools profile.
The WordPress container waits for a healthy database before starting. The custom theme, custom plugins, must-use plugins, and utility scripts are mounted directly from the repository, while WordPress core, uploads, and database data live in Docker volumes.
This split gives me fast feedback while developing owned code without committing runtime files. It also means stopping the containers does not erase the local site. When I need a completely clean installation, I can deliberately remove the volumes and start again.
WP-CLI makes initialization repeatable. A fresh environment can install WordPress, activate the theme and plugins, and seed the initial pages, services, homepage, and navigation. The seed scripts refuse to run outside the local environment, which reduces the risk of accidentally replacing production content.
Local credentials and integration keys are supplied through an ignored environment file. They never need to appear in the repository.
A custom Gutenberg-first theme
The site uses a theme built from scratch rather than a large multipurpose theme or page builder. The server-rendered layer stays close to WordPress: PHP templates, standard theme APIs, and Gutenberg blocks.
The theme registers reusable block patterns for complete Katalyst pages and individual page sections. It also defines shared block styles and editor styling so content authors see a close representation of the final design while working in Gutenberg.
This approach gives editors flexibility without handing every page a completely blank canvas. Reusable patterns provide the intended structure and visual language, while native blocks keep the content portable and editable.
Most importantly, this does not ask the content team to abandon a process they have already mastered. They still sign in to WordPress, use Gutenberg, preview changes and publish in the normal way. The engineering underneath the site changes; the everyday business workflow does not.
The frontend toolchain is deliberately modern but small:
- Tailwind CSS 4 for the visual system
- Vite 8 for development and production builds
- Modern vanilla JavaScript for interaction
- No React application layer unless a future feature genuinely requires one
During local development, WordPress loads CSS and JavaScript modules from the Vite development server, including hot module replacement. A style or script change appears immediately without rebuilding the whole theme.
For production, Vite generates optimized, content-hashed assets and a manifest. The PHP theme reads that manifest to enqueue the correct files. This avoids hard-coded bundle names and gives deployed assets reliable cache busting.
The block editor receives its own compiled stylesheet from the same design system, helping the editing experience stay aligned with the public site.
Purpose-built functionality instead of plugin sprawl
Functionality that belongs to the business lives in small custom plugins. The project currently separates services and enquiry handling from the theme, with shared bootstrapping in must-use plugins where appropriate.
That separation is intentional. A service content type or enquiry workflow is application behavior, not presentation. Keeping it outside the theme prevents important content and logic from disappearing if the visual layer changes later.
It also avoids installing a large third-party plugin for every small requirement. Fewer moving parts mean fewer compatibility surprises, a smaller security surface, and code that matches the actual site rather than a generic configuration screen. Custom does not have to mean complicated; a focused plugin can be smaller and easier to maintain than adapting a broad product to one narrow requirement.
A right-sized production server
Production runs on a single Ubuntu 24.04 LTS virtual machine in Azure. The server uses Nginx, PHP 8.3 FPM, and MariaDB, with TLS certificates renewed automatically through Certbot.
The machine is protected with a host firewall, Fail2ban, and unattended security updates. WordPress configuration and database credentials remain on the server and are not available to the deployment process.
A single VM is not infinite scale, and it is not a substitute for a high-availability architecture when the business genuinely requires one. It is a practical step beyond shared hosting: resources are dedicated and measurable, the application can be tuned, and the machine can be resized as demand grows.
That keeps initial cost and operational complexity under control while leaving a sensible growth path. Page caching, object caching, a CDN, separate database infrastructure or multiple application servers can be introduced when measurements justify them. The site does not need to pay the complexity cost of all those components on day one.
Building immutable theme releases in GitHub Actions
A push to the main branch starts the production deployment workflow.
The workflow installs the locked Node.js dependencies with npm ci, builds the Vite and Tailwind assets, and creates a runtime-only theme artifact. Development files such as source assets, package manifests, and node_modules are left out of the production bundle.
Each deployment is uploaded to its own immutable release directory, identified by the Git commit SHA. The workflow then changes a symbolic link so WordPress begins serving the new release.
That final switch is atomic. Visitors do not see a half-uploaded theme containing a mixture of old and new files.
The deployment account is deliberately restricted. It has no sudo access and can write only inside the theme release area. It cannot read WordPress configuration or alter core files, uploads, or database data. GitHub Actions supplies the SSH material through encrypted repository secrets and verifies the server host key rather than accepting an unknown host.
After switching releases, the workflow requests both a built asset and the public homepage. If either check fails, it restores the previous release automatically.
This is one of the highest-value parts of the setup: deployment is no longer “copy files and hope.” It is a repeatable release process with a narrow permission boundary, a health check, and a rollback path.
What this setup gets right
The individual tools are useful, but the boundaries between them matter more:
- Docker owns the disposable local runtime.
- Git owns only custom application code.
- Gutenberg owns structured editorial content.
- Vite owns frontend asset compilation.
- GitHub Actions owns repeatable releases.
- The production server owns secrets, persistent data, and the WordPress runtime.
Each part has a clear responsibility. That makes the system easier to reason about and reduces the chance that one operation—especially a deployment—damages unrelated state.
From the business perspective, the result is continuity rather than upheaval. Editors keep WordPress. Customers keep the public website. Developers gain a controlled environment for custom work. Infrastructure can grow without forcing the company to redesign its publishing process at the same time.
What I would add next
The next improvements are operational rather than architectural: a separate staging environment, automated off-site backup verification, and more proactive uptime and application monitoring. Those can be added without changing the core development or deployment model.
That is a useful sign. The foundation is simple enough to understand today but has clear extension points for tomorrow.
Final thoughts
A growing business should not have to choose between an increasingly constrained shared-hosting setup and a disruptive CMS replacement. WordPress can continue doing what it is good at—content editing, server-rendered pages and a familiar publishing experience—while normal engineering practices improve the system around it.
For this site, that means a custom Gutenberg-first theme, focused plugins, a small frontend toolchain, repeatable Docker development, a strict source-control boundary and atomic production releases on a right-sized Azure VM.
The result is still recognizably WordPress for the people running the business. It simply has clearer ownership, less bloat, safer deployments and a more credible path beyond the limits of shared hosting.