061 —Workflow
SFTP-only deployment: een recept dat de developer overleeft
Een omkeerbare SFTP-deploy zo opgezet dat de volgende eigenaar van de credentials de hele release-flow vanaf alleen de server kan reconstrueren.
De overdracht die er nooit was
Een Nederlands bureau waar we mee werken nam vorige maand een WordPress-site over nadat de vorige freelancer geen e-mail meer beantwoordde. Ze hadden drie dingen: een SFTP-credential, een MySQL-connection string en een wp-admin login. Geen git-repository, geen deploy-script, geen README, geen idee welke bestanden in /wp-content/mu-plugins/ nog gebruikt werden en welke dood gewicht waren uit 2019.
Dit is het standaardgeval voor een klein bureau dat een verouderde site erft. Geen kwade opzet, gewoon verloop. De persoon die de site bouwde is verder gegaan, de documentatie heeft nooit bestaan, en de nieuwe eigenaar staart naar een tree van 14.000 bestanden en vraagt zich af welke edit de homepage offline gaat halen.
Wat volgt is het SFTP-only deployment-recept dat we aan teams in die positie meegeven. Het gaat van niets meer uit dan wat in de credentials-mail stond, en het is zo opgezet dat de volgende persoon, nadat jij ook vertrekt, de hele flow vanaf alleen de server kan reverse-engineeren.
Eerst spiegelen, dan pas bewerken
Voor je iets bewerkt: trek de live tree naar je eigen schijf. Niet als back-up, maar als de working copy. Elke wijziging die je maakt moet lokaal ontstaan en naar boven reizen, nooit andersom.
lftp -u user,pass sftp://site.example.com \
-e "mirror --parallel=4 --verbose /public_html ./live; quit"Dat levert je een getrouwe snapshot. Commit hem meteen in een private git-repo, ook al push je hem nergens heen:
cd live
git init
git add -A
git commit -m "snapshot $(date -u +%Y-%m-%dT%H:%M:%SZ) from site.example.com"De repo is voor jou, niet voor de server. Het doel is dat je elke keer dat je opnieuw spiegelt een diff hebt, zodat je kunt zien wat cronjobs, plugins of andere admins onder je voeten hebben veranderd.
Een deploy van één shell-command en één symlink
Het patroon dat een overdracht overleeft, is het patroon dat een onbekende rechtstreeks van de server kan aflezen. Gebruik een release-directory per deploy, gekoppeld aan een timestamp, en een current symlink die wijst naar de versie die live is. Apache's DocumentRoot volgt de symlink, de wissel is atomic, en rollback is één ln -sfn.
#!/usr/bin/env bash
# deploy.sh - local, one-shot SFTP release
set -euo pipefail
STAMP=$(date -u +%Y%m%d-%H%M%S)
REMOTE="/home/user/releases/$STAMP"
lftp -u "$SFTP_USER,$SFTP_PASS" "sftp://$SFTP_HOST" <<EOF
mkdir -p $REMOTE
mirror -R --parallel=4 --exclude-glob .git --exclude-glob node_modules/ ./live $REMOTE
rm -f /home/user/public_html
ln -s $REMOTE /home/user/public_html
quit
EOF
echo "released $STAMP"Nu heb je een directory met gedateerde releases op de server. Een rollback is precies dit:
ssh user@site.example.com 'ln -sfn /home/user/releases/20260601-093014 /home/user/public_html'Heb je alleen SFTP en geen shell, dan werkt hetzelfde door een mini-switch.php te uploaden die een timestamp als parameter neemt en PHP's symlink() aanroept. Verwijder het helper-bestand na elk gebruik, anders blijft het daar staan als rollback-endpoint zonder auth.
De database-kant vastpinnen
Bestanden zijn de makkelijke helft. De database is waar onbeheerde sites rare dingen gaan doen. Het recept heeft een dump nodig die bij elke deploy meedraait en naast de release-directory landt, zodat een toekomstige lezer ziet dat release N hoorde bij database-staat N.
mysqldump --single-transaction --quick --routines --triggers \
-h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" \
| gzip > "./live/.releases/$STAMP.sql.gz"De mysqldump-handleiding behandelt bovenstaande flags; de belangrijkste voor een live site zijn --single-transaction (zodat je writes niet locked) en --quick (zodat het streamt in plaats van de hele tabel in geheugen te buffer). Upload dat bestand mee met de rest van de release. De directory /.releases/ in de root van de document tree is conventioneel genoeg dat een toekomstige lezer de meest recente .sql.gz opent en begrijpt waar hij naar kijkt zonder dat iemand het hem uitlegt.
Koppel elke dump aan een retentie-sweep, anders loopt je schijf vol. Heb je shell-toegang:
find /home/user/releases -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;Heb je alleen SFTP, schrijf dan een PHP-cleanup-script van twintig regels dat de deploy uploadt en via een eenmalige URL triggert, waarna het zichzelf verwijdert.
De README die op de server woont
Alles hierboven betekent niets als de volgende persoon het niet kan vinden. Plaats één bestand op /public_html/HANDOVER.md dat de structuur in begrijpelijk Nederlands uitlegt. Drie korte secties is genoeg:
- Releases: waar ze staan, hoe de
currentsymlink werkt, hoe je rollbackt. - Database: waar de dumps staan, hoe het credentials-bestand heet, waar wp-config.php staat.
- Cron: de output van
crontab -l, ook als hij leeg is. Juist als hij leeg is.
Voeg dit toe aan .htaccess, zodat het bestand leesbaar is voor de server maar niet voor de buitenwereld:
<Files "HANDOVER.md">
Require all denied
</Files>De Apache-mod_authz_core-documentatie bevat de volledige lijst directives als je het strakker wilt scopen, bijvoorbeeld op één kantoor-IP. Waar het om gaat: de documentatie staat op dezelfde plek als wat hij documenteert. Raakt het bureau zijn password manager kwijt, dan reist de README nog steeds mee met de site.
Wat je vandaag kunt doen
Toen we Pier bouwden liepen we precies hier tegenaan: kleine teams die halverwege de levensloop een site overnemen zonder deploy-verhaal. De manier waarop wij het uiteindelijk hebben opgelost, was elke edit standaard een release maken, met volledige versiehistorie per bestand en een MySQL editor die tabellen samen met de file tree snapshot.
De kleinste variant van dit alles is een mirror, een git init en één HANDOVER.md die je committed voor je iets aanraakt. Twintig minuten werk waarmee je een geërfde site verandert in iets dat de volgende persoon ook kan erven.
— Vragen —
Wat als de host PHP-symlink() blokkeert?
Gebruik een deploy gebaseerd op rename. Upload naar een sibling-directory, hernoem dan de oude public_html opzij en de nieuwe op zijn plek. SFTP ondersteunt rename ook als shell-toegang en symlinks uitstaan.
Heb ik ergens een git-server nodig?
Nee. De lokale git-repo bestaat alleen om je diffs te geven als je opnieuw spiegelt. Push hem later naar een private host als het team offsite back-up wil, maar de deploy zelf raakt git nooit aan.
Hoe lang moet ik oude releases op de server bewaren?
Dertig dagen is een verstandige default voor sites met weinig verkeer. Elke release is ongeveer zo groot als de live tree, dus reken op tree-grootte maal aantal-retenties aan schijfruimte.