Pieni lähiravintola – tekninen projektikuvaus

Tämä projekti on anonymisoitu asiakastoteutus, jossa rakensin pienen ravintolan verkkosivut alusta alkaen ilman valmista julkaisujärjestelmää. Toteutus pyörii perinteisessä webhotelliympäristössä ja koostuu kevyestä PHP/HTML/CSS-koodista, erillisestä admin-näkymästä sekä varaus- ja lomakelogiikasta.

Ympäristö ja rajoitteet

Projekti toimii jaetussa webhotelliympäristössä ilman root-oikeuksia. Tämä rajaa pois omat demonit ja kontit, joten ratkaisut on rakennettu “PHP + cron + rclone” -ajattelulla ja käyttäjän omilla shell-skripteillä.

  • PHP 8.x (FPM/FastCGI), opcache käytössä suorituskyvyn tasaamiseksi
  • käyttäjäkohtainen eristys (chroot-/cagefs-tyyppinen), ei pääsyä muiden tileihin
  • ei systemd-timereita, ajastus crontab-tasolla käyttäjän omilla skripteillä
  • HTTP/2-palvelu TLS:n päällä, reititys suoraan virtuaalihostin DocumentRootiin
  • peruskovennus .htaccess-tiedostoilla: Options -Indexes, admin- ja data-hakemistot rajattu ja merkitty X-Robots-Tag: noindex -otsikoilla
  • data tallennetaan SQLite-tiedostoihin, ei erillistä MySQL-instanssia tälle projektille

Arkkitehtuuri lyhyesti

Projektin perusrakenne (anonymisoitu mutta rakenteellisesti aito):

/home/<asiakas>/
  public_html/                 # Sivuston juuri (index.php, teemat, lomakkeet)
    includes/
      session_bootstrap.php    # istunnot, peruskovennus, charset
      astiastot_db.php         # SQLite-kyselyt astiastovarauksiin
      security-helpers.php     # syötekäsittely, CSRF, header-sanitointi
    admin/
      astiavaraukset.php       # admin-listaus ja hyväksyntälogiikka
      login.php                # yksinkertainen sessiopohjainen kirjautuminen
    tools/
      ...                      # dokumentaatiot ja työkalu-skriptit
    assets/
      css/, img/, js/

  data/
    astiavaraukset.sqlite      # varausdata (WAL + foreign keys päällä)

  www-backups/                 # päivittäiset www- ja SQLite-backupit
    publattahattu-www-YYYY-MM-DD_HH-MM-SS.tar.gz
    sqlite-astiavaraukset/
    dr/
      <home-snapshotit>

  backup-www.sh                # www-backup (tar + gzip)
  prune-www-backups.sh         # rotaatio (7 päivää)
  sync-www-backups.sh          # rclone-offsite (checksum + retry)
  run-*-cron.sh                # cron-ajojen "wrapperit" + mail-notifikaatiot

Tekniset päävalinnat

  • Custom PHP 8 + HTML5 + CSS (ei CMS:ää, ei page builder -roskaa, ei ylimääräisiä plugineita)
  • Responsiivinen layout ilman raskaita JS-frameworkeja – vain kevyt vanilla-JS niihin kohtiin missä sitä oikeasti tarvitaan
  • Catering-tilauslomake, joka käyttää serveripuolen validointia ja lähettää tilaukset SMTP:n kautta ravintolan sähköpostiin (tekstipohjainen, helposti luettava formaatti)
  • Astiasto- ja varauslogiikka SQLite-tietokannan päällä, taulurakenne normalisoitu (asiakas, varaus, astiastosetti) ja viiteavaimet enforceeraavat eheyden
  • CRON-pohjainen backup- ja offsite-synkronointiketju (tar + rclone, checksum-vertailu)
  • Erillinen DR-snapshot koko käyttäjäkansiosta (home) 8 viikon rotaatiolla, mikä parantaa RPO/RTO-tavoitteiden toteutumista ilman raskaita DR-työkaluja

Varmuuskopiointi ja DR-ketju

Päivittäinen backup vie talteen www-hakemiston ja varausten SQLite-datan (RPO ≈ 24 h). Tämän päälle on rakennettu viikoittainen DR-snapshot, joka tallettaa koko käyttäjäkansion lukuun ottamatta väliaika- ja lokihakemistoja. Snapshot arkistoidaan www-backups/dr/-kansioon ja synkronoidaan rclonella offsiteen, jolloin sekä levyvika että “ihmisen moka” voidaan peruuttaa.

Offsite-kopioissa hyödynnetään rclonen tarkistussummia (--checksum / palvelukohtaiset hashit), ja ajot logitetaan erilliseen rclone-sync.log-tiedostoon. DR-prosessi on testattu oikealla palautusharjoituksella erilliseen test-restore/-hakemistoon.

#!/bin/bash
# run-dr-snapshot.sh – viikoittainen home-snapshot

set -euo pipefail

HOME_DIR="/home/<asiakas>"
DR_DIR="$HOME_DIR/www-backups/dr"
DATE="$(date +%F)"
TARGET="$DR_DIR/<asiakas>-home-${DATE}.tar.gz"

mkdir -p "$DR_DIR"

echo "=================================================="
echo "DR-snapshot ajo: $(date '+%Y-%m-%d %H:%M:%S')"
echo "Tallennuspolku:  $TARGET"
echo "=================================================="

cd /home

tar -czf "$TARGET" \
  --warning=no-file-changed \
  --exclude="<asiakas>/tmp/*" \
  --exclude="<asiakas>/logs/*" \
  --exclude="<asiakas>/www-logs/*" \
  --exclude="<asiakas>/www-backups/dr/*" \
  "<asiakas>"

echo "DR-snapshot valmis: $TARGET"

Tietokanta ja varauslogiikka (PHP + SQLite)

Varausdatan käsittelyssä käytän erillistä tietokerrosta (includes/astiastot_db.php), jossa kaikki kyselyt ovat valmisteltuja (prepared statements) ja datatyypit rajataan tiukasti. Tietokanta on suunniteltu niin, että se kestää pienen ravintolan arjen: useita varauksia päivässä, peruutuksia ja tilojen päällekkäisyystarkistuksia.

<?php
// includes/astiastot_db.php (lyhennetty esimerkki)

declare(strict_types=1);

function get_astiadb(): PDO {
    static $pdo = null;
    if ($pdo instanceof PDO) {
        return $pdo;
    }

    $dbPath = __DIR__ . '/../data/astiavaraukset.sqlite';

    $pdo = new PDO('sqlite:' . $dbPath, null, null, [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
    ]);

    // Peruskovennus: write-ahead logging ja viiteavaimet päälle.
    $pdo->exec('PRAGMA journal_mode=WAL;');
    $pdo->exec('PRAGMA foreign_keys=ON;');
    $pdo->exec('PRAGMA synchronous=NORMAL;');

    return $pdo;
}

function get_future_reservations(DateTimeImmutable $from): array {
    $pdo = get_astiadb();

    $sql = <<<SQL
        SELECT id, set_id, customer_name, start_date, end_date, status
        FROM reservations
        WHERE date(end_date) >= :from
        ORDER BY start_date ASC
    SQL;

    $stmt = $pdo->prepare($sql);
    $stmt->execute([
        ':from' => $from->format('Y-m-d'),
    ]);

    return $stmt->fetchAll();
}

Tietoturva ja laatu

  • lomakkeissa CSRF-tokensuojaus + honeypot-kentät bottien varalta; serveripuolen validointi on ensisijainen, client-side vain lisätuki
  • header-sanitointi (esim. sähköpostien otsikot stripataan CR/LF-merkeistä CRLF-injektion estämiseksi)
  • session_bootstrap.php huolehtii istunnon asetuksista: session.cookie_httponly, session.cookie_secure, session.use_strict_mode jne.
  • admin-näkymät rajattu salasanalla + palvelinpään IP/Directory-suojauksella (kerrosmalli, ei yksi lukko)
  • perussuojaus HTTP-otsikoilla: mahdollisuus lisätä X-Frame-Options, X-Content-Type-Options, Referrer-Policy ja Content-Security-Policy ilman, että fronttikoodi tarvitsee refaktoroida
  • backup-, prune-, sync- ja DR-ajot logitetaan erillisiin tekstilokeihin ja kootaan tarvittaessa zip-liitteeksi cron-sähköposteihin
  • palautus on testattu oikealla “fire drill” -harjoituksella: arkisto purettu erilliseen test-restore/-hakemistoon ja sivusto käynnistetty sieltä

Mitä opin / missä tämä on hyödyllinen

Case toimii hyvänä esimerkkinä siitä, miten pienen yrityksen sivusto voidaan rakentaa niin, että ulkoasun lisäksi myös infra ja palautettavuus on mietitty:

  • sisältö pysyy selkeänä myös ei-tekniselle henkilöstölle – hallintanäkymä on käyttöliittymältään yhtä monimutkainen kuin sähköposti
  • tapahtumat ja tilaukset kulkevat yksinkertaisten lomakkeiden kautta sähköpostiin, ei monimutkaista workflow-moottoria, jota kukaan ei ylläpidä
  • päivittäinen www-backup, SQLite-backup ja viikoittainen DR-snapshot muodostavat käytännössä kolmikerroksisen turvaverkon (online, nearline, offsite)
  • offsite-varmistus ei ole “ehkä joskus”, vaan osa automaattista prosessia rclonen ja cron-ajojen kautta
  • räätälöity ratkaisu on kevyempi ja läpinäkyvämpi kuin CMS + lisäosaviidakko, etenkin kun datamäärät ja liikenne ovat pienen ravintolan tasolla