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.

Dalla v4.5.4 Cipi include Deployer 8, che richiede PHP ≥ 8.3. 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.

  1. Ferma i queue worker (cipi worker stop)
  2. Clona il repo in releases/N/
  3. Esegue composer install --no-dev (con il PHP dell'app)
  4. Collega shared/.env e shared/storage/
  5. Esegue artisan migrate --force
  6. Esegue artisan optimize
  7. Esegue artisan storage:link
  8. Scambia il symlink current atomicamente
  9. Riavvia i queue worker
  10. Elimina release vecchie (mantieni le ultime 5)
bash
$ 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='…'.

Se un deploy viene interrotto (es. per un errore di rete), Deployer può lasciare un file di lock. Usa 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.

bash
$ 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.

Ogni operazione 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:

bash
# 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
Quando è specificata una porta non standard, Cipi scrive anche la voce 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

bash
# 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:

bash
# 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
Se rimuovi un token provider dopo che app sono state create con auto-setup, Cipi non potrà pulire chiavi deploy e webhook quando elimini o modifichi quelle app. Viene mostrato un avviso e dovrai rimuoverli manualmente dalle impostazioni del repository del provider.

Personalizzare lo script di deploy

La configurazione di deploy per ogni app è salvata in:

/home/myapp/.deployer/deploy.php

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:

php
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:

php
// 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:

bash
$ 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:

php
// ── 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:

php
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):

php
add('shared_dirs', ['node_modules']);

Modifica il file sul server come utente dell'app, poi testa con cipi deploy myapp:

bash
$ 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
Alternativa: esegui 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

php
// Seed only in specific environments
task('artisan:db:seed', function () {
    run('{{bin/php}} {{release_path}}/artisan db:seed --force');
});
Cipi può sovrascrivere deploy.php quando esegui 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:

php
// 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:

bash
$ 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
Il log deploy è sempre disponibile in ~/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
Scegli un trigger per app. Non lasciare un webhook di produzione attivo mentre esegui anche deploy pipeline al push — due esecuzioni Deployer concorrenti entrano in conflitto sul file di lock. Usa 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.

flow
  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:

bash
$ 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:

bash
$ 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:

bash
$ 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):

env
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:

bash
$ 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
L'agent supporta anche deploy manuali e attivati da AI via tool MCP 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 frontend e api in 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

Genera una coppia di chiavi ed25519 dedicata per il runner CI. Aggiungi la chiave pubblica a /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.
bash
# 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.

yaml
# .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:

yaml
          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.

yaml
# .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:

yaml
  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:

yaml
# 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.

yaml
# 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:

yaml
# .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.

yaml
# 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):

yaml
# .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
Per trovare l'ID chat Telegram, aggiungi il bot al gruppo/canale target, poi chiama 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:

bash
# 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.

Prerequisito: esegui 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

yaml
# .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: testbackupdeploy. 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

yaml
# .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:

bash
# 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

bash
# 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/
I backup locali non vengono mai eliminati automaticamente. Ogni deploy aggiunge un nuovo file .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 -f

Questo 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:

example URLs
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:

bash
# 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)

1. DNS wildcard — aggiungi un record 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).

yaml
# .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).

yaml
# .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
      "
In GitLab, puoi attivare 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

Ogni app preview è un'app Cipi completa — ottiene utente Linux, database, pool FPM, worker Supervisor e crontab propri. Su un VPS piccolo si accumulano in fretta. Esegui 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).