A complete system I run, with proof you can check live
The site you are reading runs on a server I set up, secure, update and monitor myself — from the bare machine to this page.
The need
A portfolio should prove skills, not just list them. Hosting it on a managed platform would hide exactly what I want to show: running a reliable, secure service over time.
The idea
Make the hosting itself the demonstration: everything written here can be checked live, on the very machine serving this page.
What I built
- A cloud server in the Paris region, obtained after six attempts over several days — free capacity there is scarce.
- The whole web stack, with each service running in its own restricted space so one problem can't spread to the others.
- Hardening started on the first night — 7 measures applied in one night without any downtime — then continued step by step.
- Automatic updates: an approved change goes live on its own, is health-checked, and rolls back automatically if the new version doesn't answer.
- Continuous backups of the database, plus nightly snapshots kept daily, weekly and monthly.
- A separate probe that checks the site's security every hour and publishes the result — you can see it below.
The result
- 5security checks re-run every hour by a separate probe
- 7applied in one night, zero downtime
- 6attempts to get the server
- 1 hbetween two independent security checks
It has served this portfolio since August 2026, alongside EventMap. The live checks below are measured by a separate probe, not written by hand.
Check it yourself
Live checks
- TLS verified
- origin verified
- headers verified
- DNSSEC verified
- uptime verified
In pictures
What I learned
Never test only from the machine itself: with two firewalls stacked, a service can answer perfectly locally while nobody on the Internet can reach it. And choosing not to install something — here, a database server — is an operations skill too.
Technical details Stack, architecture and decisions — for technical readers
Stack
- Ubuntu 22.04
- Oracle Cloud ARM (Always Free)
- nginx
- Node.js + Express
- SQLite (node:sqlite)
- systemd
- fail2ban
- iptables
- Cloudflare
- GitHub Actions
- Litestream + restic
Architecture at a glance
- Visitor's browser
- Cloudflare — TLS and cache
- nginx — security headers, rate limiting
- Node.js + Express — pages rendered on the server, in 7 languages; a sandboxed systemd service
- SQLite (node:sqlite) — projects and first-party visit records
- Around it: GitHub Actions tests every change; the VM pulls the validated release, checks /health and rolls back on failure; the Observatory re-checks security every 30 minutes.
How it works
- VM with 4 OCPU / 24 GB (Ubuntu 22.04). nginx reverse proxy in front of isolated services, each under a dedicated system user without a shell, with ProtectSystem=strict and NoNewPrivileges.
- First night (23 Aug 2026): admin password regenerated, fail2ban on SSH, strict security headers (CSP, HSTS, X-Frame-Options), last IP octet zeroed in access logs, bounded journald retention, nginx log rotation, encrypted backup of secrets stored off the VM.
- Then: nginx rate limiting, rpcbind disabled, swap, remote desktop bound to 127.0.0.1 (SSH tunnel only), SSH password authentication disabled, Cloudflare Origin CA certificate in Full (strict) mode.
- Pull-based continuous deployment: a push to master is validated by GitHub Actions, which moves a prod tag; the VM pulls it through a systemd timer, health-checks it and rolls back automatically. No inbound port, no VM credentials stored at GitHub.
- Two stacked firewalls (iptables on the VM + the cloud Security List) — the reason I never test only from the machine itself.
- Deliberate trade-off: security updates install automatically, but the VM never reboots or changes major version on its own.
- First-party visit tracking only: no third-party cookie, IP addresses hashed before storage, records purged after 12 months.
Decisions & trade-offs
- Pages rendered on the server rather than loaded in the browser: they stay indexable in seven languages and readable without JavaScript.
- systemd services rather than Docker: production runs as sandboxed systemd units with CPU and memory quotas — a container that is never deployed would prove nothing.
- One public JSON feed (/api/contenus.json), reused by the Mindmap, rather than a second API: one source of truth, nothing to keep in sync.
Incidents & fixes
- After the redesign, the automatic pull-request report outgrew GitHub's 65,536-character comment limit (embedded screenshots): tests passed but publishing failed. Fixed with lighter thumbnails and a 60,000-character budget.
- Cloudflare kept serving old scripts for up to 4 hours after a deployment: every CSS and JavaScript URL now carries a content fingerprint (?v=), so a new release is never mixed with old files.
- In Arabic, a value such as “43 → 9.9 MB” was displayed backwards by the bidirectional text algorithm: Latin values are now isolated with Unicode marks.
Still missing
- No automatic reboot — a deliberate choice