Procédure de déploiement Nextcloud — Docker Compose
Procédure de déploiement Nextcloud — Docker Compose
Plateforme : Debian 13 (Trixie) Architecture : backend conteneurisé derrière un reverse proxy Nginx Version du document : 1.0 Public : administrateurs système Durée estimée : 1 h 30
Sommaire
- Conventions et variables
- Prérequis
- Préparation du disque de données
- Préparation du système
- Installation de Docker
- Déploiement de la stack
- Configuration post-installation
- Configuration du reverse proxy
- Durcissement réseau
- Exploitation courante
- Résolution des incidents
- Récapitulatif des chemins
1. Conventions et variables
Toutes les valeurs ci-dessous sont des exemples génériques. Les remplacer par celles de l'environnement cible avant exécution.
| Variable | Valeur d'exemple | Description |
|---|---|---|
NOM_SERVEUR | SRVAPPCLOUD01 | Nom d'hôte du serveur applicatif |
IP_SERVEUR | 192.0.2.10 | Adresse IP de service |
NOM_DNS_PUBLIC | cloud.exemple.tld | FQDN public de l'instance |
NOM_REVERSE_PROXY | SRVPROXY01 | Nom d'hôte du frontal |
IP_REVERSE_PROXY | 192.0.2.20 | Adresse IP du frontal |
DISQUE_DATA | /dev/sdb | Disque de données dédié |
POINT_DE_MONTAGE | /srv/nextcloud | Racine du stockage |
REPERTOIRE_STACK | /opt/nextcloud | Emplacement des fichiers Compose |
PORT_BACKEND | 8080 | Port d'écoute du backend |
FUSEAU_HORAIRE | Europe/Paris | Fuseau du serveur |
Les plages
192.0.2.0/24sont réservées par la RFC 5737 pour la documentation.
Aucun mot de passe ne figure dans ce document. Ils sont générés à l'étape 6.1 et doivent être conservés dans un gestionnaire de secrets.
2. Prérequis
- Machine virtuelle ou physique sous Debian 13 (Trixie)
- Disque système de 40 Go minimum
- Disque de données dédié, non partitionné
- Accès root ou sudo
- Accès réseau sortant vers les dépôts Debian et Docker
- Enregistrement DNS public pointant vers le reverse proxy
- Reverse proxy Nginx opérationnel avec Certbot fonctionnel
lsb_release -a
uname -r
3. Préparation du disque de données
3.1 Identification du disque
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
sudo fdisk -l
Repérer le disque de données, sans table de partition ni système de fichiers. Le nom peut être /dev/sdb, /dev/vdb (virtio) ou /dev/nvme0n1 selon l'hyperviseur.
⚠️ Vérifier impérativement le nom du périphérique avant de poursuivre. Les commandes suivantes sont destructives.
3.2 Partitionnement en GPT
GPT est obligatoire au-delà de 2 To ; MBR ne convient pas.
sudo apt update
sudo apt install -y parted
sudo parted DISQUE_DATA --script mklabel gpt
sudo parted DISQUE_DATA --script mkpart primary ext4 0% 100%
sudo parted DISQUE_DATA --script align-check optimal 1
sudo partprobe DISQUE_DATA
lsblk DISQUE_DATA
3.3 Formatage
sudo mkfs.ext4 -m 1 -L NC-DATA DISQUE_DATA1
| Option | Effet |
|---|---|
-m 1 | Réduit la réserve root de 5 % à 1 % |
-L | Applique un label facilitant l'identification |
3.4 Montage persistant
sudo blkid DISQUE_DATA1
sudo mkdir -p POINT_DE_MONTAGE
Ligne à ajouter dans /etc/fstab, en substituant l'UUID relevé :
UUID=<UUID> POINT_DE_MONTAGE ext4 defaults,noatime,nofail 0 2
L'option nofail évite un démarrage en mode urgence si le disque n'est pas présenté par l'hyperviseur.
sudo systemctl daemon-reload
sudo mount -a
df -hT POINT_DE_MONTAGE
3.5 Arborescence et permissions
sudo mkdir -p POINT_DE_MONTAGE/{html,data,db,redis}
sudo chown -R 33:33 POINT_DE_MONTAGE/html POINT_DE_MONTAGE/data
sudo chown -R 999:999 POINT_DE_MONTAGE/db POINT_DE_MONTAGE/redis
sudo chmod 750 POINT_DE_MONTAGE/html POINT_DE_MONTAGE/data
sudo chmod 750 POINT_DE_MONTAGE/db POINT_DE_MONTAGE/redis
ls -ln POINT_DE_MONTAGE
Les UID appliqués correspondent aux utilisateurs internes des conteneurs :
| UID | Utilisateur | Répertoires |
|---|---|---|
| 33 | www-data | html, data |
| 999 | mysql / redis | db, redis |
4. Préparation du système
4.1 Mise à jour et paquets de base
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg chrony vim htop
4.2 Identité et horloge
sudo hostnamectl set-hostname NOM_SERVEUR
sudo timedatectl set-timezone FUSEAU_HORAIRE
timedatectl status
Vérifier /etc/hosts :
127.0.0.1 localhost
IP_SERVEUR NOM_SERVEUR.domaine.local NOM_SERVEUR
5. Installation de Docker
5.1 Ajout du dépôt officiel
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
Contenu de /etc/apt/sources.list.d/docker.list :
deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable
Si le dépôt Docker ne publie pas encore le nom de code
trixie, le remplacer parbookworm. La compatibilité est assurée.
5.2 Installation des paquets
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world
docker compose version
5.3 Déplacement du stockage Docker (optionnel)
Recommandé lorsque le disque système est de faible capacité : les images et couches sont alors stockées sur le disque de données.
sudo systemctl stop docker
sudo mkdir -p POINT_DE_MONTAGE/docker
Contenu de /etc/docker/daemon.json :
{
"data-root": "POINT_DE_MONTAGE/docker",
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
sudo rsync -aP /var/lib/docker/ POINT_DE_MONTAGE/docker/
sudo systemctl start docker
docker info | grep "Docker Root Dir"
5.4 Accès sans sudo (optionnel)
sudo usermod -aG docker $USER
Nécessite une reconnexion de la session.
6. Déploiement de la stack
6.1 Génération des secrets
Générer quatre mots de passe distincts. Le jeu de caractères alphanumériques évite tout problème d'échappement dans les fichiers de configuration et les lignes de commande.
for i in 1 2 3 4; do
openssl rand -base64 64 | tr -dc 'A-Za-z0-9' | head -c 32; echo
done
| Rang | Affectation |
|---|---|
| 1 | Mot de passe root MariaDB |
| 2 | Mot de passe de l'utilisateur applicatif MariaDB |
| 3 | Mot de passe Redis |
| 4 | Mot de passe du compte administrateur Nextcloud |
Consigner ces valeurs dans le gestionnaire de secrets de l'organisation avant de poursuivre.
6.2 Fichier d'environnement
sudo mkdir -p REPERTOIRE_STACK
Créer REPERTOIRE_STACK/.env en substituant les valeurs générées :
MYSQL_ROOT_PASSWORD=<secret_1>
MYSQL_PASSWORD=<secret_2>
REDIS_PASSWORD=<secret_3>
NC_ADMIN_USER=ncadmin
NC_ADMIN_PASSWORD=<secret_4>
sudo chown root:root REPERTOIRE_STACK/.env
sudo chmod 600 REPERTOIRE_STACK/.env
ls -l REPERTOIRE_STACK/.env
6.3 Fichier Compose
Créer REPERTOIRE_STACK/compose.yaml :
services:
db:
image: mariadb:11.4
container_name: nc-db
restart: unless-stopped
command: >
--transaction-isolation=READ-COMMITTED
--binlog-format=ROW
--innodb-file-per-table=1
--skip-innodb-read-only-compressed
volumes:
- POINT_DE_MONTAGE/db:/var/lib/mysql
environment:
MARIADB_AUTO_UPGRADE: "1"
MARIADB_DISABLE_UPGRADE_BACKUP: "1"
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
networks: [nc-net]
redis:
image: redis:7-alpine
container_name: nc-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD} --save 60 1
volumes:
- POINT_DE_MONTAGE/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks: [nc-net]
app:
image: nextcloud:stable-apache
container_name: nc-app
restart: unless-stopped
depends_on:
db: { condition: service_healthy }
redis: { condition: service_healthy }
ports:
- "IP_SERVEUR:PORT_BACKEND:80"
volumes:
- POINT_DE_MONTAGE/html:/var/www/html
- POINT_DE_MONTAGE/data:/var/www/html/data
environment:
MYSQL_HOST: db
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
REDIS_HOST: redis
REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
NEXTCLOUD_ADMIN_USER: ${NC_ADMIN_USER}
NEXTCLOUD_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD}
NEXTCLOUD_DATA_DIR: /var/www/html/data
NEXTCLOUD_TRUSTED_DOMAINS: "NOM_DNS_PUBLIC IP_SERVEUR"
TRUSTED_PROXIES: "IP_REVERSE_PROXY"
OVERWRITEHOST: "NOM_DNS_PUBLIC"
OVERWRITEPROTOCOL: "https"
OVERWRITECLIURL: "https://NOM_DNS_PUBLIC"
APACHE_DISABLE_REWRITE_IP: "1"
PHP_MEMORY_LIMIT: 1G
PHP_UPLOAD_LIMIT: 16G
networks: [nc-net]
cron:
image: nextcloud:stable-apache
container_name: nc-cron
restart: unless-stopped
entrypoint: /cron.sh
depends_on:
db: { condition: service_healthy }
redis: { condition: service_healthy }
volumes:
- POINT_DE_MONTAGE/html:/var/www/html
- POINT_DE_MONTAGE/data:/var/www/html/data
networks: [nc-net]
networks:
nc-net:
driver: bridge
Points structurants :
- Le port est publié uniquement sur l'IP de service, et non sur toutes les interfaces. Le backend n'est donc pas joignable depuis d'autres réseaux.
TRUSTED_PROXIES,OVERWRITEPROTOCOLetAPACHE_DISABLE_REWRITE_IPsont indispensables derrière un reverse proxy. Sans eux, les URL générées restent en HTTP et les journaux enregistrent l'IP du proxy au lieu de celle des clients.- Le service
cronexécute les tâches de fond via un conteneur dédié, mode recommandé en production.
6.4 Démarrage
cd REPERTOIRE_STACK
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs -f app
L'installation initiale prend généralement une à trois minutes.
6.5 Contrôle local
curl -I http://IP_SERVEUR:PORT_BACKEND/status.php
curl -s http://IP_SERVEUR:PORT_BACKEND/status.php
Réponse attendue : code HTTP 200, avec "installed": true, "maintenance": false et "needsDbUpgrade": false.
7. Configuration post-installation
7.1 Raccourci occ
cd REPERTOIRE_STACK
NCOCC="sudo docker compose exec -u www-data app php occ"
7.2 Réglages de base
$NCOCC config:system:set default_phone_region --value="FR"
$NCOCC config:system:set maintenance_window_start --type=integer --value=1
$NCOCC background:job:mode cron
7.3 Optimisations de base de données
$NCOCC db:add-missing-indices
$NCOCC db:add-missing-columns
$NCOCC db:convert-filecache-bigint
$NCOCC status
7.4 Vérification des paramètres de proxy
$NCOCC config:system:get trusted_proxies
$NCOCC config:system:get overwriteprotocol
$NCOCC config:system:get overwrite.cli.url
$NCOCC config:system:get trusted_domains
Si une valeur est absente, la forcer :
$NCOCC config:system:set trusted_proxies 0 --value="IP_REVERSE_PROXY"
$NCOCC config:system:set overwriteprotocol --value="https"
$NCOCC config:system:set overwritehost --value="NOM_DNS_PUBLIC"
$NCOCC config:system:set overwrite.cli.url --value="https://NOM_DNS_PUBLIC"
$NCOCC config:system:set trusted_domains 1 --value="NOM_DNS_PUBLIC"
7.5 Vérification du cache Redis
$NCOCC config:system:get memcache.locking
Valeur attendue : \OC\Memcache\Redis
sudo docker compose exec redis sh -c \
'redis-cli -a "$REDIS_PASSWORD" INFO keyspace' 2>/dev/null
8. Configuration du reverse proxy
Opérations réalisées sur NOM_REVERSE_PROXY.
⚠️ Sur un reverse proxy en production, ne jamais utiliser le plugin
--nginxde Certbot en mode interactif. Il réécrit les fichiers de configuration existants. Utiliser exclusivementcertonly --webroot.
8.1 Sauvegarde préalable
sudo tar czf /root/backup-nginx-$(date +%F).tar.gz /etc/nginx /etc/letsencrypt
sudo certbot certificates
sudo nginx -t
nginx -v
Relever la méthode d'émission des certificats existants :
grep -E "authenticator|webroot_path|installer" /etc/letsencrypt/renewal/*.conf
8.2 Vhost temporaire pour l'émission du certificat
À ignorer si un certificat valide existe déjà pour le domaine.
sudo mkdir -p /var/www/certbot
sudo chown -R www-data:www-data /var/www/certbot
server {
listen 80;
listen [::]:80;
server_name NOM_DNS_PUBLIC;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type "text/plain";
}
location / { return 404; }
}
sudo ln -s /etc/nginx/sites-available/<fichier> /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
8.3 Émission du certificat
Test à blanc :
sudo certbot certonly --webroot -w /var/www/certbot \
-d NOM_DNS_PUBLIC \
--email <adresse@exemple.tld> --agree-tos --no-eff-email \
--deploy-hook "systemctl reload nginx" \
--dry-run
Puis émission réelle, en retirant --dry-run.
Chaque certificat dispose de son propre fichier de renouvellement dans /etc/letsencrypt/renewal/. Une nouvelle émission n'impacte donc pas les certificats déjà en place.
8.4 Vhost définitif
server {
listen 80;
listen [::]:80;
server_name NOM_DNS_PUBLIC;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type "text/plain";
}
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name NOM_DNS_PUBLIC;
ssl_certificate /etc/letsencrypt/live/NOM_DNS_PUBLIC/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/NOM_DNS_PUBLIC/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
client_max_body_size 16G;
client_body_timeout 3600s;
proxy_request_buffering off;
proxy_buffering off;
proxy_max_temp_file_size 0;
location = /.well-known/carddav { return 301 /remote.php/dav; }
location = /.well-known/caldav { return 301 /remote.php/dav; }
location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; }
location / {
proxy_pass http://IP_SERVEUR:PORT_BACKEND;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 60s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
}
}
Remarques :
-
Sur Nginx antérieur à 1.25.1, remplacer
listen 443 ssl;ethttp2 on;par une seule lignelisten 443 ssl http2;. -
client_max_body_sizedoit être supérieur ou égal àPHP_UPLOAD_LIMIT, faute de quoi Nginx interrompt les envois volumineux. -
Les redirections carddav et caldav évitent une alerte permanente dans la vue d'ensemble d'administration.
-
Le préfixe
^~sur acme-challenge garantit sa priorité sur les autres blocs.well-known. -
Connection "upgrade"en dur transmet l'en-tête sur toutes les requêtes. La forme rigoureuse repose sur une directivemapen contextehttp. Avant d'en ajouter un, vérifier qu'il n'existe pas déjà ailleurs, sous peine d'erreur de doublon à l'échelle du serveur :grep -rn "connection_upgrade" /etc/nginx/
8.5 Application et retour arrière
sudo nginx -t
sudo systemctl reload nginx
En cas d'échec du test de configuration, restaurer immédiatement la sauvegarde réalisée en 8.1, puis relancer nginx -t avant tout rechargement.
8.6 Vérifications finales
curl -I https://NOM_DNS_PUBLIC/status.php
curl -s https://NOM_DNS_PUBLIC/status.php
sudo certbot renew --dry-run
sudo systemctl status certbot.timer
Le test de renouvellement à blanc doit réussir pour l'ensemble des certificats du serveur, et non uniquement pour le nouveau.
Contrôler enfin, depuis l'interface web, la section Administration → Vue d'ensemble. Aucune alerte ne doit subsister concernant les en-têtes HTTP, les tâches de fond ou les index de base de données.
9. Durcissement réseau (optionnel)
Le port du backend étant déjà publié sur une seule adresse IP, l'exposition reste limitée. Pour un filtrage explicite :
sudo apt install -y ufw
sudo ufw allow 22/tcp
sudo ufw allow from IP_REVERSE_PROXY to any port PORT_BACKEND proto tcp
sudo ufw --force enable
sudo ufw status verbose
Limite connue : Docker manipule directement nftables et contourne partiellement les règles UFW. Un filtrage réellement contraignant nécessite des règles dans la chaîne
DOCKER-USER.
10. Exploitation courante
10.1 Commandes usuelles
cd REPERTOIRE_STACK
sudo docker compose ps # état des services
sudo docker compose logs -f app # journaux applicatifs
sudo docker compose logs --tail=50 cron # journaux des tâches de fond
sudo docker compose restart app # redémarrage du service web
sudo docker compose down # arrêt de la stack
sudo docker compose up -d # démarrage de la stack
10.2 Mise à jour
Ne jamais franchir plus d'une version majeure à la fois. Réaliser une sauvegarde complète au préalable.
cd REPERTOIRE_STACK
sudo docker compose pull
sudo docker compose up -d
sudo docker compose exec -u www-data app php occ status
sudo docker compose exec -u www-data app php occ upgrade
Purger périodiquement les images obsolètes :
sudo docker image prune -a
10.3 Sauvegarde
Trois éléments sont à sauvegarder conjointement : la base de données, le fichier de configuration et les données utilisateur.
cd REPERTOIRE_STACK
NCOCC="sudo docker compose exec -u www-data app php occ"
$NCOCC maintenance:mode --on
sudo docker compose exec -T db mariadb-dump -u root -p \
--single-transaction --default-character-set=utf8mb4 nextcloud \
> /chemin/sauvegarde/base-$(date +%F).sql
sudo tar czf /chemin/sauvegarde/config-$(date +%F).tgz \
POINT_DE_MONTAGE/html/config
$NCOCC maintenance:mode --off
Les données utilisateur situées dans POINT_DE_MONTAGE/data relèvent d'une sauvegarde par snapshot ou par synchronisation externe, leur volume étant incompatible avec une archive quotidienne.
10.4 Réinitialisation du mot de passe administrateur
Les variables NEXTCLOUD_ADMIN_USER et NEXTCLOUD_ADMIN_PASSWORD ne sont lues qu'à la première installation. Les modifier ensuite dans le fichier d'environnement reste sans effet.
cd REPERTOIRE_STACK
sudo docker compose exec -u www-data app php occ user:resetpassword ncadmin
10.5 Rotation des secrets de service
| Secret | Procédure |
|---|---|
| Mot de passe applicatif MariaDB | ALTER USER 'nextcloud'@'%' IDENTIFIED BY '<nouveau>'; → mettre à jour le .env et dbpassword dans POINT_DE_MONTAGE/html/config/config.php → docker compose up -d |
| Mot de passe Redis | Mettre à jour le .env → mettre à jour password dans la section redis de config.php → docker compose up -d |
| Mot de passe root MariaDB | ALTER USER 'root'@'localhost' IDENTIFIED BY '<nouveau>'; → mettre à jour le .env |
11. Résolution des incidents courants
| Symptôme | Cause probable | Action |
|---|---|---|
| Erreur Access through untrusted domain | Le nom DNS ne figure pas dans trusted_domains | occ config:system:set trusted_domains 1 --value="NOM_DNS_PUBLIC" |
| Liens générés en HTTP, boucle de redirection | overwriteprotocol ou trusted_proxies non définis | Appliquer les commandes de la section 7.4 |
| Les journaux affichent l'IP du proxy pour tous les clients | APACHE_DISABLE_REWRITE_IP absent ou trusted_proxies incorrect | Vérifier la variable, puis docker compose up -d |
| Erreur 413 lors d'un envoi volumineux | client_max_body_size inférieur à PHP_UPLOAD_LIMIT | Aligner les deux valeurs, puis recharger Nginx |
| Erreur 502 depuis le reverse proxy | Backend injoignable, ou proxy_pass en HTTPS vers un port en clair | Tester curl -I http://IP_SERVEUR:PORT_BACKEND/status.php depuis le proxy, vérifier le schéma dans proxy_pass |
| Alerte sur les tâches de fond | Conteneur cron arrêté, ou mode de tâche incorrect | docker compose logs cron, puis occ background:job:mode cron |
| Lenteurs générales, verrous de fichiers fréquents | Redis non utilisé pour le verrouillage distribué | occ config:system:get memcache.locking, vérifier la connectivité au conteneur |
| Démarrage en mode urgence après redémarrage | Disque de données absent et option nofail manquante | Ajouter nofail à la ligne concernée de /etc/fstab |
12. Récapitulatif des chemins
| Chemin | Contenu |
|---|---|
REPERTOIRE_STACK/compose.yaml | Définition de la stack |
REPERTOIRE_STACK/.env | Secrets, permissions 600 |
POINT_DE_MONTAGE/html | Code applicatif Nextcloud |
POINT_DE_MONTAGE/html/config | Configuration, à sauvegarder |
POINT_DE_MONTAGE/data | Données utilisateur |
POINT_DE_MONTAGE/db | Données MariaDB |
POINT_DE_MONTAGE/redis | Persistance Redis |
/etc/docker/daemon.json | Configuration du démon Docker |
/etc/fstab | Montage du disque de données |
/etc/nginx/sites-available/ | Vhosts du reverse proxy |
/etc/letsencrypt/live/ | Certificats en vigueur |
/etc/letsencrypt/renewal/ | Paramètres de renouvellement |
/var/www/certbot | Racine des défis ACME |
Fin de procédure