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.
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
| Utilizzo | vCPU | RAM | Storage |
|---|---|---|---|
| Sito vetrina / blog | 1-2 | 2 GB | 20 GB NVMe |
| Sito a traffico medio, più siti | 2-4 | 4 GB | 40 GB NVMe |
| WooCommerce / alto traffico | 4+ | 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.
Per più siti ad alto traffico, la stessa procedura si applica ai server dedicati Eco e Performance.
Principio GitOps applicato a WordPress
| Elemento | Dove risiede | Versionato in Git? |
|---|---|---|
| Core WordPress, plugin e temi pubblici | composer.json + composer.lock | Sì (dichiarazione + versioni esatte) |
| Tema e plugin sviluppati da te | web/app/themes/, web/app/plugins/ | Sì (codice sorgente) |
| Configurazione (costanti, ambienti) | config/ | Sì |
| Segreti (password, chiavi, salt) | .env su ogni server | No |
| Media (upload) | web/app/uploads/ sul server | No (da sottoporre a backup) |
| Contenuti (articoli, pagine, impostazioni) | Database MySQL/MariaDB | No (da sottoporre a backup) |
Il ciclo di vita diventa:
- Modifichi il codice o aggiorni una dipendenza in locale
- Fai commit e push sul branch
main - La pipeline CI installa le dipendenze e distribuisce una nuova release sul server
- In caso di problemi, torni alla release precedente (
git reverto cambio del link simbolico)
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
Apunta all'indirizzo IP del VPS
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
-
Crea il progetto:
composer create-project roots/bedrock mon-sitecd mon-site -
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.phpinfoCon Bedrock, l'amministrazione si trova all'indirizzo
https://tuo-dominio.com/wp/wp-adminewp-contentè sostituito daweb/app. -
Crea il tuo file
.envlocale a partire dal modello:cp .env.example .env.envDB_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' -
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; doecho "$k='$(openssl rand -base64 48 | tr -d '\n')'"doneCopia il risultato nel tuo
.enval posto delle righegenerateme. Puoi anche usare il generatore di Roots.
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.
-
Installa un plugin o un tema:
composer require wpackagist-plugin/wordpress-seocomposer require wpackagist-plugin/wp-mail-smtpcomposer require wpackagist-theme/twentytwentyfiveIl nome del pacchetto corrisponde allo slug WordPress.org (
https://wordpress.org/plugins/<slug>/). -
Aggiorna le dipendenze:
# Aggiornare tutto secondo i vincoli di composer.jsoncomposer update# Aggiornare solo il core WordPresscomposer update roots/wordpress --with-all-dependencies# Vedere gli aggiornamenti disponibilicomposer outdated -
Rimuovi un plugin:
composer remove wpackagist-plugin/wp-mail-smtp
È 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:.gitignoreweb/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::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
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.
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
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:
| Secret | Valore |
|---|---|
SSH_HOST | Indirizzo IP del tuo VPS HMS |
SSH_USER | deploy |
SSH_PRIVATE_KEY | Contenuto del file deploy_key (chiave privata) |
SSH_KNOWN_HOSTS | Risultato del comando ssh-keyscan |
Elimina poi i file deploy_key e deploy_key.pub dal tuo computer.
Creare il workflow
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.
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
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
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
-
Elenca i plugin e i temi installati sul vecchio sito:
wp plugin listewp theme list -
Aggiungili al progetto Bedrock con
composer require wpackagist-plugin/<slug>(e versiona i tuoi sviluppi specifici) -
Copia la vecchia cartella
wp-content/uploads/in/var/www/mon-site/shared/uploads/ -
Importa il database, poi correggi i percorsi dei media, che passano da
wp-contentaapp:cd /var/www/mon-site/currentsudo -u www-data wp db import /percorso/verso/backup.sqlsudo -u www-data wp search-replace '/wp-content/uploads' '/app/uploads' --all-tables -
Se il prefisso delle tabelle non è
wp_, adattaDB_PREFIXnel.env
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.locke blocca le versioni sensibili incomposer.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.loge/var/log/php8.3-fpm.log, oltre alla presenza del link.envnella 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/uploadsappartenga awww-datae che il linkweb/app/uploadspunti correttamente ad essa - La pipeline fallisce alla connessione SSH: controlla i secret
SSH_PRIVATE_KEYeSSH_KNOWN_HOSTS, e testassh deploy@indirizzo_ip_vpsdal tuo computer - Nessuna installazione di plugin dall'amministrazione: è normale in produzione (
DISALLOW_FILE_MODS), usa Composer