ECT

Etoile Cercle Triangle - Star Circle Triangle

Introduction au langage de programmation Rust - Partie 2

| Comments

Article initialement publié sur https://www.technologies-ebusiness.com/langages/introduction-a-rust-partie-2

Nous poursuivons notre découverte du langage Rust après une première partie dans laquelle vous avez pratiqué quelques concepts de base de Rust : éléments de syntaxe, déclaration de variables immuables, fonctions et matching. Si la pratique de Rust directement dans le navigateur était adaptée pour débuter, je vous propose désormais de développer directement sur votre poste. Nous pourrons alors continuer à explorer les possibilités de Rust. Installer Rust

Le site officiel https://www.rust-lang.org propose des binaires ou des installeurs pour Linux, Mac ou Windows, qui ne se mettent pas à jour automatiquement. Sachant qu’une nouvelle version du langage Rust et des outils est publiée toutes les six semaines et que l’on est régulièrement amené à jongler entre les différents channels de Rust (stable, beta, nightly), je vous déconseille cette façon d’installer Rust.

Préférez plutôt l’usage de Rustup, le nouveau programme officiel d’installation de Rust (la page officielle de téléchargement commence aussi à y faire référence). Vous trouvez la procédure d’installation sur le site https://www.rustup.rs, à savoir une simple commande à taper dans une console : curl https://sh.rustup.rs -sSf | sh. Vous installez Rustup qui ensuite installe pour vous le compilateur Rust rustc, l’outil de gestion de dépendances et de build cargo, ainsi que le débuggueur ou le formateur de code de la version stable courante de Rust. Vous pourrez très simplement mettre à jour cette version stable avec rustup update quand vous en aurez besoin (toutes les six semaines !) ou bien installer la beta par exemple (rustup install beta).

Précis et efficace … quand vous avez curl et un interpréteur sh à disposition, ce qui n’est pas le cas par défaut sous Windows. Si vous développez avec Rust sous Windows, vous aurez, tôt ou tard, besoin aussi de Git pour versionner vos fichiers sources. Je vous recommande donc d’installer d’abord l’outillage de Git qui vient avec une ligne de commande assez complète et qui ressemble à ce que vous pourriez obtenir sur un Linux : https://git-for-windows.github.io. Téléchargez l’installeur 32 ou 64 bits selon votre machine, puis lancez la commande d’installation de Rustup dans le shell proposé par Git for Windows.

A la fin de l’installation, en ligne de commande (sous Windows celle de Git for Windows, n’oubliez pas car je ne le répéterai plus :-), tapez :

1
$ rustc --version

Si vous obtenez quelque chose comme rustc 1.10.0 (cfcb716cf 2016-07-03), le compilateur Rust est opérationnel sur votre poste !

Editeur de texte malin ou environnement de développement intégré (IDE) ?

Autant vous le dire d’emblée, les “assistants” de développement Rust sont loin d’être du niveau de ce que l’on peut trouver dans d’autres langages comme Java ou .Net. Il y a encore beaucoup de travail à faire mais on arrive tout même à se créer un environnement acceptable. Personnellement, je travaille avec Sublime Text, complété par quelques plugins qui me permettent d’avoir la coloration syntaxique, le formatage et le linting, une validation moins précise de la syntaxe. Je vous invite à consulter areweideyet.com pour choisir l’environnement le plus adapté à votre contexte : Vim, Emacs, Atom, Visual Studio, Eclipse … ou simplement, si vous souhaitez aller vite, un éditeur de texte type Notepad++, Geany ou Sublime Text seront suffisants.

Calculer division : reboot

Vous souvenez-vous de votre premier programme Rust écrit dans la première partie de ce dossier ? Nous allons le revisiter avec les nouveaux outils dont nous nous sommes dotés, et en particulier cargo. C’est un mix de Maven pour la structure standard des projets Rust, de npm pour la gestion de dépendances ou l’installation d’un programme et de commandes permettant de gérer un projet Rust.

Créons-en un de type “programme Rust” (grâce au paramètre --bin) :

1
$ cargo new --bin division

Avec votre éditeur de texte, copiez-collez le contenu de notre dernier programme dans le fichier src/main.rs du répertoire division créé par Cargo :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn calculer_division(x: i32, y: i32) -> i32  {
    match y {
        0 => panic!("Division par 0"),
        1 => x,
        _ => x / y
    }
}


fn main() {

    let resultat = calculer_division(-4, 2);

    println!("Résultat : {}", resultat);

}

Code complet sur ce Gist : https://git.io/vKcsS. Sauvegardez, puis lancez en ligne de commande :

1
2
3
4
5
6
7
$ cargo run

  Compiling division v0.1.0 (file://.../division)

    Running `target/debug/division`

Résultat : -2

Cargo compile et lance à la suite le programme sans argument. Si vous souhaitez simplement compiler, lancez cargo build et si vous souhaitez lancer le programme vous-même, sachez qu’il se trouve dans le répertoire target/debug :

1
2
3
$ ./target/debug/division

Résultat : -2

Une API sûre

Nous allons variabiliser le numérateur de notre division et le passer en paramètre de la ligne de commande. Explorons l’API de Rust pour ce besoin : lire les arguments en paramètre du programme s’effectue grâce à une fonction du module std::env déclarée comme ceci (Cf. https://doc.rust-lang.org/std/env/fn.args.html) :

1
pub fn args() -> Args

La fonction std::env::args() nous renvoie donc une instance de la struct Args (elle-même dans le module std::env), une structure qui va contenir des champs et des méthodes permettant de manipuler les arguments du programme, et ce de manière sûre. Qu’est-ce que cela signifie ? Sûr implique par exemple le bannissement de la “nullité”, source de fréquentes erreurs d’exécution (le fameux NullPointerException en Java par exemple). En Rust, toutes les API sont conçues pour renvoyer quelque chose -un résultat ou une erreur- et même “rien” est quelque chose en Rust.

Option - Some - None

Regardons la déclaration de la fonction nth de Args qui va nous permettre de récupérer le nième argument de notre programme. Elle est déclarée comme ceci (la signature est légèrement adaptée pour une compréhension plus aisée) :

1
fn nth(&mut self, n: usize) -> Option<String>

Mettons de coté le &mut self, nous y reviendrons par la suite. nth est une fonction qui renvoie un Option de type String. Option est une énumération Rust à deux variantes possibles :

1
2
3
4
pub enum Option<T> {
    None,
    Some(T),
}

Si vous substituez le type générique T par String, vous comprenez alors que la méthode nth peut renvoyer soit None, qui signifie qu’il y a pas de valeur à cette position, soit Some(String), qui signifie qu’il existe une valeur à la position demandée et quelle peut être extraite de la valeur de l’énumération. Ce qui est génial, c’est que ces variantes peuvent être “matchées” (Cf. partie 1 de ce dossier) :

1
2
3
4
5
6
7
8
use std::env;

...

let numerateur = match env::args().nth(1) {
    Some(argument) => argument,
    None => panic!("Argument obligatoire manquant : le numérateur")
};

Ici, on effectue aussi du pattern matching pour extraire la valeur contenue dans le Some. Some(argument) permet de déclarer la variable argument, affectée à la valeur contenu dans le Some, que l’on peut alors renvoyer (avec un return implicite). On arrête le programme prématurément avec la macro panic! et un message explicite en cas de None.

Enfin, cerise sur le gâteau, comme tout est expression en Rust, on peut affecter directement le retour de notre match à une variable : let numerateur = match env::args() ....

Notez l’utilisation du module env importé par une ligne en début de programme, use std::env;, à ajouter pour chaque module utilisé dans le programme.

Avec les Option et l’API de Rust, nous avons pu extraire notre paramètre de ligne de commande et l’avons rendu obligatoire. Ce n’est cependant pas suffisant : en effet, la fonction nth renvoie un Option<String> dans notre cas, ce qui veut dire que la variable numerateur ci-dessus est de type String, alors que nous attendons un i32. Il faut donc convertir notre valeur.

Result - Ok - Err

Rust propose une API de conversion de String :

1
fn parse<F>(&self) -> Result<F, F::Err> where F: FromStr

Prenons quelques instants pour comprendre la signature de cette fonction :

  • fn parse : “parse” est le nom de la fonction :-)
  • F : il s’agit d’un type générique, que vous pouvez substituer mentalement par le type que vous voulez obtenir après le parsing de votre String La définition en est donnée en fin de ligne, where F: FromStr, ce qui signifie que F doit respecter le contrat décrit dans le trait std::str::FromStr (c’est une sorte d’interface)
  • &self : il s’agit de la syntaxe spécifique à Rust qui indique que cette fonction n’est pas statique et qu’elle s’applique sur des instances de l’objet courant (String ici)
  • Result<F, F::Err> : le type de retour, encapsulant F et un type d’erreur associée à F

Result est la façon élégante en Rust de gérer les éventuels retours en erreur d’un traitement. Il est décrit comme ceci dans l’API de Rust :

1
2
3
4
pub enum Result<T, E> {
  Ok(T),
  Err(E),
}

Un peu comme Option vue précédemment, Result est une énumération à deux variantes, sur lesquelles on pourra matcher :

  • Ok est utilisé pour encapsuler le résultat d’un traitement qui s’est bien déroulé
  • Err permet de propager les erreurs de traitement

Il n’y a donc pas d’exception en Rust et l’usage des codes retours pour indiquer un résultat de traitement est considéré comme une mauvaise pratique, car non sûre.

Le type i32 implémentant bien le trait std::str::FromStr, on pourrait écrire la fonction dédiée au parsing d’une String en i32 :

1
fn parse(&self) -> Result<i32, ParseIntError>

L’association de ParseIntError à i32 est décrite dans sa documentation (cherchez impl FromStr for i32 et juste en dessous le type Err = ParseIntError).

Gardez en tête que c’est une simple vue de l’esprit, car elle est redondante avec la définition de la fonction décrite avec un type générique, mais elle permet de fixer les idées quand on n’est pas encore à l’aise avec la syntaxe de Rust.

Nous pouvons donc convertir notre String numerateur en i32 après un matching :

1
2
3
4
let numerateur = match numerateur.parse::<i32>() {
    Ok(numerateur) => numerateur,
    Err(error) => panic!("Impossible de convertir notre argument. Raison: {}", error)
};

Il faut un peu aider le compilateur car il ne peut pas deviner quelle conversion on souhaite appliquer. Pour cela, on utilise la syntaxe turbofish: ::<>, qui permet de spécifier le type de destination.

Si le parse s’est bien déroulé, le matching sur Ok permet d’extraire le numérateur sous forme de i32 désormais ; sinon avec le matching sur Err, on arrête une nouvelle fois le programme avec un message d’erreur adéquat.

Enfin, substituez le premier paramètre de l’appel de la fonction calculer_division par la variable numerateur (code complet : https://git.io/v6ypr), compilez et exécutez en une fois :

1
2
3
4
5
6
7
$ cargo run 4

  Compiling division v0.1.0 (file://.../division)

    Running `target/debug/division 4`

Résultat : 2

Bravo, en quelques lignes de code robustes, vous avez gérés la présence et l’absence d’argument lors de l’exécution de notre programme et ce, de manière plutôt élégante.

Ainsi se termine cette 2è partie de notre dossier consacré à Rust. Dans la prochaine partie, le niveau de difficulté montera d’un cran : il sera en effet temps de se confronter au borrow checker !

Introduction au langage de programmation Rust - Partie 1

| Comments

Article initialement publié sur https://www.technologies-ebusiness.com/langages/introduction-a-rust-partie-1

Rust est un jeune langage qui a pour ambition de se substituer au C/C++ en proposant de nouveaux paradigmes de programmation, une librairie standard de haut niveau et un écosystème riche soutenu par une communauté très active.

Multiplateformes (systèmes d’exploitation ou architectures de processeurs) et pourvu de tous les concepts de programmation attendus pour un langage moderne (programmation orientée “objet”, programmation fonctionnelle, facilités à développer des programmes d’exécution concurrente), Rust a beaucoup d’atouts pour séduire.

Au travers de cette série d’articles, nous allons découvrir ensemble ce langage qui me paraît être le plus excitant depuis ces dernières années.

Genèse

Avant de plonger dans le code, je vous propose de revenir sur les origines du langage. Elles remontent à 2010 : Graydon Hoare, ingénieur chez Mozilla, révèle ses travaux sur Rust. A l’époque, le “marché” des langages informatiques est un peu similaire à aujourd’hui, à savoir une domination de C/C++ et de Java, chacun cantonné à ses domaines de prédilection. En schématisant, Java règne sur l’informatique de “gestion” (systèmes d’information bancaires, assurance …) ainsi que sur Android, tandis que C/C++ est le choix par défaut de la programmation que je qualifierais de “système” ou “bas niveau” : système d’exploitation, pilotes, informatique embarquée, machines virtuelles (Java lui- même, ou NodeJS par exemple) …

Quelle caractéristique clivante peut bien isoler à ce point ces deux mondes de l’informatique ? Je pense qu’il s’agit principalement de la gestion de la mémoire, qui diverge totalement. Avec Java, elle est en grande partie masquée au développeur, c’est-à-dire que lorsqu’un développeur crée des objets en mémoire, il n’a pas vraiment à se soucier de son nettoyage. En effet, un processus qui s’exécute en arrière-plan, le ramasse-miette (garbage collector), se charge de détecter les objets qui ne seraient plus utilisés et de les supprimer de la mémoire. Le travail du développeur est donc grandement simplifié et celui-ci peut se concentrer sur le code métier le plus utile. C’est ce qui explique le succès de tous les langages à machine virtuelle et ramasse-miettes, comme Javascript ou .Net.

Si le fonctionnement du ramasse-miettes n’a pas ou peu de conséquences négatives -largement compensées par d’autres avantages- dans les applications de haut niveau comme celles dédiées au web, il est problématique, voire rédhibitoire pour d’autres. La latence, certes réduite, induite par l’initialisation ou le fonctionnement du ramasse-miettes, ne permet pas de construire des systèmes de plus bas niveau, comme les systèmes d’exploitation, les pilotes, les machines virtuelles elles-mêmes ou encore des programmes en ligne de commandes efficaces … Dans ces cas, la mémoire doit donc être gérée “à la main”.

En C/C++, la responsabilité du cycle de vie des structures ou objets en mémoire incombe au développeur et gare à la qualité du logiciel si cette tâche est mal effectuée : corruption de données, plantages, failles de sécurité … sont les conséquences principales d’une mauvaise gestion de la mémoire, phénomène encore aggravé dans un contexte de programme à exécution concurrente. Ne jetons pas la pierre aux développeurs : c’est une tâche ingrate, pénible et peu outillée. Alors dans ces conditions, comment produire des logiciels bas niveau de qualité ?

Depuis les premières ébauches, c’est précisément l’un des objectifs du langages Rust. Il ne repose ni sur un ramasse-miettes (ex: Java), ni sur une gestion mémoire manuelle du développeur (ex: C/C++). Rust propose une nouvelle façon de gérer la mémoire, qui se veut sûre et garantie à la compilation, ce qui permet d’en limiter l’impact sur les performances à l’exécution. Autant vous prévenir tout de suite : la courbe d’apprentissage de Rust est donc plus lente que dans d’autres langages mais largement compensée en qualité et fiabilité des programmes produits. Concrètement, vous allez transpirer au début mais serez fiers de la qualité de vos productions. Nous verrons par la suite comment elle se concrétise.

La syntaxe du langage, ainsi que la librairie standard, ont été débattues, testées, amendées par la -déjà très active- communauté, pendant des années, la première version stable 1.0 de Rust ayant été publiée en avril 2015. Ce délai a permis aussi de mettre en place une gouvernance ouverte et transparente qui régit l’évolution du langage, à laquelle chacun peut participer (board central, RFC ouvertes …). Trois channels de Rust sont édités en parallèle : nightly, beta et stable. De nouvelles versions de Rust stable et beta sont publiées toutes les 6 semaines, apportant à chaque fois leurs lots d’évolutions au langage.

Avant de démarrer, soyez attentifs lors de vos recherches sur Internet à la version de Rust concernée par les articles de blogs ou les solutions apportées sur Stack Overflow. On y trouve en effet beaucoup de contenus obsolètes, car applicables à des versions de Rust antérieures à la version 1.0.

Enfin, un mot sur la communauté de développeurs Rust. Elle est accueillante et bienveillante, vous trouverez de nombreux développeurs prêts à vous aider, à vous faire progresser ainsi qu’à critiquer votre code de manière constructive. Une bonne surprise et une des forces de ce langage.

Bonjour lecteurs !

Commençons à écrire quelques lignes de code, sans installation préalable de Rust sur votre poste. Rendez-vous sur https://play.rust-lang.org/ pour ouvrir l’interpréteur web Rust, qui fonctionne très bien sur smartphone ou tablette. Idéal pour tester simplement Rust sans vous prendre la tête ! Ne modifiez pas le paramétrage par défaut de la page (boutons du haut) et concentrez-vous sur la zone de texte centrale : c’est là qu’il faut taper le code Rust.

Lors de votre première connexion, le formulaire est déjà préinitialisé avec un morceau de code que je vous propose de substituer par un contenu plus adapté à notre contexte :

1
2
3
4
fn main() {

    println!("{}", "Bonjour Programmez!");
}

Si vous avez des difficultés à saisir ce code, vous pouvez ouvrir directement cette URL : https://is.gd/JzBaCy. Par la suite, je présenterai systématiquement un lien vers le code présenté, vous permettant éventuellement de le copier/coller.

Cliquez ensuite sur le gros bouton rouge “Run” en haut pour exécuter ce programme. Vous devez alors obtenir sous la zone de texte de code l’affichage de “Bonjour Programmez!”. Bravo, vous venez d’écrire votre premier programme Rust !

Analysons ensemble ces quelques lignes

La syntaxe s’inspire fortement du langage C (que l’on retrouve en Java, Javascript, .Net …) et le formatage du code est standardisé par le langage : indentation, espaces, positions des accolades … Notez le bouton “Format” dans l’interpréteur Web qui formate le code de la zone de texte comme on peut s’y attendre 🙂

  • fn permet de déclarer une fonction, nommée ici main et sans argument. Par convention, c’est le point d’entrée unique d’un programme écrit en Rust
  • println! permet d’écrire dans la sortie standard, du texte ou des structures plus complexes. Notez le ! qui signifie que println! est une macro. C’est une routine de génération de code à la compilation et un pattern de développement très utilisé par les développeurs Rust pour masquer une complexité

Je vous recommande l’utilisation de println! à deux arguments et plus, syntaxe familière à ceux qui connaissent les fonctions printf/fprintf du C ou str.format de Python : le 1er paramètre représente la mise en forme, les suivants les valeurs à substituer aux {} présents dans le contenu du 1er argument (plus de détails sur https://doc.rust-lang.org/std/fmt/)

  • ; obligatoire en fin de ligne la plupart du temps en Rust

Modifions maintenant notre programme en déclarant une variable (https://is.gd/SIDHI3) :

1
2
3
4
fn main() {
    let une_chaine = "Bonjour Programmez!";
    println!("{}", une_chaine);
}

Les variables se déclarent avec le mot-clé let, sont immuables (que l’on ne peut pas réaffecter) et sont fortement typées. Pourtant vous remarquerez que nous n’avons pas déclaré le type de cette variable. En effet, Rust met en œuvre de l’inférence de type, à savoir qu’il tente de détecter le type des variables mais pas des paramètres. Un allègement significatif de la syntaxe qui facilite l’écriture et la relecture. Il faudra cependant parfois l’aider à deviner correctement le type. Remarquez enfin la syntaxe de type snake case, avec des _ pour séparer les concepts dans les noms des variables (par opposition à la syntaxe camel case: uneVariable). Pratique, car le compilateur râlera si vous ne respectez pas cette convention.

Lancez avec “Run” pour compiler et exécuter ce code.

Poursuivons la découverte des concepts de Rust en introduisant une nouvelle fonction qui permet d’effectuer la division d’un nombre par un autre :

1
2
3
4
5
fn calculer_division(x: i32, y: i32) -> i32  {
    println!("Numérateur: {}", x);
    println!("Dénominateur: {}", y);
    x / y
}

L’appel de la fonction que nous venons de déclarer s’effectue exactement comme l’on s’y attend (programme complet : https://is.gd/0U61pa) :

1
2
3
4
5
6
...

fn main() {
    let resultat = calculer_division(-4, 2);
    println!("Résultat : {}", resultat);
}

Par choix, il n’y a pas d’inférence de type en Rust lors de la déclaration des fonctions : les types des paramètres d’entrée et du retour doivent être explicitement spécifiés. Le type des arguments est indiqué après chaque nom de variable suivi de : et le type de retour est spécifié après la flèche ->. Rust propose une liste de types primitifs très complète, puisque vous avez par exemple la possibilité de choisir des entiers, des flottants signés ou non signés, des entiers ou des flottants de “longueur” variable.

Exemples :

  • u8 : est un nombre de longueur 8 octets uniquement positif, donc un nombre compris entre 0 et 255 (2⁸-1)

  • i16 : est un nombre de longueur 16 octets, positif ou négatif, donc un nombre compris entre -32 768 (-216/2) et 32 767 (216/2-1)

Je vous conseille d’explorer la liste complète des types primitifs pour vous faire une idée des autres types disponibles (https://doc.rust-lang.org/book/primitive-types.html).

Les plus attentifs d’entre vous aurons remarqué que cette fonction ne “retourne” explicitement rien comparé à d’autres langages et que la dernière ligne ne se termine pas par un point-virgule, alors que l’on a précédemment vu qu’il était obligatoire. En Rust, il y a un return implicite sur la dernière expression exécutée d’une fonction. Attention, ce n’est pas nécessairement la dernière ligne de code de la fonction. Dans l’exemple ci-dessous, il y a 2 façons de sortir de la fonction et aucune d’entre elles ne correspond à la dernière ligne de code de la fonction.

1
2
3
4
5
6
7
8
9
10
11
fn calculer_division(x: i32, y: i32) -> i32  {
    if y != 0 {
        x / y
    } else {
        panic!("Division par 0")
    }
}

fn main() {
...
}

Exemple complet : https://is.gd/9LKj2d.

A propos du point-virgule, dans notre cas ici qui semble manquant, vous ne devez pas en mettre en fin de ligne. Essayez, vous aurez une erreur de compilation ! Pourquoi ? Rust est un langage basé sur les expressions et non sur des déclarations. Cela signifie que tout renvoie quelque chose : assigner une variable renvoie quelque chose, if renvoie quelque chose … L’expression x / y renvoie le résultat de la division, compatible avec le type i32, là où l’expression x / y; un résultat dont le type est (), incompatible avec i32.

Enfin, modifions une dernière fois notre programme pour induire une syntaxe plus habituelle pour les Rustacéens (traduction de Rustaceans”, le nom officiel des développeurs Rust) : le matching*.

1
2
3
4
5
6
7
8
9
10
11
fn calculer_division(x: i32, y: i32) -> i32  {
    match y {
        0 => panic!("Division par 0"),
        1 => x,
        _ => x / y
    }
}

fn main() {
...
}

Exemple complet : https://is.gd/X7889d.

La syntaxe parle d’elle-même, c’est simple à comprendre. Le match permet de gérer plus de cas que ne peut le faire un simple if tout en rendant obligatoire le traitement du cas par défaut. Qui n’a jamais oublié un else ou un case default en Java, par exemple ? En Rust, c’est impossible car le compilateur s’assure que tous les cas possibles de matching sont bien déclarés et gérés par le développeur. _ signifie au compilateur “tous les autres cas de matching” (dans notre cas, “tout sauf 0 et 1”). Sachez aussi que le matching offre aussi beaucoup plus de possibilités que ne montre ce simple exemple.

Ici se termine la 1ère partie de cette introduction à Rust. Je vous invite à poursuivre la lecture de ce dossier avec la seconde partie, dans laquelle vous installerez Rust et ses outils sur votre poste de développement et découvrirez d’autres concepts passionnants du langage, avec quelques notions de programmation fonctionnelle.

How to configure Docker DNS on Ubuntu in a corporate environment?

| Comments

When you are in a corporate environment, network is often configured to restrict outgoing requests, such as DNS resolution requests. By default, Docker uses Google DNS (8.8.8.8 and 8.8.4.4) to resolve domain names:

1
2
3
4
$ docker run busybox nslookup google.com
Server:    8.8.8.8
Address 1: 8.8.8.8
nslookup: can't resolve 'google.com'

If you are in a corporate environment, resolution fails because you have to use your internal DNS server.

You can find many, many documentation about how to configure Docker DNS on Ubuntu (such as official Docker doc), but none of them answer these requirements all together:

1/ Configuration must be portable: works at home or at work or anywhere else

2/ Configuration must be written in files not provided by a deb package to avoid conflicts after package updates

Portable configuration

Ubuntu provides Dnsmasq, a local DNS server configured to use DNS server of your network through DHCP. Docker can’t use it because it doesn’t allow to use a local DNS server if its IP address is a local configuration, such as 127.0.0.1.

So we’ll configure Dnsmasq to listen to another available IP address, such as the one provided by docker0 interface, to solve this issue.

Edit the new file /etc/NetworkManager/dnsmasq.d/docker.conf (as sudo):

1
interface=docker0

Then restart NetworkManager service:

1
sudo service network-manager restart

Configure Docker DNS

Extract the IP address of the docker0 interface:

1
2
$ docker network inspect bridge | grep Gateway
                    "Gateway": "172.17.0.1"

Then edit /etc/docker/daemon.json (as sudo):

1
2
3
{
    "dns": ["172.17.0.1"]
}

Restart Docker:

1
sudo service docker restart

Finally, check DNS resolution works again:

1
2
3
4
5
6
$ docker run busybox nslookup google.com
Server:    172.17.0.1
Address 1: 172.17.0.1
Name:      google.com
Address 1: 2a00:1450:4009:811::200e lhr26s02-in-x200e.1e100.net
Address 2: 216.58.198.174 lhr25s10-in-f14.1e100.net