Configuration d’un VPS & hébergement de pages statiques
Configuration d’un VPS & hébergement de pages statiques #
Introduction #
Jusqu’à présent, j’ai opté pour deux options afin d’héberger mes projets personnels:
- un hébergement OVH pour du PHP et des pages statiques
- des offres gratuites type fly.io, render.com ou vercel pour les autres technos comme NodeJS ou Java
Toutefois, les offres gratuites ont des conditions qui fluctuent beaucoup (voire disparaissent totalement comme ce fut le cas pour Heroku) C’est pourquoi, j’ai eu envie d’explorer l’option du VPS (Virtual Private Server) pour avoir davantage de liberté et ne plus craindre les changements inopinés.
En terme d’offre, j’aurais pu prendre un VPS chez mon hébergeur actuel OVH mais j’ai eu envie d’essayer PulseHeberg qui est français, semble avoir bonne presse et propose des prix raisonnables.
Dans cet article, nous allons initialiser le VPS, le sécuriser, héberger un site statique et saupoudrer le tout d’une couche d’observabilité.
NB: je n'ai pas automatisé tout le processus. C'est pourquoi, dans les extraits de commande, vous verrez parfois des commentaires commençant par "# MANUELLEMENT: ". Ceci veut dire que vous devez faire manuellement l'action indiquée.
Initialisation et sécurisation du VPS #
Pour commencer, il faut choisir l’offre VPS qui correspond à votre besoin. Pour ma part, j’ai commencé par l’offre de base de PulseHeberg avec les paramètres suivants:
- Localisation: France
- OS: Ubuntu 24.04
- Authentification: Clé SSH (pour ce faire, il vous faudra générer une clé SSH sur votre machine puis uploader la clé publique)
Je ne m’attarderai pas sur les aspects SSH. Je pense que si vous vous intéressez à la configuration d’un VPS, vous avez déjà les bases sur ce sujet.
Mise à jour du système #
La première chose à faire lors de la première connexion est de mettre à jour le système:
apt update
apt upgrade
reboot
Création d’un utilisateur non root #
L’utilisation de l’utilisateur root n’est pas recommandée. Nous allons donc créer un nouvel utilisateur qui sera utilisé pour l’ensemble des travaux qui vont suivre:
adduser <votre_nom_utilisateur>
usermod -aG sudo <votre_nom_utilisateur>
Nous allons ensuite nous connecter avec le nouvel utilisateur afin d’y ajouter notre clé ssh:
su - <votre_nom_utilisateur>
mkdir .ssh
nano .ssh/authorized_keys
# MANUELLEMENT: Coller votre clé SSH publique
Sécurisation du service SSH #
Maintenant que nous avons configuré un nouvel utilisateur, nous allons changer la configuration du serveur SSH pour désactiver le login par password ainsi que la possibilité de se connecter avec l’utilisateur root:
sudo nano /etc/ssh/sshd_config
# MANUELLEMENT: Changer les lignes
# PasswordAuthentication de yes à no
# PermitRootLogin de prohibit-password à no
sudo service ssh restart
Une autre modification possible mais non obligatoire est de changer le port SSH par défaut. Pour ce faire, choisissez un port entre 1024 et 65536 qui ne sera pas utilisé sur votre VPS puis éditez les fichiers suivants:
sudo nano /etc/systemd/system/sockets.target.wants/ssh.socket
# MANUELLEMENT: Changer la ligne ListenStream=22 en remplaçant 22 par le nouveau port
sudo nano /etc/ssh/sshd_config
# MANUELLEMENT: Changer la ligne Port 22 en remplaçant 22 par le nouveau port
sudo systemctl daemon-reload
sudo service ssh restart
Dorénavant, il faudra ajouter l’argument -p pour spécifier le port lorsque vous vous connectez:
ssh <votre_nom_utilisateur>@<ip_vps> -p <nouveau_port_ssh>
Enfin, si comme moi vous vous connectez toujours depuis la même adresse IP, il est intéressant d’ajouter une nouvelle règle au firewall de façon à n’autoriser que les connexions SSH venant de votre adresse. J’ai opté pour l’utilisation de la configuration du pare-feu via la page dédiée de PulseHeberg. Profitez-en pour autoriser les connexions au port 80 et 443 pour être prêt pour la suite de l’article:
Si vous vous connectez depuis différentes adresses IP, il peut être intéressant de configurer fail2ban. Il s’agit d’un outil qui surveille les journaux d'authentification de l’OS et détecte les tentatives d'accès non autorisées, tels que les attaques par force brute ou les essais d'intrusion. Il bloque ensuite temporairement l'accès des IP suspectes pour empêcher ces attaques et préserver la sécurité de l’OS.
sudo apt install fail2ban
# MANUELLEMENT: Changer <SSH_PORT> par le port SSH que vous avez choisi
sudo sh -c 'printf "[ssh] \nenabled = true \nport = <SSH_PORT> \nfilter = sshd \naction = iptables[name=SSH, port=<SSH_PORT>, protocol=tcp] \nlogpath = /var/log/auth.log \nmaxretry = 3 \nbantime = 900\n" > /etc/fail2ban/jail.d/ssh.conf'
sudo service fail2ban restart
sudo server fail2ban status
Activation des mises à jour automatiques #
Afin de garantir davantage de sécurité sans devoir me connecter quotidiennement, j’ai décidé d’activer les mises à jour automatiques d’Ubuntu via unattended-upgrades. Il est possible de définir ce que l’on souhaite mettre à jour de façon automatique via le fichier de configuration /etc/apt/apt.conf.d/50unattended-upgrades:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
# MANUELLEMENT: Décommenter "${distro_id}:${distro_codename}-updates";
sudo systemctl status unattended-upgrades
Installation de Docker #
Maintenant que notre serveur VPS est initialisé et sécurisé, nous allons pouvoir commencer à installer les applications qui nous permettront d’héberger notre site statique et nos outils d’observabilité. J’ai opté pour l’utilisation de docker et docker compose afin de faciliter le déploiement et la mise à jour des solutions.
Pour installer Docker, il faut exécuter les commandes suivantes:
# Ajouter la clé GPG officielle de Docker
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Ajouter le repository Apt et lancer l'install
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Donner les droits docker à notre utilisateur courant
sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
# Démarrer docker avec systemd
sudo systemctl enable docker.service
sudo systemctl enable containerd.service
Hébergement d’un site statique avec Caddy #
Pourquoi Caddy? #
Bien souvent, les serveurs web comme Nginx et Apache ont été les solutions privilégiées pour l’hébergement de sites web statiques.
Toutefois, Caddy est un nouveau challenger qui propose des fonctionnalités très séduisantes. Ainsi, il est très simple à configurer et permet la gestion automatique des certificats SSL (avec Let’s Encrypt). Grâce à Caddy, le risque d’erreur de configuration d’un certificat ou d’oubli de renouvellement est éliminé.
Stockage des pages statiques #
Pour le stockage des pages statiques, je propose de nous conformer à la convention qui oriente le stockage vers le répertoire /var/www. Ce n’est pas obligatoire mais j’ai décidé de créer un groupe www pour gérer les droits sur ce répertoire.
# Création du répertoire et affectation des droits
sudo mkdir /var/www
sudo addgroup www
sudo adduser $USER www
sudo chown -R root:www /var/www
sudo chmod 775 /var/www
# S'assurer que les fichiers créés dans /var/www
# héritent bien des droits du groupe www
sudo chmod g+s /var/www
Vous pouvez désormais copier les fichiers statiques à héberger dans un sous répertoire de /var/www. De mon côté, j’ai opté pour la convention /var/www/<nom_de_domaine> (où <nom_de_domaine> est david.le-montagner.fr par exemple)
Configuration de Caddy #
Caddy se configure via un fichier Caddyfile dont le contenu sera:
(log_common) {
log {
output file /var/log/caddy/{args[0]}.access.log
}
}
{
admin caddy:2019
servers {
metrics
}
}
<nom_de_domaine> {
import log_common <nom_de_domaine>
root * /var/www/<nom_de_domaine>
file_server
encode gzip zstd
handle_errors {
rewrite * /404.html
file_server
}
}
Grâce à cette configuration, vous retrouvez les logs du service dans le répertoire /var/log/caddy/<votre_nom_de_domaine>.access.log. Vous bénéficiez également de la compression gzip et zstd ainsi que d’une page 404 personnalisée (404.html).
La partie admin, servers, metrics nous sera utile pour la section observabilité de la suite de l’article.
Installation de Caddy #
Pour installer Caddy, nous allons initialiser un docker compose qui sera enrichi de nouveaux services au fil de cet article:
services:
caddy:
image: caddy:latest
restart: always
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- /var/log/caddy:/var/log/caddy
- /var/www:/var/www
- caddy_data:/data
- caddy_config:/config
ports:
- "80:80"
- "443:443"
networks:
- caddy_network
networks:
caddy_network:
volumes:
caddy_data:
caddy_config:
Comme vous pouvez le constater, nous définissons certains mappings de volume tels que:
- le fichier Caddyfile mentionné ci-dessus
- un répertoire /var/log/caddy local pour pouvoir les consulter sans passer par l’exécution de commande dans le container. Ceci nous sera notamment utile pour la partie observabilité.
- le répertoire /var/www mentionné ci-dessus
Configuration du DNS #
Il ne reste plus qu’à configurer votre DNS pour faire pointer le nom de domaine vers votre VPS. Pour ceci, il suffit de créer un enregistrement A vers l’adresse IP du VPS.
A noter que si vous passez par Cloudflare, et que vous activez la fonction proxy pour bénéficier des fonctionnalités anti DDOS, il faudra faire de la configuration supplémentaire. Je n’aborderai pas ce sujet dans cet article pour ne pas trop complexifier son contenu mais vous aurez des pistes de résolution en consultant ces deux posts:
Une fois la configuration du DNS réalisée, vous pouvez lancer le docker compose de façon à ce que Caddy se lance et déclenche la génération automatique des certificats:
docker compose up -d
Observabilité de l’OS et de Caddy #
Bien que je n’aie pas encore écrit d’articles sur le sujet, je considère l’observabilité comme étant un élément primordial de toute plateforme d’hébergement. Mon usage actuel du VPS reste très modeste mais j’avais tout de même envie de poser de bonnes bases pour l’observabilité afin de pouvoir y brancher mes futurs services.
La stack #
Rien de révolutionnaire dans le choix de la stack puisque nous allons nous appuyer sur:
- Grafana pour la visualisation des données
- Prometheus pour la récupération et le stockage des métriques
- Loki pour stocker les logs
- Promtail pour recueillir les logs
Installation et recueil des données #
Nous allons donc enrichir notre docker compose pour y intégrer ces solutions:
loki:
image: grafana/loki:latest
restart: always
command: -config.file=/etc/loki/local-config.yaml
networks:
- monitoring
promtail:
image: grafana/promtail:latest
restart: always
volumes:
- /var/log/caddy:/var/log/caddy
- ./etc/promtail/config.yml:/etc/promtail/config.yml
networks:
- monitoring
prometheus:
image: prom/prometheus:latest
restart: always
volumes:
- ./etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- monitoring
- caddy_network
grafana:
environment:
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
image: grafana/grafana:latest
restart: always
volumes:
- grafana_data:/var/lib/grafana
- ./etc/grafana/provisioning:/etc/grafana/provisioning
networks:
- monitoring
- caddy_network
# Networks & volumes à ajouter à ceux existants
networks:
monitoring:
volumes:
grafana_data:
Comme vous pouvez le constater, nous mappons des fichiers de configuration pour chacun des services. Ces configurations seront détaillées dans la suite de l’article.
A noter que Grafana et Prometheus sont à la fois sur le réseau monitoring et caddy_network. Le premier pour être accessible depuis l’extérieur via le reverse proxy de Caddy (cf le chapitre “Accès Grafana” ci-dessous). Le second pour être capable de recueillir les données d’observabilité de Caddy (via la partie metrics configurée plus tôt dans l’article)
Pour ce qui est des métriques système, nous allons utiliser node-exporter qui est conçu spécialement pour cette tâche:
node-exporter:
image: prom/node-exporter:latest
restart: always
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- "--path.procfs=/host/proc"
- "--path.rootfs=/rootfs"
- "--path.sysfs=/host/sys"
- "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
networks:
- monitoring
Configuration de Prometheus #
Le rôle de Prometheus est de récupérer les métriques. Nous avons trois sources de métriques à configurer dans le fichier ./etc/prometheus/prometheus.yml:
- Prometheus qui produit ses propres métriques
- Caddy
- Node Exporter
global:
scrape_interval: 1m
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: caddy
static_configs:
- targets: ["caddy:2019"]
- job_name: "node"
static_configs:
- targets: ["node-exporter:9100"]
Configuration de Loki & Promtail #
Nous allons configurer Promtail via le fichier ./etc/promtail/config.yml de façon à ce qu’il pousse les logs de Caddy vers le serveur Loki:
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*log
- job_name: caddy
static_configs:
- targets:
- localhost
labels:
job: caddy
__path__: /var/log/caddy/*log
agent: caddy-promtail
pipeline_stages:
- json:
expressions:
duration: duration
status: status
- labels:
duration:
status:
Bien entendu, la récupération des logs de Caddy est rendue possible grâce au mapping commun de /var/log/caddy sur les services Promtail et Caddy.
Configuration de Grafana et tableaux de bord #
La configuration de Grafana va se faire via le fichier ./etc/grafana/datasources/ds.yaml. Nous allons configurer deux sources de données: Loki & Prometheus.
apiVersion: 1
datasources:
- name: Loki
type: loki
uid: loki
access: proxy
url: http://loki:3100
basicAuth: false
isDefault: false
version: 1
editable: false
- name: Prometheus
type: prometheus
uid: prometheus
access: proxy
url: http://prometheus:9090
basicAuth: false
isDefault: false
version: 1
editable: false
Maintenant que les données sont disponibles, il faut les consulter. Vous pouvez le faire en écrivant des requêtes dans Grafana. Toutefois, j’ai intégré les deux tableaux de bord suivants de façon à avoir une première base rapidement:
L’importation se fait automatiquement grâce aux fichiers de provisioning que j’ai stockés dans le répertoire ./etc/grafana/provisioning/dashboards: respectivement dans les fichiers caddyMonitoring.json et nodeExporter.json.
Accès à Grafana #
Pour l’accès à Grafana, deux options s’offrent à vous:
- Ouvrir un port spécifique (avec filtrage sur votre adresse IP si elle est fixe) et ne pas oublier de le mapper dans le docker-compose
- Créer un sous-domaine puis utiliser les fonctionnalités de reverse proxy et filtrage IP de Caddy. C’est la solution que j’ai choisie en ajoutant ceci au fichier Caddyfile:
<sous_domaine> {
import log_common <sous_domaine>
@blocked not remote_ip <votre_adresse_ip>
respond @blocked "Unauthorized" 403
reverse_proxy grafana:3000
}
Lors de votre premier accès à Grafana, il faudra utiliser le login/mot de passe (admin/admin) par défaut. L’outil vous proposera ensuite de modifier le mot de passe par défaut.
Suivi de version des images Docker avec Watchtower #
Pour gérer plus facilement la mise à jour des images Docker, nous allons nous appuyer sur Watchtower. Cet outil permet de vérifier à intervalle régulier l’existence d’une version plus récente d’une image Docker.
Encore une fois, nous allons ajouter ce service à notre docker compose:
watchtower:
image: containrrr/watchtower
restart: always
env_file: .env.watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Il est possible de laisser l’outil réaliser la mise à jour de façon automatique. Toutefois, ceci n’est pas souhaitable pour un service de production car les mises à jour peuvent parfois intégrer des breaking changes qui entraineraient alors une indisponibilité.
Nous allons donc configurer l’outil de façon à ce qu’il nous envoie une notification mail lorsqu’il détecte la disponibilité d’une nouvelle version. Voici le contenu du fichier .env.watchtower pour ce faire:
WATCHTOWER_POLL_INTERVAL=86401
WATCHTOWER_MONITOR_ONLY=true
WATCHTOWER_NOTIFICATIONS=email
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=<port_serveur_mail>
WATCHTOWER_NOTIFICATION_EMAIL_FROM=<email_expediteur>
WATCHTOWER_NOTIFICATION_EMAIL_TO=<email_destinataire>
WATCHTOWER_NOTIFICATION_EMAIL_SERVER=<host_serveur_mail>
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=<utilisateur_serveur_mail>
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=<mot_de_passe_serveur_mail>
WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2
Code source #
La configuration dans son ensemble est disponible dans ce repository. Pour que vous puissiez le tester sans avoir besoin d'un nom de domaine, j'ai fait quelques ajustements:
- Le site statique est accessible en localhost sur le port 80
- Grafana est accesible en localhost sur le port 81
- Le site statique est stocké dans ./www
- Les logs sont stockés dans ./logs
Vous pourrez ensuite facilement adapter cette base à votre besoin en y intégrant vos noms de domaine ainsi qu'un autre mapping de répertoire (par exemple, celui décrit dans cet article)
Conclusion #
La configuration que je propose reste simple car mon usage actuel est limité. Il est évidemment possible d’aller plus loin avec une gestion des droits plus stricte, du k8s, Ansible ou Terraform mais je n’ai pas jugé ceci nécessaire pour le moment.
Comme vous avez pu le constater, se lancer dans l’aventure VPS représente un investissement de temps non négligeable. Dès lors, on peut se poser la question de son intérêt face aux solutions clé en main disponibles sur le marché. Toutefois, un VPS offre beaucoup de flexibilité et j’estime donc qu’il s’agit d’une bonne solution pour les projets perso où l’on creuse bien souvent de nouvelles technologies.