Skip to main content

WordPress in GitOps con Bedrock su un VPS HMS

Questa guida spiega come ospitare un sito WordPress su un VPS HostMyServers e gestirlo in GitOps grazie a Bedrock, il boilerplate WordPress di Roots. Il tuo repository Git diventa la fonte di verità: il core WordPress, i plugin e i temi sono dichiarati nel codice, versionati, poi distribuiti automaticamente sul tuo VPS a ogni git push.

Un VPS ti offre l'accesso root, SSH e il controllo totale dello stack (Nginx, PHP-FPM, MariaDB) indispensabili per questa modalità di deployment, cosa che un hosting condiviso non consente.

Perché Bedrock?

Un WordPress classico mescola il codice (core, plugin, temi), la configurazione (wp-config.php) e i dati (upload) nella stessa cartella, e si aggiorna dall'interfaccia di amministrazione. È quindi difficile da versionare e riprodurre. Bedrock offre:

  • Composer per gestire il core WordPress, i plugin e i temi come dipendenze (versioni bloccate in composer.lock)
  • Una configurazione tramite variabili d'ambiente (file .env), senza segreti in Git
  • File di configurazione per ambiente (sviluppo, staging, produzione)
  • Una struttura più sicura: solo la cartella web/ è esposta dal server web
  • La disattivazione delle modifiche dall'amministrazione in produzione (DISALLOW_FILE_MODS)

Ordina un VPS HostMyServers

Questa guida è pensata per un VPS HostMyServers. Scegli l'offerta in base al tuo traffico:

  • VPS NVMe - Eccellente rapporto qualità/prezzo, ideale per avviare un sito vetrina o un blog
  • VPS Performance - Consigliato per i negozi WooCommerce e i siti ad alto traffico
UtilizzovCPURAMStorage
Sito vetrina / blog1-22 GB20 GB NVMe
Sito a traffico medio, più siti2-44 GB40 GB NVMe
WooCommerce / alto traffico4+8 GB+80 GB NVMe+

Al momento dell'ordine, seleziona Ubuntu 24.04 LTS o Debian 12 come sistema operativo. Una volta consegnato il VPS, l'indirizzo IP e le credenziali root ti vengono comunicati via e-mail e sono disponibili nella tua area clienti HostMyServers.

Progetti più importanti

Per più siti ad alto traffico, la stessa procedura si applica ai server dedicati Eco e Performance.

Principio GitOps applicato a WordPress

ElementoDove risiedeVersionato in Git?
Core WordPress, plugin e temi pubblicicomposer.json + composer.lockSì (dichiarazione + versioni esatte)
Tema e plugin sviluppati da teweb/app/themes/, web/app/plugins/Sì (codice sorgente)
Configurazione (costanti, ambienti)config/
Segreti (password, chiavi, salt).env su ogni serverNo
Media (upload)web/app/uploads/ sul serverNo (da sottoporre a backup)
Contenuti (articoli, pagine, impostazioni)Database MySQL/MariaDBNo (da sottoporre a backup)

Il ciclo di vita diventa:

  1. Modifichi il codice o aggiorni una dipendenza in locale
  2. Fai commit e push sul branch main
  3. La pipeline CI installa le dipendenze e distribuisce una nuova release sul server
  4. In caso di problemi, torni alla release precedente (git revert o cambio del link simbolico)
Ciò che resta fuori da Git

Il database e i media sono dati, non codice. Non sono gestiti da Git: predisponi backup regolari (wp db export, backup della cartella shared/uploads, snapshot del VPS).

Prerequisiti

Sul tuo computer di sviluppo:

  • Git
  • PHP ≥ 8.1 (8.3 consigliato) e Composer 2
  • Un account GitHub (o GitLab) per ospitare il repository

Sul tuo VPS HMS (Ubuntu 24.04 LTS / Debian 12):

  • Accesso SSH root o utente con sudo
  • Un VPS protetto (utente non root, SSH, firewall): vedi Proteggere il proprio server
  • Un nome di dominio il cui record DNS A punta all'indirizzo IP del VPS
Nessun Composer in produzione

In questa guida, composer install viene eseguito dalla pipeline CI. Il VPS riceve solo file già pronti: non ha bisogno né di Composer, né di Git, né di accesso ai repository di pacchetti.

Fase 1: creare il progetto Bedrock in locale

  1. Crea il progetto:

    composer create-project roots/bedrock mon-site
    cd mon-site
  2. Scopri la struttura:

    mon-site/
    ├── composer.json # Core WordPress, plugin e temi dichiarati
    ├── composer.lock # Versioni esatte installate
    ├── .env.example # Modello di configurazione
    ├── wp-cli.yml # Indica a WP-CLI dove si trova WordPress (web/wp)
    ├── config/
    │ ├── application.php # Configurazione principale (sostituisce wp-config.php)
    │ └── environments/
    │ ├── development.php
    │ └── staging.php
    ├── vendor/ # Dipendenze Composer (non versionato)
    └── web/ # Radice del server web (document root)
    ├── app/ # Equivalente di wp-content
    │ ├── mu-plugins/
    │ ├── plugins/
    │ ├── themes/
    │ └── uploads/
    ├── wp/ # Core WordPress (non versionato)
    ├── wp-config.php
    └── index.php
    info

    Con Bedrock, l'amministrazione si trova all'indirizzo https://tuo-dominio.com/wp/wp-admin e wp-content è sostituito da web/app.

  3. Crea il tuo file .env locale a partire dal modello:

    cp .env.example .env
    .env
    DB_NAME='wordpress_dev'
    DB_USER='wordpress_user'
    DB_PASSWORD='password_locale'
    DB_HOST='localhost'
    DB_PREFIX='wp_'

    WP_ENV='development'
    WP_HOME='http://mon-site.test'
    WP_SITEURL="${WP_HOME}/wp"

    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'
  4. Genera chiavi e salt unici (da fare per ogni ambiente):

    for k in AUTH_KEY SECURE_AUTH_KEY LOGGED_IN_KEY NONCE_KEY AUTH_SALT SECURE_AUTH_SALT LOGGED_IN_SALT NONCE_SALT; do
    echo "$k='$(openssl rand -base64 48 | tr -d '\n')'"
    done

    Copia il risultato nel tuo .env al posto delle righe generateme. Puoi anche usare il generatore di Roots.

Ambiente locale

Per far girare il sito in locale, usa lo strumento che preferisci (DDEV, Lando, Laravel Valet, Docker…) facendo puntare la radice web sulla cartella web/.

Fase 2: gestire plugin e temi con Composer

Bedrock è preconfigurato con WPackagist, un mirror Composer di tutti i plugin e i temi del repository ufficiale WordPress.org.

  1. Installa un plugin o un tema:

    composer require wpackagist-plugin/wordpress-seo
    composer require wpackagist-plugin/wp-mail-smtp
    composer require wpackagist-theme/twentytwentyfive

    Il nome del pacchetto corrisponde allo slug WordPress.org (https://wordpress.org/plugins/<slug>/).

  2. Aggiorna le dipendenze:

    # Aggiornare tutto secondo i vincoli di composer.json
    composer update

    # Aggiornare solo il core WordPress
    composer update roots/wordpress --with-all-dependencies

    # Vedere gli aggiornamenti disponibili
    composer outdated
  3. Rimuovi un plugin:

    composer remove wpackagist-plugin/wp-mail-smtp
Fai sempre commit di composer.lock

È il file composer.lock a garantire che la produzione installi esattamente le stesse versioni testate in locale. Deve sempre essere versionato.

Plugin premium o privati

I plugin a pagamento non sono su WPackagist. Due soluzioni:

  • Consigliata: usare il repository Composer fornito dall'editore (ACF Pro, Gravity Forms, WPML… ne offrono uno) con una chiave di autenticazione salvata in auth.json (non versionato) e nei secret della tua CI.

  • Alternativa: versionare il plugin direttamente nel repository aggiungendo un'eccezione al .gitignore:

    .gitignore
    web/app/plugins/*
    !web/app/plugins/.gitkeep
    !web/app/plugins/mon-plugin-premium

Il tuo tema personalizzato (in web/app/themes/) è versionato per impostazione predefinita.

Fase 3: regolare la configurazione

La configurazione comune si trova in config/application.php. Le sovrascritture per ambiente sono in config/environments/<WP_ENV>.php. Per impostazione predefinita, Bedrock:

  • disattiva l'editor di file e l'installazione/aggiornamento dei plugin dall'amministrazione (DISALLOW_FILE_MODS) in produzione
  • li consente in development
  • nasconde gli errori PHP in produzione

Questo comportamento è al centro del GitOps: in produzione, ogni modifica del codice passa da Git. Se un plugin mostra "aggiornamento disponibile", esegui l'aggiornamento con Composer in locale, testa, poi fai commit.

Esempio di aggiunta di una costante per tutti gli ambienti:

config/application.php
Config::define('WP_POST_REVISIONS', 10);
Config::define('WP_MEMORY_LIMIT', '256M');

Fase 4: inizializzare il repository Git

Bedrock fornisce un .gitignore adatto: vendor/, web/wp/, i plugin installati da Composer, gli upload e il file .env sono ignorati.

git init -b main
git add .
git commit -m "Inizializzazione del progetto Bedrock"
git remote add origin git@github.com:votre-compte/mon-site.git
git push -u origin main

Verifica che nessun segreto sia versionato:

git ls-files | grep -E '(^|/)\.env$|auth\.json' || echo "OK : aucun secret versionné"

Fase 5: preparare il VPS HMS

Connettersi al VPS

Connettiti con l'indirizzo IP e le credenziali ricevute al momento della consegna:

ssh root@indirizzo_ip_vps

Installare Nginx, PHP-FPM e MariaDB

sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx mariadb-server \
php-fpm php-mysql php-curl php-gd php-intl php-mbstring php-xml php-zip php-imagick
php -v

Ubuntu 24.04 installa PHP 8.3 (Debian 12: PHP 8.2). Nel resto della guida, adatta php8.3-fpm alla versione mostrata da php -v. Per proteggere MariaDB, segui la guida Installare e proteggere MariaDB.

Se il firewall UFW è attivo, apri le porte web:

sudo ufw allow 'Nginx Full'

Creare l'utente di deployment e la struttura delle cartelle

La pipeline si connetterà con un utente dedicato deploy. Il codice appartiene a deploy ed è solo in lettura per PHP (www-data); solo la cartella degli upload è accessibile in scrittura da PHP.

sudo adduser --disabled-password --gecos "" deploy
sudo usermod -aG www-data deploy

sudo mkdir -p /var/www/mon-site/{releases,shared/uploads}
sudo chown -R deploy:www-data /var/www/mon-site
sudo chown -R www-data:www-data /var/www/mon-site/shared/uploads
sudo chmod 2775 /var/www/mon-site/shared/uploads

La struttura di deployment sarà la seguente:

/var/www/mon-site/
├── current -> releases/<sha> # Link simbolico verso la release attiva
├── releases/
│ ├── 3f2a9c1.../ # Una release per ogni commit distribuito
│ └── 8be41d0.../
└── shared/
├── .env # Configurazione di produzione (fuori da Git)
└── uploads/ # Media persistenti tra le release

Creare il database

sudo mysql -u root -p
CREATE DATABASE wordpress_prod DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wordpress_user'@'localhost' IDENTIFIED BY 'password_sicura';
GRANT ALL PRIVILEGES ON wordpress_prod.* TO 'wordpress_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Creare il file .env di produzione

sudo -u deploy nano /var/www/mon-site/shared/.env
/var/www/mon-site/shared/.env
DB_NAME='wordpress_prod'
DB_USER='wordpress_user'
DB_PASSWORD='password_sicura'
DB_HOST='localhost'
DB_PREFIX='wp_'

WP_ENV='production'
WP_HOME='https://tuo-dominio.com'
WP_SITEURL="${WP_HOME}/wp"

# Chiavi e salt generati con il comando openssl della fase 1
AUTH_KEY='...'
SECURE_AUTH_KEY='...'
LOGGED_IN_KEY='...'
NONCE_KEY='...'
AUTH_SALT='...'
SECURE_AUTH_SALT='...'
LOGGED_IN_SALT='...'
NONCE_SALT='...'
sudo chown deploy:www-data /var/www/mon-site/shared/.env
sudo chmod 640 /var/www/mon-site/shared/.env

Configurare Nginx

La radice web punta a current/web: il codice, vendor/ e il .env non sono mai esposti.

/etc/nginx/sites-available/mon-site
server {
listen 80;
server_name tuo-dominio.com www.tuo-dominio.com;

root /var/www/mon-site/current/web;
index index.php;

client_max_body_size 64M;

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

# Impedire l'esecuzione di PHP negli upload
location ~* ^/app/uploads/.*\.php$ {
deny all;
}

location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
# Risolvere il link simbolico "current" per ogni richiesta
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
}

location ~ /\. {
deny all;
}
}
sudo ln -s /etc/nginx/sites-available/mon-site /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
$realpath_root

L'uso di $realpath_root (invece di $document_root) garantisce che PHP-FPM carichi i file della nuova release non appena avviene il cambio del link current, senza servire vecchi percorsi in cache.

Attiva poi HTTPS, ad esempio con Certbot (vedi anche Installare un certificato SSL):

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d tuo-dominio.com -d www.tuo-dominio.com

Autorizzare il reload di PHP-FPM

La pipeline ricarica PHP-FPM dopo ogni deployment per svuotare la cache OPcache. Autorizza solo questo comando per l'utente deploy:

echo 'deploy ALL=(root) NOPASSWD: /usr/bin/systemctl reload php8.3-fpm' | sudo tee /etc/sudoers.d/deploy
sudo chmod 440 /etc/sudoers.d/deploy
sudo visudo -c

Creare la chiave SSH di deployment

Sul tuo computer, genera una coppia di chiavi dedicata alla pipeline:

ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./deploy_key -N ""

Aggiungi la chiave pubblica sul VPS:

sudo mkdir -p /home/deploy/.ssh
sudo nano /home/deploy/.ssh/authorized_keys # incolla il contenuto di deploy_key.pub
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh && sudo chmod 600 /home/deploy/.ssh/authorized_keys

Recupera l'impronta SSH del VPS (per evitare qualsiasi attacco di tipo man-in-the-middle):

ssh-keyscan -H indirizzo_ip_vps

Fase 6: automatizzare il deployment con GitHub Actions

Dichiarare i secret

Nel tuo repository GitHub, vai in Settings → Secrets and variables → Actions e crea:

SecretValore
SSH_HOSTIndirizzo IP del tuo VPS HMS
SSH_USERdeploy
SSH_PRIVATE_KEYContenuto del file deploy_key (chiave privata)
SSH_KNOWN_HOSTSRisultato del comando ssh-keyscan

Elimina poi i file deploy_key e deploy_key.pub dal tuo computer.

Creare il workflow

.github/workflows/deploy.yml
name: Deploy

on:
push:
branches: [main]
workflow_dispatch:

concurrency:
group: production
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
environment: production
env:
BASE_DIR: /var/www/mon-site
TARGET: ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}
steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2

- name: Installa le dipendenze
run: composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

- name: Configura SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/id_ed25519

- name: Invia la release
run: |
rsync -az --delete \
--exclude='.git' --exclude='.github' \
--exclude='.env' --exclude='web/app/uploads' \
./ "$TARGET:$BASE_DIR/releases/$GITHUB_SHA/"

- name: Attiva la release
run: |
ssh "$TARGET" bash -s -- "$BASE_DIR" "$GITHUB_SHA" <<'EOF'
set -euo pipefail
BASE_DIR="$1"
RELEASE="$BASE_DIR/releases/$2"

# Collegare la configurazione e i media condivisi
ln -sfn "$BASE_DIR/shared/.env" "$RELEASE/.env"
rm -rf "$RELEASE/web/app/uploads"
ln -sfn "$BASE_DIR/shared/uploads" "$RELEASE/web/app/uploads"

# Cambio atomico del link "current"
ln -sfn "$RELEASE" "$BASE_DIR/current.tmp"
mv -Tf "$BASE_DIR/current.tmp" "$BASE_DIR/current"

# Svuotare OPcache
sudo systemctl reload php8.3-fpm

# Conservare le ultime 5 release
cd "$BASE_DIR/releases"
ls -1t | tail -n +6 | xargs -r rm -rf
EOF

Fai commit e push di questo file: il primo deployment parte automaticamente. Segui la sua esecuzione nella scheda Actions del repository.

Variante GitLab

Lo stesso principio si applica con GitLab CI: un'immagine composer:2 per il composer install, poi rsync e ssh con variabili CI/CD protette al posto dei secret GitHub.

Primo avvio

Una volta riuscito il primo deployment, installa WordPress. O tramite il browser all'indirizzo https://tuo-dominio.com/wp/wp-admin/install.php, oppure con WP-CLI dal server:

cd /var/www/mon-site/current
sudo -u www-data wp core install \
--url="https://tuo-dominio.com" \
--title="Titolo del tuo sito" \
--admin_user="admin" \
--admin_password="MotDePasseFort123!" \
--admin_email="tua@email.com"

Consulta la guida Installare WordPress con WP-CLI per installare WP-CLI sul server.

Sitemap XML e SEO

WordPress genera nativamente una mappa del sito (sitemap) all'indirizzo https://tuo-dominio.com/wp-sitemap.xml. Con Bedrock e la configurazione Nginx sopra (try_files ... /index.php?$args), funziona senza impostazioni aggiuntive. Verificalo:

curl -sI https://tuo-dominio.com/wp-sitemap.xml | head -n 1

Se installi un plugin SEO tramite Composer (ad esempio composer require wpackagist-plugin/wordpress-seo), questo sostituisce la sitemap nativa con la propria (/sitemap_index.xml per Yoast SEO). La sitemap resta generata dinamicamente dal database: non c'è nulla da versionare in Git.

Ricordati poi di:

  • Dichiarare l'URL della sitemap in Google Search Console e Bing Webmaster Tools
  • Verificare che Impostazioni → Lettura → Visibilità sui motori di ricerca non sia selezionata in produzione
Staging non indicizzato

Bedrock include il mu-plugin bedrock-disallow-indexing: su un ambiente in cui WP_ENV vale staging o development, il sito chiede ai motori di ricerca di non indicizzarlo (DISALLOW_INDEXING). Solo la produzione compare quindi nei risultati di ricerca.

Lavorare quotidianamente

Aggiornare WordPress e i plugin

composer update
# Testare il sito in locale, poi:
git add composer.json composer.lock
git commit -m "chore: aggiornamento WordPress e plugin"
git push

Il deployment avviene automaticamente. Per essere avvisato delle nuove versioni, attiva Dependabot (ecosistema composer) o Renovate sul repository: apriranno pull request di aggiornamento che dovrai solo approvare.

Usare un branch di staging

Per validare le modifiche prima della produzione, crea un secondo ambiente (sottodominio staging.tuo-dominio.com, altra cartella, altro database, WP_ENV='staging') e duplica il workflow facendolo scattare su un branch staging. Il file config/environments/staging.php si applica quindi automaticamente.

Tornare indietro (rollback)

Metodo GitOps (consigliato): annulla il commit incriminato, la pipeline distribuisce di nuovo lo stato precedente.

git revert <sha_del_commit>
git push

Metodo d'emergenza: cambia manualmente il link current sulla release precedente, direttamente sul server.

cd /var/www/mon-site
ls -1t releases/ # individuare la release precedente
ln -sfn "$PWD/releases/<sha_precedente>" current.tmp && mv -Tf current.tmp current
sudo systemctl reload php8.3-fpm
caution

Un rollback del codice non ripristina il database. Se un aggiornamento ha modificato lo schema del database, ripristina anche un backup SQL effettuato prima del deployment.

Migrare un sito WordPress esistente a Bedrock

  1. Elenca i plugin e i temi installati sul vecchio sito: wp plugin list e wp theme list

  2. Aggiungili al progetto Bedrock con composer require wpackagist-plugin/<slug> (e versiona i tuoi sviluppi specifici)

  3. Copia la vecchia cartella wp-content/uploads/ in /var/www/mon-site/shared/uploads/

  4. Importa il database, poi correggi i percorsi dei media, che passano da wp-content a app:

    cd /var/www/mon-site/current
    sudo -u www-data wp db import /percorso/verso/backup.sql
    sudo -u www-data wp search-replace '/wp-content/uploads' '/app/uploads' --all-tables
  5. Se il prefisso delle tabelle non è wp_, adatta DB_PREFIX nel .env

tip

Fai una prova con --dry-run prima di ogni wp search-replace per verificare il numero di sostituzioni.

Buone pratiche

  • Non modificare mai i file direttamente sul server: tutto passa da Git
  • Fai sempre commit di composer.lock e blocca le versioni sensibili in composer.json
  • Tieni i segreti fuori dal repository (.env, auth.json) e usa i secret della CI
  • Proteggi il branch main (revisione obbligatoria delle pull request)
  • Esegui backup regolari del database e della cartella shared/uploads, e integra con gli snapshot/backup del tuo VPS
  • Distribuisci prima su un ambiente di staging

In caso di problemi

  • Pagina bianca o errore 500: controlla i log sudo tail -f /var/log/nginx/error.log e /var/log/php8.3-fpm.log, oltre alla presenza del link .env nella release attiva
  • Errore di connessione al database: verifica le credenziali del file shared/.env
  • La vecchia versione resta visualizzata dopo il deployment: verifica che PHP-FPM sia stato effettivamente ricaricato e che Nginx usi $realpath_root
  • Impossibile inviare media: verifica che shared/uploads appartenga a www-data e che il link web/app/uploads punti correttamente ad essa
  • La pipeline fallisce alla connessione SSH: controlla i secret SSH_PRIVATE_KEY e SSH_KNOWN_HOSTS, e testa ssh deploy@indirizzo_ip_vps dal tuo computer
  • Nessuna installazione di plugin dall'amministrazione: è normale in produzione (DISALLOW_FILE_MODS), usa Composer