Aller au contenu principal

Création d'un conteneur Docker

La prochaine étape est de configurer un conteneur Docker, c'est-à-dire un environnement virtuel et isolé contenant tout ce que votre application a besoin pour fonctionner (.NET, Postgres, le projet, etc.).

Un conteneur fait référence à plusieurs images et ajoute ensuite des paramètres personnalisés au besoin, comme par exemple les ports, les mots de passe, etc.

Plusieurs images sont déjà disponibles (ex.: Postgres), alors que d'autres ne le sont pas. En effet, il est possible de fournir une image "pré-faite" de Postgres, mais il n'est pas possible de faire une image à l'avance de toutes les applications!

Ainsi, il faudra créer une image pour votre projet. Cette image contiendra entre autres les commandes à exécuter pour compiler le projet et l'exécuter. Elle sera construite à partir d'une image de base fournie par Microsoft pour les projets .NET.

info

Si votre projet utilise un framework basé sur JavaScript (Vue.js, Nuxt, etc.) plutôt que .NET, référez-vous à la section Autres technologiques pour un point de départ adapté. Toutes les autres sections du guide (AWS, DNS, SSL, etc.) restent les mêmes, seuls le Dockerfile et le docker-compose.yml diffèrent.

Créer un Dockerfile pour votre projet

Pour définir une image, on crée un fichier Dockerfile.

  1. Dans le projet .NET que vous voulez déployer, créez un fichier Dockerfile (sans extension). Par exemple, dans mon cas, pour l'exemple, je crée un fichier dans Snowfall.Web.Mvc/Dockerfile

  2. Ensuite, prenez une image de base pertinente pour le projet. Dans le cas des projets .NET, Microsoft fournit une image de base pour chaque version de .NET.

    Snowfall.Web.Mvc/Dockerfile
    # Utilise l'image de base ASP.NET Runtime 9.0 pour exécuter des applications ASP.NET.
    FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
  3. Ensuite, on construit l'application.

    Snowfall.Web.Mvc/Dockerfile
    # Utilise l'image de base ASP.NET Runtime 9.0 pour exécuter des applications ASP.NET.
    FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base

    # Définit le répertoire de travail dans le conteneur comme étant "/app".
    WORKDIR /app

    # Expose le port 8080 pour permettre au conteneur d'accepter des connexions HTTP.
    EXPOSE 8080

    # Utilise l'image .NET SDK 9.0 pour la phase de construction (elle contient les outils nécessaires pour compiler l'application).
    FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

    # Définit le répertoire de travail dans le conteneur pour la phase de construction comme étant "/src".
    WORKDIR /src

    # Copie le fichier .csproj du projet "Snowfall.Web.Mvc" dans le répertoire "/src/Snowfall.Web.Mvc" du conteneur.
    COPY ["Snowfall.Web.Mvc/Snowfall.Web.Mvc.csproj", "Snowfall.Web.Mvc/"]

    # Exécute `dotnet restore` pour télécharger les dépendances spécifiées dans le fichier .csproj.
    RUN dotnet restore "Snowfall.Web.Mvc/Snowfall.Web.Mvc.csproj"

    # Copie l'ensemble du contenu du répertoire local dans le conteneur.
    COPY . .

    # Compile et publie l'application en mode Release, avec les fichiers de sortie placés dans "/app/publish".
    RUN dotnet publish "Snowfall.Web.Mvc/Snowfall.Web.Mvc.csproj" -c Release -o /app/publish

    FROM base AS final

    # Définit à nouveau "/app" comme répertoire de travail.
    WORKDIR /app

    # Copie les fichiers publiés de la phase de construction vers l'image finale.
    COPY --from=build /app/publish .

    # Définit le point d'entrée pour lancer l'application, en exécutant la commande `dotnet Snowfall.Web.Mvc.dll`.
    ENTRYPOINT ["dotnet", "Snowfall.Web.Mvc.dll"]
attention

Depuis .NET 8, le port par défaut dans les conteneurs Docker est passé de 80 à 8080. C'est pourquoi on utilise EXPOSE 8080 et la variable d'environnement ASPNETCORE_HTTP_PORTS=8080.

Si vous avez plusieurs projets à déployer, il vous faudra un Dockerfile par projet. La structure devrait être sensiblement la même. Nous y reviendrons.

Créer un fichier .dockerignore

Créez un fichier .dockerignore à la racine de la solution. Ce fichier permet d'exclure des fichiers et dossiers inutiles lors de la construction de l'image Docker, ce qui accélère le processus.

**/bin/
**/obj/
.git/
.env

Créer un fichier .env

Pour éviter de mettre des informations sensibles (mots de passe, etc.) directement dans le fichier docker-compose.yml (nous allons créer ce fichier à la prochaine étape!), on utilise un fichier .env contenant les variables d'environnement.

Par défaut, Docker Compose lira un fichier nommé .env à la racine du projet.

  1. Créez un fichier .env à la racine de la solution (même niveau que docker-compose) et mettez-y les informations dont votre application a besoin.
    .env
    POSTGRES_DB=snowfall_db
    POSTGRES_USER=snowfall_user
    POSTGRES_PASSWORD=motdepasse
    ASPNETCORE_HTTP_PORTS=8080
    DossierStorage=/storage-images
    APP_DATABASE_CONNECTION="Host=postgres;Port=5432;Database=snowfall_db;Username=snowfall_user;Password=motdepasse"
Les variables d'environnement

Vous connaissez déjà les variables d'environnement et les fichiers .env, la seule différence est que nous allons utiliser le fichier spécifiquement pour communiquer à Docker des informations sensibles.

Cela sera aussi très utile pour faire la distinction entre les valeurs locales et les valeurs sur le serveur.

Ajouter .env au .gitignore

Assurez-vous d'exclure le fichier .env dans le .gitignore. En effet, les fichiers .env peuvent contenir des données sensibles et on ne veut pas les rendre disponibles sur git.

.gitignore
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

# Node
node_modules/
npm-debug.log

.env
.env.docker

Créer un conteneur pour le projet

Votre projet n'est pas seulement une application .NET, vous avez aussi besoin d'une base de données et d'un serveur Web pouvant accepter les requêtes web.

Pour ce faire, on crée un conteneur à l'aide d'un fichier docker-compose.yml. Le fichier docker-compose.yml contient les références à tout ce que l'application a besoin pour fonctionner.

  1. À la racine de la solution, créez un fichier docker-compose.yml
  2. Un fichier docker-compose.yml contient au minimum la section services qui décrit les services à utiliser dans le conteneur et peut aussi contenir d'autres sections comme volumes afin de créer des espaces de stockage.

Service Postgres

Commençons par le service Postgres.

docker-compose.yml
services:
postgres:
image: postgres:17-alpine
container_name: snowfall_postgres
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped

volumes:
pgdata:
  • services: Définit une section pour configurer les services dans le fichier.
  • postgres: Nom du service (peut être utilisé par d'autres services).
  • image: postgres:17-alpine, utilise l'image Docker officielle de PostgreSQL, version 17, basée sur Alpine Linux.
  • container_name: Attribue un nom spécifique au conteneur Docker (snowfall_postgres) pour faciliter son identification.
  • environment: Définit des variables d'environnement pour configurer PostgreSQL. Les valeurs ${...} sont lues depuis le fichier .env.
    • POSTGRES_DB: Nom de la base de données à créer au démarrage.
    • POSTGRES_USER: Nom d'utilisateur par défaut.
    • POSTGRES_PASSWORD: Mot de passe pour l'utilisateur.
  • volumes: Monte un volume pour persister les données PostgreSQL.
    • pgdata:/var/lib/postgresql/data: Le volume nommé pgdata est lié au répertoire /var/lib/postgresql/data dans le conteneur, où PostgreSQL stocke ses données.
  • healthcheck: Vérifie que Postgres est prêt à accepter des connexions à l'aide de pg_isready. Le double $$ est nécessaire pour échapper le $ dans docker-compose.
  • restart: unless-stopped: Redémarre automatiquement le service sauf s'il est arrêté manuellement.

Service client (votre application)

docker-compose.yml
services:
postgres:
image: postgres:17-alpine
container_name: snowfall_postgres
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped

client:
build:
context: .
dockerfile: Snowfall.Web.Mvc/Dockerfile
container_name: snowfall_client
environment:
- ASPNETCORE_HTTP_PORTS=${ASPNETCORE_HTTP_PORTS}
- DossierStorage=${DossierStorage}
- ConnectionStrings__AppDatabaseConnection=${APP_DATABASE_CONNECTION}
expose:
- "8080"
volumes:
- shared-images:/storage-images
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped

volumes:
pgdata:
shared-images:
  • client: Nom du service pour votre application web.
  • build: Spécifie que l'image Docker pour ce service sera construite à partir d'un Dockerfile.
    • context: Définit le répertoire de construction comme étant le répertoire courant (.).
    • dockerfile: Indique le chemin du Dockerfile spécifique à utiliser.
  • container_name: Nom du conteneur pour ce service (snowfall_client).
  • environment: Définit des variables d'environnement pour configurer l'application. Les valeurs ${...} sont lues depuis le fichier .env.
    • ASPNETCORE_HTTP_PORTS: Configure le port HTTP sur lequel l'application écoute (8080).
      • DossierStorage : Variable spécifique à l'application, définissant le chemin de stockage (par exemple, pour des images).
  • expose: Expose le port 8080 pour rendre ce service accessible aux autres services dans le réseau Docker interne (mais pas à l'extérieur).
  • volumes: Monte un volume pour le partage des fichiers.
    • shared-images:/storage-images: Monte le volume nommé shared-images dans le répertoire /storage-images du conteneur.
  • depends_on: Indique que ce service dépend du service postgres. Avec condition: service_healthy, le client attend que Postgres soit réellement prêt (et non seulement démarré).
  • restart: unless-stopped: Redémarre automatiquement le service sauf s'il est arrêté manuellement.

Service de serveur Web (Caddy)

Pour que vous puissiez accepter les requêtes web, il faut un serveur web (ou un reverse proxy). En effet, lorsque vous exécutez le projet à l'aide de Rider ou Visual Studio, un serveur web est automatiquement fourni et vous n'avez pas à vous soucier de ce détail.

Ce n'est pas le cas lorsque vous déployez votre application, il faut une couche logicielle pour gérer les requêtes HTTP entrantes et sortantes. Nous utiliserons Caddy, un serveur web moderne qui gère automatiquement les certificats SSL (HTTPS).

  1. Créer un fichier Caddyfile (sans extension) à la racine de la solution avec une configuration de base:

    :80 {
    reverse_proxy client:8080
    }

    Cette configuration dit simplement: "Écoute sur le port 80 et redirige toutes les requêtes vers le service client sur le port 8080." C'est suffisant pour le développement local.

    attention

    En production, on remplacera :80 par votre nom de domaine pour activer le HTTPS automatique. Voir la section HTTPS et certificat SSL.

  2. Ajoutez un service caddy dans docker-compose faisant référence au fichier de configuration.

    docker-compose.yml
    services:
    postgres:
    image: postgres:17-alpine
    container_name: snowfall_postgres
    environment:
    - POSTGRES_DB=${POSTGRES_DB}
    - POSTGRES_USER=${POSTGRES_USER}
    - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    volumes:
    - pgdata:/var/lib/postgresql/data
    healthcheck:
    test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
    interval: 5s
    timeout: 5s
    retries: 5
    restart: unless-stopped

    client:
    build:
    context: .
    dockerfile: Snowfall.Web.Mvc/Dockerfile
    container_name: snowfall_client
    environment:
    - ASPNETCORE_HTTP_PORTS=${ASPNETCORE_HTTP_PORTS}
    - DossierStorage=${DossierStorage}
    - ConnectionStrings__AppDatabaseConnection=${APP_DATABASE_CONNECTION}
    expose:
    - "8080"
    volumes:
    - shared-images:/storage-images
    depends_on:
    postgres:
    condition: service_healthy
    restart: unless-stopped

    caddy:
    image: caddy:alpine
    container_name: snowfall_caddy
    ports:
    - "80:80"
    - "443:443"
    volumes:
    - ./Caddyfile:/etc/caddy/Caddyfile:ro
    - caddy_data:/data
    - caddy_config:/config
    depends_on:
    - client
    restart: unless-stopped

    volumes:
    pgdata:
    shared-images:
    caddy_data:
    caddy_config:
    • caddy_data: Volume qui stocke les certificats SSL générés automatiquement par Caddy.
    • caddy_config: Volume qui stocke la configuration en cache de Caddy.

Ajuster vos variables d'environnement (appsettings/.env)

Votre conteneur sera exécuté en environnement "production" et non "development". Ainsi, il est important d'ajuster votre appsettings ou votre .env, pour l'environnement de production, avec les bonnes valeurs de connection string, par exemple.

Bâtir le conteneur

Pour bâtir le conteneur et compiler l'application, il suffit d'exécuter la commande docker compose up dans un terminal à la racine du projet (au même niveau que le fichier docker-compose.yml).

docker compose up

Si tout se passe sans erreur, vous devriez voir quelque chose comme ceci:

...

snowfall_client | info: Microsoft.Hosting.Lifetime[14]
snowfall_client | Now listening on: http://[::]:8080
snowfall_client | info: Microsoft.Hosting.Lifetime[0]
snowfall_client | Application started. Press Ctrl+C to shut down.
snowfall_client | Hosting environment: Production
snowfall_client | Content root path: /app

Votre application devrait écouter sur le port 8080, et Caddy redirige le port 80 vers votre application. Vous devriez être en mesure d'accéder à localhost dans votre navigateur et votre site devrait s'afficher.

Images et fichiers partagés

Si votre application utilise un dossier d'images partagé (ex.: Storage), les fichiers de ce dossier ne seront pas automatiquement disponibles dans le conteneur. En effet, le volume Docker shared-images est vide à sa création.

Pour copier les fichiers par défaut dans le conteneur, ajoutez une ligne COPY dans votre Dockerfile, dans la phase finale:

Snowfall.Web.Mvc/Dockerfile
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
COPY Storage /storage-images
ENTRYPOINT ["dotnet", "Snowfall.Web.Mvc.dll"]
attention

Remplacez Storage par le chemin de votre dossier d'images relatif à la racine de la solution.

Ensuite, il est important de reconstruire l'image et de recréer le volume:

docker compose down
docker volume rm <nom-du-projet>_shared-images
docker compose up --build
info

Le paramètre --build est essentiel! Sans celui-ci, Docker Compose réutilise l'ancienne image en cache et ne prendra pas en compte les modifications au Dockerfile.

Vous pouvez trouver le nom exact du volume avec docker volume ls.

Build d'assets JavaScript (Vite, npm)

Si votre projet utilise un outil comme Vite pour compiler des assets JavaScript ou CSS, il faut ajouter une étape de build Node.js dans le Dockerfile. Sinon, les fichiers compilés (ex.: wwwroot/dist) ne seront pas présents dans le conteneur.

L'idée est d'ajouter une phase node-build qui installe les dépendances et exécute le build, puis de copier le résultat dans la phase .NET avant le dotnet publish.

Snowfall.Web.Mvc/Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8080

# Build des assets JS/CSS
FROM node:22-alpine AS node-build
WORKDIR /src
COPY Snowfall.Web.Mvc/package*.json ./
RUN npm ci
COPY Snowfall.Web.Mvc/ ./
RUN npm run build

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["Snowfall.Web.Mvc/Snowfall.Web.Mvc.csproj", "Snowfall.Web.Mvc/"]
RUN dotnet restore "Snowfall.Web.Mvc/Snowfall.Web.Mvc.csproj"
COPY . .
COPY --from=node-build /src/wwwroot/dist Snowfall.Web.Mvc/wwwroot/dist
RUN dotnet publish "Snowfall.Web.Mvc/Snowfall.Web.Mvc.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "Snowfall.Web.Mvc.dll"]
  • FROM node:22-alpine AS node-build: Utilise une image Node.js légère pour compiler les assets.
  • COPY Snowfall.Web.Mvc/package*.json ./: Copie package.json et package-lock.json en premier. Cela permet à Docker de mettre en cache l'étape npm ci et de ne la relancer que si les dépendances changent.
  • RUN npm ci: Installe les dépendances de manière déterministe (préférable à npm install pour les builds).
  • RUN npm run build: Exécute le build Vite qui génère les fichiers dans wwwroot/dist.
  • COPY --from=node-build ...: Copie le résultat du build Node.js dans le projet .NET avant la publication.
attention

Adaptez les chemins selon la structure de votre projet. Le package.json doit se trouver dans le dossier du projet (ex.: Snowfall.Web.Mvc/) et le dossier de sortie (wwwroot/dist) doit correspondre à la configuration de votre vite.config.js.