Deploy e CI/CD
cipi deploy
Cipi usa Deployer per tutti i deployment. Ogni deploy è atomico: una nuova directory release viene preparata completamente prima dello swap del symlink current, così il traffico non viene mai interrotto.
cipi deploy e cipi deploy --rollback si interrompono con un messaggio chiaro di upgrade prima di invocare Deployer quando un'app è ancora bloccata a una versione PHP più vecchia — cambiala prima con cipi app edit <app> --php=8.3 (o superiore).Pipeline di deploy
Deployer e Composer girano con la versione PHP configurata dell'app (es. /usr/bin/php8.5), non il default di sistema. Vale per cipi deploy, cipi deploy --rollback, trigger deploy da crontab, deploy cipi sync import e gli alias deploy / composer nel .bashrc dell'utente dell'app.
- Ferma i queue worker (
cipi worker stop) - Clona il repo in
releases/N/ - Esegue
composer install --no-dev(con il PHP dell'app) - Collega
shared/.enveshared/storage/ - Esegue
artisan migrate --force - Esegue
artisan optimize - Esegue
artisan storage:link - Scambia il symlink
currentatomicamente - Riavvia i queue worker
- Elimina release vecchie (mantieni le ultime 5)
$ cipi deploy myapp # deploy latest commit $ cipi deploy myapp --rollback # instant rollback to previous release $ cipi deploy myapp --releases # list all releases with timestamps $ cipi deploy myapp --key # show the SSH deploy key $ cipi deploy myapp --webhook # show webhook URL and token $ cipi deploy myapp --unlock # remove a stuck deploy lock $ cipi deploy myapp --snapshot # v5.0+ opt-in DB dump before deploy $ cipi deploy myapp --snapshot-required # fail if snapshot cannot be taken $ cipi deploy myapp --trust-host=git.mycompany.com # trust a custom Git server fingerprint $ cipi deploy myapp --trust-host=git.mycompany.com:2222 # trust on non-standard port (also writes ~/.ssh/config)
Dalla v5.0, gli snapshot DB pre-deploy opt-in vengono salvati sotto /var/log/cipi/backups/ quando predeploy_snapshot è abilitato o passi --snapshot / --snapshot-required. Il rollback del codice non ripristina automaticamente il database. Le app Octane usano il template Deployer laravel-octane.php (reload/restart di Octane al deploy); abilita build Node con cipi app edit <app> --node-build='…'.
cipi deploy myapp --unlock per rimuoverlo prima di rifare il deploy.auth.json
Gestisci il file auth.json di un'app. Questo file si trova in /home/<app>/shared/auth.json e viene collegato automaticamente in ogni release da Deployer — esattamente come .env. Usalo per salvare dati strutturati di credenziali (es. chiavi API, feature flag o qualsiasi payload JSON) che la tua app Laravel può leggere a runtime.
$ cipi auth create myapp # create auth.json with initial { "users": [] } structure $ cipi auth edit myapp # open in $EDITOR (fallback: nano), validate JSON on close $ cipi auth show myapp # print contents formatted with jq $ cipi auth delete myapp # delete file (asks for confirmation)
Dettagli comando
| Comando | Descrizione |
|---|---|
cipi auth create <app> |
Crea shared/auth.json con la struttura iniziale {"users":[]}, imposta i permessi a 640 (proprietario app:app) e aggiunge auth.json a shared_files nella config Deployer dell'app così viene collegato a ogni deploy.
|
cipi auth edit <app> |
Apre shared/auth.json in $EDITOR (fallback su nano). Dopo la chiusura dell'editor, valida il JSON con jq e avvisa se il file è malformato.
|
cipi auth show <app> |
Stampa il contenuto di shared/auth.json formattato con jq.
|
cipi auth delete <app> |
Chiede conferma, poi elimina shared/auth.json e rimuove la voce auth.json da shared_files nella config Deployer dell'app.
|
Integrazione Deployer
cipi auth create aggiunge automaticamente auth.json alla lista shared_files in /home/<app>/.deployer/deploy.php, e cipi auth delete lo rimuove. Il file è trattato esattamente come .env: persiste tra le release e non viene mai sovrascritto da un deploy.
cipi auth viene registrata via log_action per auditabilità. La sezione AUTH è elencata anche nell'output di cipi help.
Git provider
Cipi è pronto a lavorare con GitHub e GitLab ma supporta qualsiasi altro Git provider che supporti chiavi deploy SSH — nessun vendor lock-in.
Per Git server self-hosted o personalizzati, devi fidarti dell'host fingerprint del server prima che Deployer possa clonare via SSH. Usa il flag --trust-host per aggiungere l'fingerprint al ~/.ssh/known_hosts dell'utente dell'app automaticamente:
# show the deploy key and add it to your Git provider $ cipi deploy myapp --key # trust a custom Git server fingerprint (standard port) $ cipi deploy myapp --trust-host=git.mycompany.com # trust a custom Git server on a non-standard port # (also writes ~/.ssh/config automatically) $ cipi deploy myapp --trust-host=git.mycompany.com:2222
Host / Port nel ~/.ssh/config dell'utente dell'app così Deployer può raggiungere il server senza configurazione extra.
Git auto-setup
Se salvi un Personal Access Token GitHub o GitLab, Cipi aggiunge automaticamente la chiave deploy SSH e crea il webhook sul repository ogni volta che esegui cipi app create. Nessun passaggio manuale richiesto.
Salva un token
# GitHub (fine-grained or classic PAT) $ cipi git github-token ghp_xxxxxxxxxxxxxxxxxxxx # GitLab (gitlab.com) $ cipi git gitlab-token glpat-xxxxxxxxxxxxxxxxxxxx # GitLab (self-hosted — set the URL before or after the token) $ cipi git gitlab-url https://gitlab.example.com $ cipi git gitlab-token glpat-xxxxxxxxxxxxxxxxxxxx
Permessi token GitHub
I token fine-grained (consigliati) richiedono Administration e Webhooks impostati su Read and write sui repository target. I token classic richiedono lo scope repo.
Permessi token GitLab
Lo scope api è il minimo richiesto — GitLab non offre uno scope più granulare che copra sia deploy key che webhook.
Ciclo di vita automatico
| Evento | Cosa fa Cipi automaticamente |
|---|---|
app create |
Aggiunge chiave deploy e crea webhook sul repository via API. Il riepilogo mostra "auto-configurato ✓" invece delle istruzioni manuali. |
app edit --repository=... |
Rimuove chiave deploy e webhook dal vecchio repository, poi li aggiunge al nuovo. |
app delete |
Rimuove chiave deploy e webhook dal repository prima di eliminare l'app. |
Comandi cipi git
| Comando | Descrizione |
|---|---|
cipi git status |
Mostra stato connessione provider e dettagli integrazione per app (ID chiave deploy, ID webhook) |
cipi git github-token <token> |
Salva un Personal Access Token GitHub |
cipi git gitlab-token <token> |
Salva un Personal Access Token GitLab |
cipi git gitlab-url <url> |
Imposta l'URL base per un'istanza GitLab self-hosted |
cipi git remove-github |
Rimuovi il token GitHub salvato |
cipi git remove-gitlab |
Rimuovi il token GitLab e l'URL salvati |
Setup manuale (fallback)
L'auto-setup viene saltato quando nessun token è configurato, quando la chiamata API fallisce (permessi errati, repository non trovato, rate limit) o quando il repository è ospitato su un provider diverso da GitHub o GitLab (es. Gitea, Forgejo, Bitbucket). In tutti questi casi Cipi ricade sul workflow manuale e la creazione dell'app procede normalmente.
Per configurare chiave deploy e webhook manualmente:
# print the SSH deploy key to add to your Git provider $ cipi deploy myapp --key # print the webhook URL and token $ cipi deploy myapp --webhook # if using a custom Git server, trust the host fingerprint first $ cipi deploy myapp --trust-host=git.mycompany.com
Poi aggiungili nelle impostazioni del repository del tuo provider:
- Deploy key — GitHub: Settings → Deploy keys → Add deploy key; GitLab: Settings → Repository → Deploy keys
- Webhook — GitHub: Settings → Webhooks → Add webhook; GitLab: Settings → Webhooks → Add new webhook. Imposta payload URL e secret ai valori mostrati da
cipi deploy myapp --webhook
Personalizzare lo script di deploy
La configurazione di deploy per ogni app è salvata in:
Questo file è auto-generato da Cipi durante app create e aggiornato automaticamente quando cambi versione PHP o branch di deploy via cipi app edit. Puoi modificarlo per personalizzare la pipeline di deploy, ma dovresti capire le implicazioni prima di farlo.
Pipeline di deploy predefinita
Il deploy.php auto-generato esegue questi task in ordine:
deploy:prepare // create releases/N/ directory deploy:vendors // composer install --no-dev deploy:shared // link shared/.env and shared/storage/ artisan:migrate // php artisan migrate --force artisan:optimize // php artisan optimize artisan:storage:link // php artisan storage:link deploy:symlink // swap current → releases/N/ atomically cipi:restart-workers // supervisorctl restart myapp-* deploy:cleanup // keep last 5 releases, delete older
Aggiungere task personalizzati
Puoi aggiungere task prima o dopo qualsiasi step. Per un esempio completo di build frontend (npm install && npm run build), vedi Building frontend assets sotto. Altri esempi comuni:
// Run artisan db:seed after migrations after('artisan:migrate', 'artisan:db:seed'); // Clear view cache after symlink swap after('deploy:symlink', 'artisan:view:clear'); // Custom task — send a Slack notification task('notify:slack', function () { run('curl -X POST https://hooks.slack.com/... -d \'{"text":"Deployed!"}\''); }); after('deploy:symlink', 'notify:slack');
Build asset frontend (npm / Vite)
Non esiste un flag CLI cipi dedicato per build frontend (es. npm install && npm run build). Personalizzare deploy.php è l'approccio supportato e previsto — definisci un task() Deployer e aggancialo con after() o before(). Non devi evitarlo; estendere la pipeline è esattamente a cosa serve il file.
Cipi installa Node.js e npm sul server durante il setup. Verifica che siano disponibili come utente dell'app:
$ ssh myapp@your-server-ip
myapp@server:~$ node -v && npm -v
Committa package.json e package-lock.json nel repository. Aggancia la build dopo deploy:shared così .env è collegato (Vite legge le variabili VITE_* da lì) e prima di deploy:symlink così gli asset compilati esistono nella release prima che vada live.
Aggiungi il blocco sotto in fondo a /home/myapp/.deployer/deploy.php, sotto le definizioni task auto-generate di Cipi:
// ── Custom: frontend build (safe zone — keep below Cipi-managed blocks) ── task('npm:build', function () { cd('{{release_path}}'); run('npm ci --no-audit --no-fund && npm run build'); }); // .env is linked → build assets → then migrations / optimize / symlink after('deploy:shared', 'npm:build');
Esegue l'equivalente di npm install && npm run build a ogni deploy. Preferisci npm ci in produzione quando package-lock.json è committato — è più veloce e riproducibile. Usa npm install solo se non blocchi le dipendenze.
Se ti servono step install e build separati (es. per cache node_modules tra le release), dividili in due task:
task('npm:ci', function () {
cd('{{release_path}}');
run('npm ci --no-audit --no-fund');
});
task('npm:build', function () {
cd('{{release_path}}');
run('npm run build');
});
after('deploy:shared', 'npm:ci');
after('npm:ci', 'npm:build');
Per velocizzare i deploy successivi, puoi persistere le dipendenze tra le release aggiungendo node_modules alle directory condivise di Deployer (opzionale — solo se il progetto lo supporta):
add('shared_dirs', ['node_modules']);
Modifica il file sul server come utente dell'app, poi testa con cipi deploy myapp:
$ ssh myapp@your-server-ip myapp@server:~$ nano ~/.deployer/deploy.php # paste the custom tasks at the bottom, save, then as root: $ cipi deploy myapp
npm ci && npm run build in GitHub Actions o GitLab CI prima dello step deploy SSH, così il server riceve solo asset pre-compilati. Vedi Pipeline CI/CD — deploy SSH.Eseguire comandi artisan aggiuntivi
// Seed only in specific environments
task('artisan:db:seed', function () {
run('{{bin/php}} {{release_path}}/artisan db:seed --force');
});
cipi app edit myapp --php=X o cipi app edit myapp --branch=X. Fai backup delle personalizzazioni o tienile in una sezione chiaramente separata dai blocchi gestiti da Cipi. Un pattern sicuro è mettere tutti i task personalizzati in fondo al file dopo la definizione task predefinita.
Disabilitare uno step predefinito
Per saltare un task — ad esempio se gestisci le migration manualmente — commentalo o rimuovilo dalla definizione task deploy:
// Remove the migrate step from the pipeline task('deploy', [ 'deploy:prepare', 'deploy:vendors', 'deploy:shared', // 'artisan:migrate', ← disabled 'artisan:optimize', 'artisan:storage:link', 'deploy:symlink', 'cipi:restart-workers', 'deploy:cleanup', ]);
Testare le modifiche
Dopo aver modificato deploy.php, fai sempre un deploy di test prima di pushare in produzione:
$ cipi deploy myapp # If something goes wrong, instant rollback: $ cipi deploy myapp --rollback # If the deploy is stuck (e.g. interrupted mid-run): $ cipi deploy myapp --unlock
~/logs/deploy.log o via cipi app logs myapp --type=deploy. Controllalo per primo quando risolvi un deploy fallito.
Deploy e CI/CD — Panoramica
Con Cipi, CI (build e test) e CD (release in produzione) possono essere separati o combinati. Ogni deploy esegue in ultima analisi la stessa pipeline Deployer sul server — clone, composer install, migration, swap symlink, restart worker. Ciò che cambia è cosa attiva quella pipeline.
Cipi supporta due modelli di trigger. Per la maggior parte delle app Laravel, parti con webhook + Cipi Agent. Passa a una pipeline CI/CD completa quando ti servono gate, backup o orchestrazione infrastrutturale che un semplice push hook non può esprimere.
Due modi per attivare un deploy
| Webhook + Cipi Agent (consigliato) | Pipeline CI/CD via SSH | |
|---|---|---|
| Trigger | Il Git provider invia POST a /cipi/webhook al push |
Job GitHub Actions / GitLab CI esegue cipi deploy via SSH |
| Accesso server da CI | Nessuno — solo HTTPS verso il dominio dell'app | Chiave SSH dedicata salvata come secret CI |
| Test pre-deploy | Esegui in locale o in un job CI separato; il deploy parte comunque al push a meno che non disabiliti il webhook | Nativo — lo step deploy parte solo dopo che needs: test (o equivalente) passa |
| Backup prima del release | Manuale o cron sul server | Job pipeline — vedi safe deploy |
| App preview / review | Non supportato di serie | La pipeline crea app Cipi per branch — vedi preview environments |
| Complessità di setup | Bassa — composer require cipi/agent + un webhook |
Media — chiave SSH, secret, workflow YAML |
Quale approccio dovrei usare?
| Caso d'uso | Approccio consigliato | Dove approfondire |
|---|---|---|
Singola app Laravel, push-to-deploy su main |
Webhook + Agent | Setup webhook |
| Deploy solo se i test CI passano | Pipeline SSH (disabilita webhook produzione) | Deploy pipeline SSH |
| Backup DB + file prima di ogni release in produzione | Pipeline SSH | Safe deploy con backup |
| Alert Slack / Telegram sull'esito del deploy | Pipeline SSH | Notifiche deploy |
| URL effimero per feature branch (review app) | Pipeline SSH | Ambienti preview |
| Deploy di più app su un server da un solo repo | Entrambi — webhook per app, o una pipeline con cipi deploy paralleli |
Deploy multi-app |
cipi deploy myapp --unlock se resta un lock bloccato.
Deploy automatici — Cipi Agent e webhook
cipi-agent (cipi/agent) è un package Laravel che espone POST /cipi/webhook dentro l'applicazione in esecuzione. Quando GitHub o GitLab invia un evento push, l'agent valida la firma del payload, risponde subito e accoda un deploy sul server — nessun SSH dal runner CI, nessun sudo, nessuna porta inbound aperta oltre HTTPS.
Come funziona il flusso webhook
Il design separa acknowledgement HTTP veloce da lavoro Deployer lento. Un deploy può richiedere diversi minuti; i Git provider fanno timeout delle chiamate HTTP webhook dopo ~10 secondi. Cipi risolve con un file flag e il crontab dell'utente dell'app.
Developer Git provider Your Laravel app (Cipi Agent) Server (app user cron)
│ │ │ │
│ git push main │ │ │
│ ───────────────────────────► │ │ │
│ │ POST /cipi/webhook │ │
│ │ (signed with secret) │ │
│ │ ───────────────────────────► │ │
│ │ │ 1. Verify CIPI_WEBHOOK_TOKEN │
│ │ │ 2. Check branch (CIPI_DEPLOY_BRANCH) │
│ │ │ 3. Write ~/.deploy-trigger │
│ │ ◄─────────────────────────── │ 4. Return 200 immediately │
│ │ │ │
│ │ │ every minute (* * * * *) │
│ │ │ ◄──────────────────────────────────────│
│ │ │ cron sees .deploy-trigger │
│ │ │ removes file, runs Deployer │
│ │ │ in background as app user │
│ │ │ │
│ │ │ clone → composer → migrate │
│ │ │ → symlink swap → workers │
Deployer gira sempre come utente Linux dell'app (es. myapp), con il binario PHP corretto e i permessi file — lo stesso contesto di un cipi deploy myapp manuale. Il webhook non invoca Deployer direttamente; deposita solo il file trigger che il crontab di Cipi già monitora.
Prerequisiti
| Requisito | Perché |
|---|---|
| App Cipi creata con repository Git | La chiave deploy deve clonare il repo — vedi Git auto-setup |
| Almeno un deploy manuale riuscito | Il package agent deve essere presente nella release current prima che la route webhook esista |
composer require cipi/agent nel progetto |
Registra la route /cipi/webhook e la validazione della firma |
| URL webhook raggiungibile via HTTPS | I Git provider richiedono un URL pubblico; usa prima cipi ssl install |
CIPI_WEBHOOK_TOKEN in shared/.env |
Auto-generato a cipi app create; condiviso tra tutte le release |
Setup passo passo
1. Crea l'app e fai deploy una volta manualmente così il server può clonare il repository:
$ cipi app create --user=myapp --domain=myapp.com \ --repository=git@github.com:you/myapp.git --branch=main --php=8.5 $ cipi deploy myapp
2. Installa Cipi Agent nel progetto Laravel in locale, commit e push:
$ composer require cipi/agent $ git add composer.json composer.lock $ git commit -m "Add Cipi Agent for webhook deploys" $ git push origin main $ cipi deploy myapp # one more manual deploy until webhook is live
3. Configura il webhook. Se hai salvato un token GitHub o GitLab, Cipi potrebbe aver già creato il webhook durante app create — verifica con cipi git status. Altrimenti recupera URL e secret:
$ cipi deploy myapp --webhook
Aggiungi il webhook nel tuo Git provider:
| Provider | Payload URL | Secret field | Events |
|---|---|---|---|
| GitHub | https://myapp.com/cipi/webhook |
Secret → valore da --webhook |
Solo l'evento push |
| GitLab | https://myapp.com/cipi/webhook |
Secret token → stesso valore | Push events |
4. Limita al branch di deploy (consigliato per produzione):
CIPI_DEPLOY_BRANCH=main
Imposta in shared/.env via cipi app env myapp. I push su altri branch ricevono risposta skipped e nessun deploy parte.
5. Verifica. Fai push di un piccolo commit su main e monitora il log deploy:
$ cipi app logs myapp --type=deploy # or on the server as the app user: $ tail -f /home/myapp/logs/deploy.log
Entro circa un minuto dalla consegna webhook, dovrebbe comparire una nuova release Deployer. Conferma il commit live con php artisan cipi:status o l'endpoint health check.
Git auto-setup
Quando un token GitHub o GitLab è configurato sul server, Cipi registra la chiave deploy e crea il webhook automaticamente a ogni cipi app create. Il riepilogo app mostra "auto-configurato ✓" invece delle istruzioni manuali. Gli eventi del ciclo di vita (app edit --repository, app delete) mantengono chiavi e webhook sincronizzati.
Risoluzione problemi
| Sintomo | Probabile causa | Correzione |
|---|---|---|
| Il webhook restituisce 404 | Agent non ancora deployato | Esegui cipi deploy myapp dopo aver aggiunto cipi/agent a composer.json
|
| Il webhook restituisce 403 / firma non valida | Secret non corrispondente | Ricopia il token da cipi deploy myapp --webhook nelle impostazioni del provider
|
| 200 OK ma nessun deploy | Branch filtrato | Verifica che CIPI_DEPLOY_BRANCH corrisponda al branch pushato |
| Deploy bloccato / errore lock | Deploy precedente interrotto | cipi deploy myapp --unlock poi riprova |
| Il deploy parte due volte su un push | Webhook + pipeline entrambi attivi | Disabilita un trigger — vedi panoramica |
deploy — usa lo stesso meccanismo .deploy-trigger. Vedi Cipi Agent per health check, MCP e funzionalità anonymizer.Pipeline CI/CD — deploy SSH
Quando il modello webhook non basta, esegui job GitHub Actions o GitLab CI/CD che entrano via SSH nel server e invocano cipi deploy. È la scelta giusta quando il deploy deve essere condizionale — gated su test, preceduto da backup, seguito da notifiche o orchestrazione di nuove app preview.
Quando ti serve una pipeline invece di un webhook
- Quality gate — esegui
php artisan test, analisi statica o build frontend prima che codice raggiunga produzione - Safe release — snapshot del database e
shared/su S3 prima dello swap del symlink (safe deploy) - Visibilità team — posta successo/fallimento su Slack o Telegram con rollback in caso di errore (notifiche deploy)
- Review app — crea o aggiorna un'app Cipi completa per feature branch (ambienti preview)
- Monorepo multi-app — deploy
frontendeapiin parallelo dopo un solo job di test
Per questi workflow, disabilita il webhook di produzione (o non crearlo) così solo la pipeline attiva i deploy. Puoi usare Cipi Agent nell'app per health check e MCP.
Accesso SSH per CI
/root/.ssh/authorized_keys sul server (o all'utente cipi se preferisci sudo cipi deploy) e salva la chiave privata come secret CI. Non riusare chiavi deploy Git o SSH personali.
# on your local machine $ ssh-keygen -t ed25519 -C "ci-deploy" -f ~/.ssh/ci_deploy -N "" # copy the public key to the server $ ssh-copy-id -i ~/.ssh/ci_deploy.pub root@your-server-ip # copy the private key content → add it as a CI secret (SERVER_SSH_KEY) $ cat ~/.ssh/ci_deploy
Salva SERVER_HOST (IP o hostname del server) insieme a SERVER_SSH_KEY nei secret del repository (GitHub) o nelle variabili CI/CD (GitLab).
GitHub Actions — test poi deploy
Aggiungi la chiave privata come secret del repository chiamato SERVER_SSH_KEY e l'IP del server come SERVER_HOST.
# .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests run: php artisan test deploy: runs-on: ubuntu-latest needs: test # only deploy if tests pass steps: - name: Deploy via Cipi uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: cipi key: ${{ secrets.SERVER_SSH_KEY }} script: sudo cipi deploy myapp
Per rollback in caso di errore, estendi lo step script:
script: |
sudo cipi deploy myapp || (sudo cipi deploy myapp --rollback && exit 1)
GitLab CI / CD
Aggiungi la chiave privata come variabile CI/CD chiamata SERVER_SSH_KEY (tipo: File) e l'IP del server come SERVER_HOST.
# .gitlab-ci.yml
stages:
- test
- deploy
test:
stage: test
script:
- php artisan test
deploy:
stage: deploy
environment: production
only:
- main
before_script:
- apt-get install -y openssh-client
- eval $(ssh-agent -s)
- echo "$SERVER_SSH_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts
script:
- ssh root@$SERVER_HOST "cipi deploy myapp"
Con rollback in caso di errore:
script:
- ssh root@$SERVER_HOST "cipi deploy myapp || (cipi deploy myapp --rollback && exit 1)"
Deploy multi-app
Se la stessa pipeline gestisce più app sullo stesso server:
# GitHub Actions — deploy multiple apps in parallel
- name: Deploy
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: root
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cipi deploy frontend &
cipi deploy api &
wait
Pattern pipeline avanzati
Quando il deploy SSH funziona, componi queste sezioni in un unico workflow di produzione:
| Pattern | Cosa aggiunge la pipeline | Guida |
|---|---|---|
| Notifiche | Messaggio Slack o Telegram su successo, fallimento e auto-rollback | Notifiche deploy |
| Safe deploy | cipi db backup + cipi backup run prima di cipi deploy; rollback in caso di errore
|
Safe deploy con backup |
| Ambienti preview | Crea/aggiorna/elimina app Cipi per branch con DNS wildcard + SSL | Ambienti preview |
Un setup maturo tipico usa il webhook per un'app staging (feedback istantaneo a ogni push) e una pipeline per produzione (test → backup → deploy → notify). Ogni app ha il proprio trigger — non entrano mai in conflitto perché puntano a utenti app Cipi diversi.
Notifiche deploy
Caso d'uso pipeline: il percorso webhook deploya in silenzio — Git restituisce 200 e il team lo scopre solo guardando i log. Con una pipeline SSH, aggiungi step di notifica dopo cipi deploy per diffondere successo, fallimento e rollback automatici su Slack o Telegram. Entrambi gli esempi sotto funzionano con GitHub Actions e GitLab CI usando solo chiamate HTTP standard — nessuna dipendenza extra di piattaforma.
Slack
Aggiungi uno step finale che posta su un webhook Slack indipendentemente dall'esito del deploy. Usa if: always() in GitHub Actions così la notifica parte sia in successo che in fallimento.
Crea un Incoming Webhook nel workspace Slack e salva l'URL come SLACK_WEBHOOK_URL nei secret CI.
# GitHub Actions — deploy + Slack notification
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
id: deploy
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: root
key: ${{ secrets.SERVER_SSH_KEY }}
script: cipi deploy myapp
- name: Notify Slack — success
if: success()
uses: slackapi/slack-github-action@v2
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": ":white_check_mark: *myapp* deployed successfully",
"attachments": [{
"color": "good",
"fields": [
{ "title": "Branch", "value": "${{ github.ref_name }}", "short": true },
{ "title": "By", "value": "${{ github.actor }}", "short": true },
{ "title": "Commit", "value": "${{ github.sha }}", "short": false }
]
}]
}
- name: Notify Slack — failure
if: failure()
uses: slackapi/slack-github-action@v2
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": ":x: *myapp* deploy FAILED — rolling back",
"attachments": [{
"color": "danger",
"fields": [
{ "title": "Branch", "value": "${{ github.ref_name }}", "short": true },
{ "title": "By", "value": "${{ github.actor }}", "short": true },
{ "title": "Run", "value": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", "short": false }
]
}]
}
- name: Rollback on failure
if: failure()
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: root
key: ${{ secrets.SERVER_SSH_KEY }}
script: cipi deploy myapp --rollback
Per GitLab CI, usa curl direttamente — nessun plugin necessario:
# .gitlab-ci.yml — deploy stage with Slack notification
deploy:
stage: deploy
script:
- ssh root@$SERVER_HOST "cipi deploy myapp" && export DEPLOY_STATUS="success" || export DEPLOY_STATUS="failed"
- |
if [ "$DEPLOY_STATUS" = "success" ]; then
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"text\":\":white_check_mark: *myapp* deployed by $GITLAB_USER_LOGIN on \`$CI_COMMIT_REF_NAME\`\"}"
else
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"text\":\":x: *myapp* deploy FAILED — <$CI_PIPELINE_URL|view pipeline>\"}"
ssh root@$SERVER_HOST "cipi deploy myapp --rollback"
exit 1
fi
Telegram
Crea un bot Telegram via @BotFather, ottieni il token del bot e trova l'ID chat/gruppo. Salvali come TELEGRAM_BOT_TOKEN e TELEGRAM_CHAT_ID nei secret CI.
# GitHub Actions — deploy + Telegram notification
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
id: deploy
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: root
key: ${{ secrets.SERVER_SSH_KEY }}
script: cipi deploy myapp
- name: Notify Telegram — success
if: success()
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d parse_mode="Markdown" \
-d text="✅ *myapp* deployed successfully%0ABranch: \`${{ github.ref_name }}\`%0ABy: ${{ github.actor }}"
- name: Notify Telegram — failure + rollback
if: failure()
run: |
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
-d parse_mode="Markdown" \
-d text="❌ *myapp* deploy FAILED — rolling back%0ABranch: \`${{ github.ref_name }}\`%0A[View run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
ssh -o StrictHostKeyChecking=no -i <(echo "${{ secrets.SERVER_SSH_KEY }}") \
root@${{ secrets.SERVER_HOST }} "cipi deploy myapp --rollback"
Equivalente GitLab CI (solo curl, nessuna dipendenza extra):
# .gitlab-ci.yml — deploy stage with Telegram notification
deploy:
stage: deploy
script:
- ssh root@$SERVER_HOST "cipi deploy myapp" && RESULT="✅ deployed" || RESULT="❌ FAILED"
- |
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d chat_id="$TELEGRAM_CHAT_ID" \
-d parse_mode="Markdown" \
-d text="*myapp* ${RESULT}%0ABranch: \`$CI_COMMIT_REF_NAME\`%0ABy: $GITLAB_USER_LOGIN"
- |
if echo "$RESULT" | grep -q "FAILED"; then
ssh root@$SERVER_HOST "cipi deploy myapp --rollback"
exit 1
fi
https://api.telegram.org/bot<TOKEN>/getUpdates e cerca il campo chat.id nella risposta. Per chat private, scrivi prima al bot.
Safe deploy — backup prima del release
Caso d'uso pipeline: un deploy webhook non può eseguire uno step backup prima di rilasciare codice — l'evento push attiva il deploy subito. In una pipeline CI/CD, aggiungi uno stage backup dedicato che deve avere successo prima che parta deploy. Un workflow production-grade dovrebbe sempre creare un punto di restore prima che il nuovo codice vada live. Cipi fornisce due comandi backup complementari che mappano a due livelli di sicurezza diversi:
# local DB snapshot — fast, on-disk, instant rollback $ cipi db backup myapp # → /var/log/cipi/backups/myapp_20260303_143012.sql.gz # S3 backup — DB dump + shared/ folder uploaded to your bucket $ cipi backup run myapp # → s3://your-bucket/cipi/myapp/2026-03-03_143015/db.sql.gz # → s3://your-bucket/cipi/myapp/2026-03-03_143015/shared.tar.gz
Usati insieme in una pipeline, ti danno sia un punto di restore locale veloce sia una copia off-server del database e di tutti i file caricati. Il deploy parte solo se entrambi i backup hanno successo.
cipi backup configure una volta sul server per collegare le credenziali S3 prima che cipi backup run possa essere usato. cipi db backup funziona senza configurazione — è sempre disponibile.
Cosa fa ogni comando internamente
cipi db backup <app> chiama mysqldump --single-transaction --routines --triggers e comprime l'output in /var/log/cipi/backups/<app>_<timestamp>.sql.gz. Il file resta sul server e non viene mai eliminato automaticamente — aggiungi uno step cleanup o un cron se lo spazio disco conta.
cipi backup run <app> fa due cose: dump del database con mariadb-dump --single-transaction in una dir temp, e archivia l'intera cartella /home/<app>/shared/ (che contiene .env, storage/ e file caricati dagli utenti). Entrambi gli archivi vengono caricati su S3 sotto il path cipi/<app>/<timestamp>/. I file temp vengono eliminati dopo upload riuscito.
GitHub Actions — workflow safe deploy
# .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: php artisan test backup: runs-on: ubuntu-latest needs: test steps: - name: Local DB backup uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: root key: ${{ secrets.SERVER_SSH_KEY }} script: cipi db backup myapp - name: S3 backup (DB + shared) uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: root key: ${{ secrets.SERVER_SSH_KEY }} script: cipi backup run myapp deploy: runs-on: ubuntu-latest needs: backup # only runs if backup job succeeds steps: - name: Deploy uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: root key: ${{ secrets.SERVER_SSH_KEY }} script: cipi deploy myapp - name: Rollback on failure if: failure() uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: root key: ${{ secrets.SERVER_SSH_KEY }} script: | cipi deploy myapp --rollback echo "Deploy failed — rolled back to previous release"
Il grafo job impone l'ordine: test → backup → deploy. Se un job fallisce, i successivi vengono saltati. Se lo step deploy fallisce, lo step rollback parte automaticamente e ripristina la release Deployer precedente.
GitLab CI/CD — pipeline safe deploy
# .gitlab-ci.yml
stages:
- test
- backup
- deploy
variables:
APP: myapp
.ssh: &ssh
before_script:
- apt-get install -y openssh-client
- eval $(ssh-agent -s)
- echo "$SERVER_SSH_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- ssh-keyscan -H "$SERVER_HOST" >> ~/.ssh/known_hosts
test:
stage: test
script: php artisan test
only: [main]
backup-local:
stage: backup
<<: *ssh
only: [main]
script:
- ssh root@$SERVER_HOST "cipi db backup $APP"
backup-s3:
stage: backup
<<: *ssh
only: [main]
script:
- ssh root@$SERVER_HOST "cipi backup run $APP"
deploy:
stage: deploy
<<: *ssh
only: [main]
script:
- |
ssh root@$SERVER_HOST "
cipi deploy $APP || {
cipi deploy $APP --rollback
echo 'Deploy failed — rolled back'
exit 1
}
"
after_script:
- echo "Released → https://myapp.com"
backup-local e backup-s3 sono nello stesso stage così girano in parallelo se hai più runner, riducendo il tempo totale della pipeline. Entrambi devono avere successo prima che parta lo stage deploy.
Ripristino da backup locale
Se devi riportare il database allo snapshot preso appena prima del deploy:
# list available local snapshots $ ls -lh /var/log/cipi/backups/myapp_*.sql.gz # restore the most recent one $ cipi db restore myapp /var/log/cipi/backups/myapp_20260303_143012.sql.gz # also roll back the code release $ cipi deploy myapp --rollback
Ripristino da backup S3
# list available S3 snapshots for this app $ cipi backup list myapp # download the DB snapshot from S3 $ aws s3 cp s3://your-bucket/cipi/myapp/2026-03-03_143015/db.sql.gz /tmp/db.sql.gz # restore the database $ cipi db restore myapp /tmp/db.sql.gz # (optional) restore shared/ files $ aws s3 cp s3://your-bucket/cipi/myapp/2026-03-03_143015/shared.tar.gz /tmp/shared.tar.gz $ tar -xzf /tmp/shared.tar.gz -C /home/myapp/
.sql.gz in /var/log/cipi/backups/. Con un calendario deploy intenso, aggiungi un cron di cleanup o mantieni solo gli ultimi N file:ls -t /var/log/cipi/backups/myapp_*.sql.gz | tail -n +6 | xargs rm -fQuesto esempio mantiene gli 5 snapshot più recenti ed elimina quelli più vecchi.
Ambienti preview (deploy per branch)
Caso d'uso pipeline: i webhook puntano a un solo URL di produzione — non possono avviare una nuova app Cipi per branch. Gli ambienti preview richiedono una pipeline CI/CD che entra via SSH nel server, calcola un nome app deterministico dal branch ed esegue cipi app create o cipi deploy di conseguenza. Ogni branch non di produzione può avere il proprio URL live — un'app Laravel completamente deployata con database, worker e HTTPS propri. Questo pattern è a volte chiamato "review app" o "ambienti effimeri".
Il formato URL usa tre slug separati da trattini, così ogni ambiente è leggibile e globalmente univoco:
https://develop-acmeco-3a1f9c2e.preview.domain.ltd https://release-1-2-3-acmeco-3a1f9c2e.preview.domain.ltd https://main-acmeco-3a1f9c2e.preview.domain.ltd
Come vengono generati gli identificativi
Tre valori sono derivati a runtime della pipeline:
# branch name → lowercase, non-alphanum → hyphens, trim edges BRANCH_SLUG=$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//') # repo/project name → same treatment PROJECT_SLUG=$(echo "$PROJECT" | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g') # deterministic MD5 hash — same branch always gets the same environment HASH=$(echo -n "${BRANCH_SLUG}${PROJECT_SLUG}" | md5sum | cut -c1-8) # Cipi app username: must be lowercase alphanumeric, 3–32 chars, no hyphens # hex chars (0–9, a–f) are valid; prefix "pr" ensures it starts with a letter APP_NAME="pr${HASH}" # e.g. pr3a1f9c2e # human-readable domain with wildcard base DOMAIN="${BRANCH_SLUG}-${PROJECT_SLUG}-${HASH}.${DEPLOY_WILDCARD_DOMAIN}"
Prerequisiti (setup server una tantum)
A *.preview.domain.ltd → <server-ip> nel tuo DNS provider. Tutti i sottodomini si risolvono automaticamente; nessuna modifica DNS per branch.2. Certificato SSL wildcard — ottieni un cert wildcard via challenge DNS-01 una volta e installalo sul server. Vedi la sezione Wildcard domains per le istruzioni. Il path cert usato dagli esempi pipeline sotto è
/etc/letsencrypt/live/preview.domain.ltd/.3. Accesso repository — gli esempi pipeline usano un URL HTTPS con personal access token (PAT) incorporato, quindi non serve setup chiave deploy SSH per app. Il token richiede solo accesso read al repository.
GitHub Actions
Aggiungi questi secret al repository: SERVER_HOST, SERVER_SSH_KEY, DEPLOY_WILDCARD_DOMAIN (es. preview.domain.ltd), GH_PAT (un PAT fine-grained con accesso read al repo).
# .github/workflows/preview.yml name: Preview on: push: branches-ignore: [main, master] # main branch uses your production pipeline delete: # clean up when a branch is deleted jobs: deploy: if: github.event_name == 'push' runs-on: ubuntu-latest steps: - name: Compute identifiers id: ids run: | BRANCH_SLUG=$(echo "${{ github.ref_name }}" \ | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//') PROJECT_SLUG=$(echo "${{ github.event.repository.name }}" \ | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g') HASH=$(echo -n "${BRANCH_SLUG}${PROJECT_SLUG}" | md5sum | cut -c1-8) APP_NAME="pr${HASH}" DOMAIN="${BRANCH_SLUG}-${PROJECT_SLUG}-${HASH}.${{ secrets.DEPLOY_WILDCARD_DOMAIN }}" REPO="https://oauth2:${{ secrets.GH_PAT }}@github.com/${{ github.repository }}.git" echo "app_name=${APP_NAME}" >> "$GITHUB_OUTPUT" echo "domain=${DOMAIN}" >> "$GITHUB_OUTPUT" echo "repo_url=${REPO}" >> "$GITHUB_OUTPUT" - name: Create or update preview uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: root key: ${{ secrets.SERVER_SSH_KEY }} script: | APP="${{ steps.ids.outputs.app_name }}" DOMAIN="${{ steps.ids.outputs.domain }}" REPO="${{ steps.ids.outputs.repo_url }}" BRANCH="${{ github.ref_name }}" WILDCARD="/etc/letsencrypt/live/${{ secrets.DEPLOY_WILDCARD_DOMAIN }}" if cipi app show "$APP" &>/dev/null; then echo "→ Updating: $APP" cipi deploy "$APP" else echo "→ Creating: $APP → $DOMAIN" cipi app create \ --user="$APP" \ --domain="$DOMAIN" \ --repository="$REPO" \ --branch="$BRANCH" \ --php=8.5 # Patch nginx to listen on 443 using the pre-installed wildcard cert awk -v cert="$WILDCARD" ' /^ listen 80;/ { print print " listen 443 ssl http2;" print " ssl_certificate " cert "/fullchain.pem;" print " ssl_certificate_key " cert "/privkey.pem;" next } { print } ' "/etc/nginx/sites-available/$APP" > /tmp/_cipi_vhost \ && mv /tmp/_cipi_vhost "/etc/nginx/sites-available/$APP" nginx -t && systemctl reload nginx cipi deploy "$APP" fi - name: Print preview URL run: | echo "" echo " Preview → https://${{ steps.ids.outputs.domain }}" echo "" cleanup: if: github.event_name == 'delete' runs-on: ubuntu-latest steps: - name: Compute identifiers id: ids run: | BRANCH_SLUG=$(echo "${{ github.event.ref }}" \ | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//') PROJECT_SLUG=$(echo "${{ github.event.repository.name }}" \ | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g') HASH=$(echo -n "${BRANCH_SLUG}${PROJECT_SLUG}" | md5sum | cut -c1-8) echo "app_name=pr${HASH}" >> "$GITHUB_OUTPUT" - name: Delete preview uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: root key: ${{ secrets.SERVER_SSH_KEY }} script: | APP="${{ steps.ids.outputs.app_name }}" if cipi app show "$APP" &>/dev/null; then echo "y" | cipi app delete "$APP" echo "→ Deleted: $APP" else echo "→ Not found, nothing to delete" fi
GitLab CI/CD
Aggiungi queste variabili CI/CD: SERVER_HOST, SERVER_SSH_KEY (tipo File), DEPLOY_WILDCARD_DOMAIN, GL_TOKEN (token accesso project/gruppo con scope read_repository).
# .gitlab-ci.yml stages: - preview - cleanup .ssh_setup: &ssh_setup before_script: - apt-get install -y openssh-client - eval $(ssh-agent -s) - echo "$SERVER_SSH_KEY" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh - ssh-keyscan -H "$SERVER_HOST" >> ~/.ssh/known_hosts .compute_ids: &compute_ids | BRANCH_SLUG=$(echo "$CI_COMMIT_REF_NAME" \ | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//') PROJECT_SLUG=$(echo "$CI_PROJECT_NAME" \ | tr '[:upper:]' '[:lower:]' \ | sed 's/[^a-z0-9]/-/g') HASH=$(echo -n "${BRANCH_SLUG}${PROJECT_SLUG}" | md5sum | cut -c1-8) APP="pr${HASH}" DOMAIN="${BRANCH_SLUG}-${PROJECT_SLUG}-${HASH}.${DEPLOY_WILDCARD_DOMAIN}" REPO="https://oauth2:${GL_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" WILDCARD="/etc/letsencrypt/live/${DEPLOY_WILDCARD_DOMAIN}" deploy-preview: stage: preview <<: *ssh_setup except: - main - master script: - *compute_ids - | ssh root@$SERVER_HOST bash -s << ENDSSH APP="$APP" DOMAIN="$DOMAIN" REPO="$REPO" BRANCH="$CI_COMMIT_REF_NAME" WILDCARD="$WILDCARD" if cipi app show "\$APP" &>/dev/null; then echo "Updating: \$APP" cipi deploy "\$APP" else echo "Creating: \$APP → \$DOMAIN" cipi app create \ --user="\$APP" \ --domain="\$DOMAIN" \ --repository="\$REPO" \ --branch="\$BRANCH" \ --php=8.5 awk -v cert="\$WILDCARD" ' /^ listen 80;/ { print print " listen 443 ssl http2;" print " ssl_certificate " cert "/fullchain.pem;" print " ssl_certificate_key " cert "/privkey.pem;" next } { print } ' "/etc/nginx/sites-available/\$APP" > /tmp/_cipi_vhost \ && mv /tmp/_cipi_vhost "/etc/nginx/sites-available/\$APP" nginx -t && systemctl reload nginx cipi deploy "\$APP" fi ENDSSH - echo "Preview → https://$DOMAIN" cleanup-preview: stage: cleanup <<: *ssh_setup only: - branches when: manual # or trigger on MR merge via rules: script: - *compute_ids - | ssh root@$SERVER_HOST " APP='$APP' if cipi app show \"\$APP\" &>/dev/null; then echo 'y' | cipi app delete \"\$APP\" fi "
cleanup-preview automaticamente quando una merge request viene mergiata aggiungendo un blocco rules: che controlla $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" o usando un workflow: dedicato con if: $CI_PIPELINE_SOURCE == "merge_request_event".
Note e limiti
cipi app list periodicamente ed elimina preview obsolete.La patch SSL nginx non è idempotente — se la pipeline esegue
cipi app create due volte (es. per un retry), la patch awk viene applicata di nuovo. L'hash garantisce che APP_NAME sia deterministico, quindi la guardia if cipi app show previene la doppia creazione in condizioni normali.Evita di eseguire
cipi ssl install su un'app preview — sovrascriverà la config cert wildcard con un cert Let's Encrypt per dominio che fallirà (il dominio non ha record DNS dedicato, solo il wildcard).