kenneth@vps~cat README.md
Zelfgehoste Linux-VPS
Een zelfbeheerde Hetzner-VPS die mijn productieworkloads draait — CollectorWWII, klantenprojecten en deze site. Ubuntu-hardening, Docker-services, Nginx reverse proxying, Cloudflare DNS/WAF, Bash-monitoring met Discord-meldingen en automatische offsite back-ups.
kenneth@vps~/docscat topology.txt
01 / overzicht van de stack
Requestflow
Media en opslag
Ondersteunende services
kenneth@vps/etc/sshsudo cat sshd_config
02 / serverhardening
SSH-hardening
Rootlogin en wachtwoordauthenticatie zijn uitgeschakeld — enkel SSH-sleutelparen worden aanvaard, op een niet-standaardpoort.
# /etc/ssh/sshd_config PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes AllowUsers kenneth Port 2222
Gebruikersbeheer
Een toegewijde niet-rootgebruiker bezit de applicatiebestanden; sudo is beperkt tot specifieke herstartcommando's van services.
- Applicatiebestanden in handen van
www-data - Deploygebruiker heeft geen interactieve shell
- Sudoers-regel beperkt tot herstartcommando's per service
kenneth@vps~sudo ufw status
UFW-firewall
Enkel de poorten die de server echt nodig heeft zijn toegelaten; al het overige inkomende verkeer wordt standaard geweigerd.
# Allow only what is needed ufw default deny incoming ufw default allow outgoing ufw allow 2222/tcp # SSH ufw allow 80/tcp # HTTP (redirect only) ufw allow 443/tcp # HTTPS ufw enable
kenneth@vps~sudo fail2ban-client status sshd
Fail2ban + automatische updates
Fail2ban blokkeert IP's na herhaalde mislukte SSH-logins; unattended-upgrades installeert beveiligingspatches automatisch.
- Fail2ban SSH-jail: 5 pogingen, 1 u ban
- Nginx 403/404-floodbescherming via custom jail
unattended-upgradesgeconfigureerd voor beveiligingspakketten
kenneth@vps/etc/nginx/sites-availablecat kennethclauwaert.com
03 / nginx reverse proxy
Virtualhost-structuur
Elk domein krijgt zijn eigen serverblok. SSL wordt op de Nginx-laag getermineerd met Let's Encrypt-certificaten via Certbot.
# /etc/nginx/sites-available/collectorwwii.eu server { listen 80; server_name collectorwwii.eu www.collectorwwii.eu; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name collectorwwii.eu; ssl_certificate /etc/letsencrypt/live/collectorwwii.eu/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/collectorwwii.eu/privkey.pem; root /var/www/collectorwwii/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }
SSL- + performantieconfig
Gedeelde SSL-parameters staan in één include zodat elke virtual host consistent blijft; gzip staat aan voor tekstbestanden.
# /etc/nginx/conf.d/ssl_params.conf ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; # /etc/nginx/conf.d/gzip.conf gzip on; gzip_types text/plain text/css application/json application/javascript text/xml; gzip_min_length 1024;
Automatische Certbot-vernieuwing
Certificaten vernieuwen via een systemd-timer; Nginx herlaadt
automatisch na vernieuwing met --deploy-hook.
kenneth@vps~systemctl status php8.2-fpm laravel-worker
04 / php-fpm en laravel
PHP-FPM-poolconfiguratie
Elke applicatie draait in haar eigen pool onder een eigen systeemgebruiker, zodat de fouten van de ene app een andere niet raken.
# /etc/php/8.2/fpm/pool.d/collectorwwii.conf [collectorwwii] user = www-data group = www-data listen = /run/php/php8.2-fpm.sock pm = dynamic pm.max_children = 10 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3
Queue worker + scheduler
De queue worker en scheduler draaien als systemd-services, niet als kale cron — herstarten-bij-falen en journald-logging gratis erbij.
# /etc/systemd/system/collectorwwii-worker.service [Unit] Description=CollectorWWII Queue Worker After=network.target mysql.service redis.service [Service] User=www-data WorkingDirectory=/var/www/collectorwwii ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target
kenneth@vps~dig kennethclauwaert.com +short
05 / cloudflare — dns, cdn, waf
DNS en proxying
Alle domeinen zijn DNS-beheerd via Cloudflare; orange-cloud proxymodus verbergt het origin-IP en biedt DDoS-mitigatie.
- A-records wijzen naar het VPS-IP; Cloudflare-proxy verbergt de origin
- CNAME voor
wwwop DNS-niveau geflattend naar de root - MX-records voor e-mail apart gerouteerd, niet geproxyd
Cachingstrategie
Statische assets cachen aan Cloudflares edge via een Page Rule; purge is gescript en wordt bij elke deploy getriggerd.
- Cache Everything voor
/assets/* - Edge-TTL: 1 maand voor geversioneerde statische bestanden
WAF en beveiligingsregels
De gratis WAF-laag blokkeert gangbare aanvalspatronen; custom regels beperken admin-paden en blokkeren bekende bot-user-agents.
- OWASP managed ruleset actief aan de Cloudflare-edge
- Custom regel: blokkeer
/admin/*voor niet-BE/NL-geo's - Rate limiting op het contactformulier (10 req/min)
Backblaze B2 via Cloudflare CDN
Media staat op B2 en wordt geserveerd via een Cloudflare-gecachet custom domein — een CDN-origin zonder egresskosten.
- Publieke B2-bucket;
cdn.collectorwwii.euCNAME naar B2-endpoint - Laravels
url()op MediaFile geeft de CDN-URL terug, niet B2 rechtstreeks
kenneth@vps~crontab -l
06 / monitoring en alerting
Crontab-setup
De uptimecheck draait elke 5 minuten; de Laravel-scheduler draait elke minuut als aparte cron-regel.
# crontab -e # Uptime monitor every 5 minutes */5 * * * * /usr/local/bin/check-uptime.sh # Laravel scheduler * * * * * cd /var/www/collectorwwii && php artisan schedule:run
Schijf- en geheugenchecks
Een dagelijks script controleert schijfgebruik en vrij RAM, en meldt via Discord als schijf boven 80% komt of vrij RAM onder 100 MB zakt.
kenneth@vps~/bincat check-uptime.sh
Uptimemonitor — Bash + Discord-webhook
Een cron-gedreven script controleert elk domein elke vijf minuten en vuurt een Discord-webhook af bij een niet-2xx-status of time-out.
#!/bin/bash # /usr/local/bin/check-uptime.sh WEBHOOK="https://discord.com/api/webhooks/..." SITES=("https://collectorwwii.eu" "https://kennethclauwaert.com") for site in "${SITES[@]}"; do STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ --max-time 10 "$site") if [[ "$STATUS" != "200" ]]; then curl -s -X POST "$WEBHOOK" \ -H 'Content-Type: application/json' \ -d "{\"content\": \"DOWN: $site — HTTP $STATUS\"}" fi done
kenneth@vps~/bincat backup-db.sh
07 / back-upstrategie
Automatische MySQL-dumps
Een nachtelijk script dumpt alle databases, comprimeert ze en uploadt het archief naar een privé-B2-bucket via de B2-CLI.
#!/bin/bash # /usr/local/bin/backup-db.sh DATE=$(date +%Y-%m-%d) BACKUP_DIR="/var/backups/mysql" BUCKET="my-backups-bucket" mysqldump -u root -p$DB_PASS collectorwwii \ | gzip > "$BACKUP_DIR/collectorwwii-$DATE.sql.gz" b2 file upload "$BUCKET" \ "$BACKUP_DIR/collectorwwii-$DATE.sql.gz" \ "db/collectorwwii-$DATE.sql.gz" # Prune local copies older than 7 days find "$BACKUP_DIR" -mtime +7 -delete
Wat geback-upt wordt, en retentie
Back-ups dekken de database en de applicatiebestanden; B2 lifecycle-regels verwijderen objecten ouder dan 30 dagen.
- MySQL-dumps — nachtelijk, gecomprimeerd, offsite naar B2
- Applicatiebestanden — wekelijkse rsync-snapshot naar B2
- Environment-bestanden — manueel versleuteld en offline bewaard
- B2-lifecycleregel: verwijder objecten > 30 dagen
- Maandelijkse hersteltest: laatste dump opnieuw importeren op staging
Hetzner-snapshots
Een wekelijkse Hetzner-VPS-snapshot voegt een volledige-schijf-herstel toe voor catastrofale fouten of migraties.
kenneth@vps~ls runbooks/
08 / hoe alles samenvalt
- zero-downtime-deploys Een shellscript pult uit Git, installeert Composer-dependencies, draait migraties in onderhoudsmodus, en zet daarna de onderhoudsmodus af en herlaadt PHP-FPM.
- redis-for-queues-and-cache Redis verzorgt Laravels queue-backend en caching op applicatieniveau, en draait als Docker-container gebonden aan localhost.
- environment-separation
.env-bestanden worden nooit gecommit; elk is eigendom vanwww-datametchmod 640, enkel leesbaar door de appgebruiker en root. - multi-domain-from-one-vps Aparte Nginx-serverblokken, webroots, PHP-FPM-pools en databasegebruikers per domein — ze delen de machine maar zijn logisch geïsoleerd.
- log-management
Nginx- en PHP-FPM-logs roteren via
logrotate; Laravel-logs schrijven naarstorage/logs/laravel.logen worden gecontroleerd in de deploy-healthcheck. - cloudflare-only-entry Het VPS-origin-IP wordt nooit blootgesteld — UFW laat HTTPS enkel toe vanaf Cloudflares gepubliceerde IP-ranges.
kenneth@vps~cat NOTES.md
09 / wat dit je leert
Alleen een productie-VPS beheren dwingt een niveau van operationele verantwoordelijkheid af dat moeilijk te simuleren is. Als er om 2 uur 's nachts iets stukgaat, ben jij de on-call engineer.
De monitoringscripts groeiden mee met elke echte incident — een logbug die de schijf 's nachts liet vollopen, een hersteltest van een back-up die een ontbrekende tabel aan het licht bracht.
Staksamenvatting
Ubuntu 22.04 · Hetzner VPS · Nginx · PHP-FPM 8.2 · MySQL 8 · Redis · Certbot · Cloudflare DNS + WAF + CDN · Backblaze B2 · Fail2ban · UFW · Bash · cron · systemd · Discord webhooks
✓ exit 0