kenneth@vps: /etc

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.

host
Hetzner VPS (CX22)
os
Ubuntu 22.04 LTS
domeinen
4 productiedomeinen, allemaal achter Cloudflare
workloads
Laravel-applicaties, MySQL, Redis, mediaservices — live verkeer

kenneth@vps~/docscat topology.txt

01 / overzicht van de stack

Requestflow

BrowserCloudflare DNS + WAFHetzner VPS (Ubuntu 22.04)UFWNginx (SSL termination)PHP-FPM 8.2 / app

Media en opslag

Laravel Storage facadeBackblaze B2Cloudflare CDN cache

Ondersteunende services

MySQL 8 · Redis · Laravel queue worker · Laravel scheduler (cron)

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-upgrades geconfigureerd 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 www op 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.eu CNAME 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 van www-data met chmod 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 naar storage/logs/laravel.log en 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