Skip to main content
  1. Posts/

WordPress · MariaDB · phpMyAdmin — GitOps Deployment

·1590 words·8 mins
Author
Daniel Lincu
I build, automate, and document Kubernetes infrastructure in my TechCats Homelab.

Overview
#

This deployment brings together WordPress, MariaDB, and phpMyAdmin as a fully declarative stack running on Kubernetes. Each component is version-controlled in Git and continuously synchronized to the cluster using Argo CD, following a GitOps workflow. WordPress connects to a persistent MariaDB backend, while phpMyAdmin provides an optional web interface for database management.

The setup is designed for clarity and reproducibility — every configuration, secret reference, and ingress rule is defined as code. Persistent volumes ensure data durability, and Traefik handles secure HTTPS access through automatic TLS certificates issued by cert-manager.

GitOps-managed WordPress stack on K3s Homelab
Figure 1: GitOps-managed WordPress stack on K3s Homelab — declaratively deployed with Argo CD, Vault, Longhorn, Traefik, and cert-manager, featuring isolated dev/prod environments with secure ingress and persistent storage.

Folder Structure
#

Each component of the stack is organized under its own directory within the apps/ folder, following a clear GitOps convention. The MariaDB and phpMyAdmin applications include their manifests and Helm values files, while WordPress uses a Kustomize structure with separate base and overlays directories for environment-specific configuration. This layout keeps application definitions modular, easy to maintain, and fully traceable through Git.

apps/
├── mariadb/
│   ├── resources/
│   ├── app.yaml
│   └── values.yaml
│
├── phpmyadmin/
│   ├── resources/
│   ├── app.yaml
│   └── values.yaml
│
└── wordpress/
    ├── base/
    └── overlays/
        ├── dev/
        └── prod/

MariaDB Configuration
#

The MariaDB component is deployed through a dedicated Argo CD Application that combines the Bitnami Helm chart with custom Kubernetes manifests for Helm values, storage, and Vault-based secret synchronization. It serves as the persistent backend for both wordpress_dev and wordpress_prod databases.

Helm Values
#

The values.yaml file defines the main configuration for the Bitnami chart, including authentication, persistence, and security parameters.

Credentials are sourced from a Vault-managed Kubernetes Secret (vault-mariadb-secrets), which is synchronized by the CSI driver but not directly mounted inside the pod. MariaDB reads the credentials from the Secret at startup, ensuring that sensitive data remains managed securely outside Git.

# Excerpt from values.yaml
auth:
  existingSecret: vault-mariadb-secrets
  database: wordpress_dev
  username: wordpress

primary:
  persistence:
    enabled: true
    existingClaim: mariadb-pvc
    storageClass: longhorn-retain
    size: 4Gi

This configuration links the Helm release to a pre-provisioned PersistentVolumeClaim and references Vault-generated credentials for authentication.

The deployment automatically created the wordpress_dev database. Once phpMyAdmin was added for database management, a second database — wordpress_prod — was manually created to host the production WordPress instance.

Vault Integration
#

A SecretProviderClass defines how credentials are synchronized from Vault to Kubernetes using the Secrets Store CSI driver. The driver fetches values from Vault and mirrors them into a Kubernetes Secret (vault-mariadb-secrets), which MariaDB reads at startup — credentials are not mounted directly into the pod filesystem.

# Excerpt from mariadb-spc.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: vault-mariadb-secrets
  namespace: mariadb
spec:
  provider: vault
  parameters:
    vaultAddress: "https://vault.vault.svc:8200"
    roleName: mariadb
    objects: |
      - objectName: "mariadb-root-password"
        secretPath: "kv/data/mariadb/credentials"
        secretKey: "rootPassword"

This approach keeps credentials securely managed in Vault, synchronized through Kubernetes, and fully compatible with standard Helm chart configurations.

Persistence
#

MariaDB uses a dedicated PersistentVolume and PersistentVolumeClaim provisioned via Longhorn, ensuring data durability across pod restarts and upgrades.

# Excerpt from persistence.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mariadb-pvc
  namespace: mariadb
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn-retain
  resources:
    requests:
      storage: 4Gi

By using the longhorn-retain StorageClass, data remains intact even if the application is redeployed or removed, maintaining stateful reliability across the stack.


phpMyAdmin Configuration
#

The phpMyAdmin component is deployed through a dedicated Argo CD Application that combines the Bitnami Helm chart with custom Kubernetes manifests for Helm values, TLS, and ingress routing. It provides a secure, web-based interface for managing both the wordpress_dev and wordpress_prod databases hosted on the MariaDB service.

Helm Values
#

The values.yaml file defines the phpMyAdmin deployment settings — security contexts, resource presets, and the connection to MariaDB within the cluster.
Ingress is disabled at the chart level since routing is handled separately through a custom Traefik IngressRoute.

# Excerpt from values.yaml
db:
  host: mariadb.mariadb.svc.cluster.local
  port: 3306
  allowArbitraryServer: false
  bundleTestDB: false
  enableSsl: false

This setup ensures encrypted HTTPS access to phpMyAdmin via phpmyadmin.kub.techcats.org, with certificates automatically renewed by cert-manager.

TLS and Ingress
#

TLS certificates are automatically provisioned by cert-manager through a custom Certificate manifest using the DNS-01 challenge with the Vault-integrated issuer.
Traffic is routed securely through Traefik using an IngressRoute definition that references the issued certificate.

# Excerpt from ir-phpmyadmin.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: phpmyadmin
  namespace: phpmyadmin
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`phpmyadmin.kub.techcats.org`)
      kind: Rule
      services:
        - name: phpmyadmin
          port: 80
  tls:
    secretName: phpmyadmin-tls-secret

This setup ensures encrypted HTTPS access to phpMyAdmin via phpmyadmin.kub.techcats.org, with certificates automatically renewed by cert-manager.
All domains under *.kub.techcats.org are resolved locally through Pi-hole and are not publicly accessible outside the homelab network.

phpMyAdmin dashboard view
Figure 1: phpMyAdmin dashboard accessible through Traefik at phpmyadmin.kub.techcats.org. The interface connects to the internal MariaDB service and manages both wordpress_dev and wordpress_prod databases.

WordPress Configuration
#

The WordPress component is deployed through two dedicated Argo CD Applications — one for development and one for production — each in its own namespace (wordpress-dev and wordpress-prod).
Both integrate the Bitnami Helm chart with custom Helm values, persistence, TLS, ingress routing, and Vault-based secret synchronization, maintaining a consistent, declarative configuration aligned with the homelab’s GitOps principles.

Helm Values
#

The values.yaml file defines the main configuration for the Bitnami WordPress Helm chart, covering authentication, persistence, and external database connectivity.
Both development and production environments share a common base configuration, while overlays apply small patches for environment-specific settings such as database names, Vault roles, and ingress definitions.

Credentials and database passwords are fetched from Vault through the Secrets Store CSI driver, which mirrors them into Kubernetes Secrets (vault-wp-admin-secret and vault-wp-db-secret) without exposing plaintext values in Git.
Each overlay references its own Vault role (wordpress-dev or wordpress-prod), ensuring that secrets remain isolated per environment while maintaining consistent configuration logic.

# Excerpt from values.yaml
externalDatabase:
  host: mariadb.mariadb.svc.cluster.local
  port: 3306
  user: wordpress
  database: wordpress
  existingSecret: vault-wp-db-secret

persistence:
  enabled: true
  existingClaim: wordpress-pvc
  storageClass: longhorn-retain
  accessModes:
    - ReadWriteOnce
  size: 4Gi

Vault Integration
#

A dedicated SecretProviderClass defines how WordPress credentials and database passwords are synchronized from Vault into Kubernetes using the Secrets Store CSI driver.
Each environment (wordpress-dev and wordpress-prod) uses a separate Vault role and secret path, ensuring isolated authentication and environment-specific secret scoping.

The CSI driver retrieves values such as the WordPress admin credentials, site metadata, and external MariaDB password directly from Vault, mirroring them into two Kubernetes Secrets — vault-wp-admin-secret and vault-wp-db-secret — which are referenced by the Helm chart at runtime.
Sensitive data never appears in Git or manifests, maintaining end-to-end secret confidentiality.

# Excerpt from wordpress-spc.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: vault-wp-secrets
  namespace: wordpress
spec:
  provider: vault
  parameters:
    vaultAddress: "https://vault.vault.svc:8200"
    roleName: wordpress
    objects: |
      - objectName: "wordpress-username"
        secretPath: "kv/data/wordpress/site"
        secretKey: "username"
      - objectName: "mariadb-password"
        secretPath: "kv/data/wordpress/db"
        secretKey: "password"

Separate patches modify the roleName and secret paths for each environment, referencing kv/data/wordpress-dev/ or kv/data/wordpress-prod/.
This approach aligns with the same Vault-based secret management workflow used across MariaDB and phpMyAdmin, keeping credentials dynamic, isolated, and centrally m

Persistence
#

WordPress uses dedicated PersistentVolume and PersistentVolumeClaim resources provisioned through Longhorn, ensuring data durability for uploads, plugins, and themes across pod restarts and redeployments.
Both environments share the same storage specification, while JSON 6902 patches adjust the volume name and handle to create distinct resources for development and production.

# Excerpt from persistence.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-pvc
  namespace: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn-retain
  resources:
    requests:
      storage: 4Gi

Each overlay applies a small patch that renames the PersistentVolume and updates its Longhorn volume handle to either wordpress-dev or wordpress-prod, preventing cross-environment overlap.
Using the longhorn-retain StorageClass ensures data persists across redeployments, providing long-term stateful reliability within the homelab.

TLS and Ingress
#

TLS certificates are automatically issued by cert-manager using the Let’s Encrypt ClusterIssuer (letsencrypt-vault-issuer), which retrieves its Cloudflare API credentials securely from Vault.
Traffic is securely routed through Traefik via an IngressRoute definition that references the generated certificate.

# Excerpt from ir-wordpress.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: wordpress
  namespace: wordpress
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`techcats.org`)
      kind: Rule
      services:
        - name: wordpress-prod
          port: 80
  tls:
    secretName: wordpress-tls-secret

Each overlay applies patches to adjust hostnames and certificates per environment —
development.techcats.org for the dev instance and techcats.org for production.
The development site is accessible only within the local network, while the production instance is exposed externally through Cloudflare and can be viewed at https://techcats.org.

Live WordPress deployment
Figure 2: Live WordPress deployment served through Traefik at techcats.org, with TLS issued by cert-manager via the Vault-integrated Let’s Encrypt ClusterIssuer.

An additional Traefik Middleware restricts access to /wp-admin and /wp-login.php, allowing only trusted local IP ranges within the homelab.
This configuration is described in detail in this post.


Backup and Recovery
#

Cluster-level backups are automated through Velero, ensuring data resilience for all namespaces and persistent volumes.
Velero runs a daily scheduled backup with the following configuration:

  • Schedule: 0 3 * * *
  • Retention: 7 days (ttl: 168h)

The entire backup process and configuration are documented in detail here.


Key Takeaways
#

  • Demonstrates a complete GitOps workflow managed through Argo CD.
  • Integrates Vault for centralized, environment-specific secret management.
  • Uses Longhorn for persistent volumes and Traefik for ingress/TLS.
  • Includes automated daily backups with Velero for data resilience and recovery.
  • Shows clear separation between development and production environments.

Repository & Implementation
#

All manifests, overlays, and Helm configurations are version-controlled in Git and deployed via Argo CD.
Full implementation available at whitehatcats/k3s-homelab-gitops.


References
#