A place to cache linked articles (think custom and personal wayback machine)
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

index.html 43KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Docker in Action - fitter, happier, more productive - Real Python (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="https://realpython.com/blog/python/docker-in-action-fitter-happier-more-productive/">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. Docker in Action - fitter, happier, more productive - Real Python (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="https://realpython.com/blog/python/docker-in-action-fitter-happier-more-productive/">Source originale du contenu</a></h3>
  445. <div class="center-text">
  446. <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/docker-in-action.png" alt="docker in action"/>
  447. </div>
  448. <p>With Docker you can easily deploy a web application along with it’s dependencies, environment variables, and configuration settings – everything you need to recreate your environment quickly and efficiently.</p>
  449. <p>This tutorial looks at just that.</p>
  450. <p><strong>Updated 02/28/2015</strong>: Added <a href="https://docs.docker.com/compose/">Docker Compose</a> and upgraded Docker and boot2docker to the latest versions.</p>
  451. <hr/>
  452. <p><strong>We’ll start by creating a Docker container for running a Python Flask application. From there, we’ll look at a nice development workflow to manage the local development of an app as well as continuous integration and delivery, step by step …</strong></p>
  453. <blockquote><p>I (<a href="https://twitter.com/mikeherman">Michael Herman</a>) originally presented this workflow at <a href="https://www.pytennessee.org/"> PyTennessee</a> on February 8th, 2015. You can view the slides <a href="http://realpython.github.io/fitter-happier-docker/">here</a>, if interested.</p></blockquote>
  454. <h2>Workflow</h2>
  455. <ol>
  456. <li>Code locally on a feature branch</li>
  457. <li>Open a pull request on Github against the master branch</li>
  458. <li>Run automated tests against the Docker container</li>
  459. <li>If the tests pass, manually merge the pull request into master</li>
  460. <li>Once merged, the automated tests run again</li>
  461. <li>If the second round of tests pass, a build is created on Docker Hub</li>
  462. <li>Once the build is created, it’s then automatically (err, automagically) deployed to production</li>
  463. </ol>
  464. <div class="center-text">
  465. <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/steps.jpg" alt="docker in action workflow"/>
  466. </div>
  467. <blockquote><p>This tutorial is meant for Mac OS X users, and we’ll be utilizing the following tools/technologies – Python v2.7.9, Flask v0.10.1, Docker v1.5.0, Docker Compose, v1.1.0, boot2docker 1.5.0, Redis v2.8.19</p></blockquote>
  468. <p>Let’s get to it…</p>
  469. <hr/>
  470. <p>First, some Docker-specific terms:</p>
  471. <ul>
  472. <li>A <em>Dockerfile</em> is a file that contains a set of instructions used to create an <em>image</em>.</li>
  473. <li>An <em>image</em> is used to build and save snapshots (the state) of an environment.</li>
  474. <li>A <em>container</em> is an instantiated, live <em>image</em> that runs a collection of processes.</li>
  475. </ul>
  476. <blockquote><p>Be sure to check out the Docker <a href="https://docs.docker.com/">documentation</a> for more info on <a href="https://docs.docker.com/reference/builder/">Dockerfiles</a>, <a href="https://docs.docker.com/terms/image/">images</a>, and <a href="https://docs.docker.com/terms/container/">containers</a>.</p></blockquote>
  477. <h2>Why Docker?</h2>
  478. <p>You can truly mimic your production environment on your local machine. No more having to debug environment specific bugs or worrying that your app will perform differently in production.</p>
  479. <ol>
  480. <li>Version control for infrastructure</li>
  481. <li>Easily distribute/recreate your entire development environment</li>
  482. <li>Build once, run anywhere – aka The Holy Grail!</li>
  483. </ol>
  484. <h2>Docker Setup</h2>
  485. <p>Since Darwin (the kernel for OS X) does not have the Linux kernel features required to run Docker containers, we need to install <a href="http://boot2docker.io/">boot2docker</a> – which is a <em>lightweight</em>t Linux distribution designed specifically to run Docker. In essence, it starts a small VM that’s configured to run Docker containers.</p>
  486. <p>Create a new directory called “fitter-happier-docker” to house your Flask project.</p>
  487. <p>Next follow the instructions from the guide <a href="https://docs.docker.com/installation/mac/">Installing Docker on Mac OS X</a> to install both Docker and the official boot2docker package.</p>
  488. <p>Test the install:</p>
  489. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  490. <span class="line-number">2</span>
  491. <span class="line-number">3</span>
  492. </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>boot2docker version
  493. </span><span class="line">Boot2Docker-cli version: v1.5.0
  494. </span><span class="line">Git commit: ccd9032
  495. </span></code></pre></td></tr></table></div></figure>
  496. <h2>Compose Up!</h2>
  497. <p><a href="http://docs.docker.com/compose/">Docker Compose</a> is an orchestration framework that handles the building and running of multiple services (via separate containers) using a simple <em>.yml</em> file. It makes it super easy to link services together running in different containers.</p>
  498. <blockquote><p>Following along with me? Grab the code in a pre-Compose state from the <a href="https://github.com/realpython/fitter-happier-docker/releases/tag/pre-compose">repository</a>.</p></blockquote>
  499. <p>Start by installing the requirements via pip and then make sure Compose is installed:</p>
  500. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  501. <span class="line-number">2</span>
  502. <span class="line-number">3</span>
  503. </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>pip install docker-compose
  504. </span><span class="line"><span class="nv">$ </span>docker-compose --version
  505. </span><span class="line">docker-compose 1.1.0
  506. </span></code></pre></td></tr></table></div></figure>
  507. <p>Now let’s get our Flask application up and running along with Redis.</p>
  508. <p>Add a new file called <em>docker-compose.yml</em> to the root directory:</p>
  509. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  510. <span class="line-number">2</span>
  511. <span class="line-number">3</span>
  512. <span class="line-number">4</span>
  513. <span class="line-number">5</span>
  514. <span class="line-number">6</span>
  515. <span class="line-number">7</span>
  516. <span class="line-number">8</span>
  517. <span class="line-number">9</span>
  518. <span class="line-number">10</span>
  519. <span class="line-number">11</span>
  520. <span class="line-number">12</span>
  521. <span class="line-number">13</span>
  522. </pre></td><td class="code"><pre><code class="yaml"><span class="line"><span class="l-Scalar-Plain">web</span><span class="p-Indicator">:</span>
  523. </span><span class="line"> <span class="l-Scalar-Plain">build</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">web</span>
  524. </span><span class="line"> <span class="l-Scalar-Plain">volumes</span><span class="p-Indicator">:</span>
  525. </span><span class="line"> <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">web:/code</span>
  526. </span><span class="line"> <span class="l-Scalar-Plain">ports</span><span class="p-Indicator">:</span>
  527. </span><span class="line"> <span class="p-Indicator">-</span> <span class="s">"80:5000"</span>
  528. </span><span class="line"> <span class="l-Scalar-Plain">links</span><span class="p-Indicator">:</span>
  529. </span><span class="line"> <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">redis</span>
  530. </span><span class="line"> <span class="l-Scalar-Plain">command</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">python app.py</span>
  531. </span><span class="line"><span class="l-Scalar-Plain">redis</span><span class="p-Indicator">:</span>
  532. </span><span class="line"> <span class="l-Scalar-Plain">image</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">redis:2.8.19</span>
  533. </span><span class="line"> <span class="l-Scalar-Plain">ports</span><span class="p-Indicator">:</span>
  534. </span><span class="line"> <span class="p-Indicator">-</span> <span class="s">"6379:6379"</span>
  535. </span></code></pre></td></tr></table></div></figure>
  536. <p>Here we add the services that make up our stack:</p>
  537. <ol>
  538. <li><strong>web</strong>: First, we build the image from the “web” directory and then mount that directory to the “code” directory within the Docker container. The Flask app is ran via the <code>python app.py</code> command. This exposes port 5000 on the container, which is forwarded to port 80 on the host environment.</li>
  539. <li><strong>redis</strong>: Next, the Redis service is built from the Docker Hub “Redis” <a href="https://registry.hub.docker.com/_/redis/">image</a>. Port 6379 is exposed and forwarded.</li>
  540. </ol>
  541. <p>Did you notice the Dockerfile in the “web” directory? This file is used to build our image, starting with an Ubuntu base, the required dependencies are installed and the app is built.</p>
  542. <h2>Build and run</h2>
  543. <p>With one simple command we can build the image and run the container:</p>
  544. <figure class="code"><figcaption><span/></figcaption></figure>
  545. <div class="center-text">
  546. <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/figup.png" alt="fig up"/>
  547. </div>
  548. <p>This command builds an image for our Flask app, pulls the Redis image, and then starts everything up.</p>
  549. <p>Grab a cup of coffee. Or two. This will take some time the first time you build the container. That said, since Docker caches each step (or <em><a href="https://docs.docker.com/terms/layer/">layer</a></em>) of the build process from the Dockerfile, rebuilding will happen <em>much</em> quicker because only the steps that have <em>changed</em> since the last build are rebuilt.</p>
  550. <blockquote><p>If you do change a line/step/layer in your Dockerfile, it will recreate/rebuild everything in that line – so be mindful of this when you structure your Dockerfile.</p></blockquote>
  551. <p>Docker Compose brings each container up at once in parallel. Each container also has a unique name and each process within the stack trace/log is color-coded for readability.</p>
  552. <p>Ready to test?</p>
  553. <p>Open your web browser and navigate to the IP address associated with the <code>DOCKER_HOST</code> variable – i.e., <a href="http://192.168.59.103/,">http://192.168.59.103/,</a> in this example. (Run <code>boot2docker ip</code> to get the address.)</p>
  554. <p>You should see the text, “Hello! This page has been seen 1 times.” in your browser:</p>
  555. <div class="center-text">
  556. <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/test.png" alt="test flask app running on docker"/>
  557. </div>
  558. <p>Refresh. The page counter should have incremented.</p>
  559. <p>Kill the processes (Ctrl-C), and then run the following command to run the process in the background.</p>
  560. <figure class="code"><figcaption><span/></figcaption></figure>
  561. <p>Want to view the currently running processes?</p>
  562. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  563. <span class="line-number">2</span>
  564. <span class="line-number">3</span>
  565. <span class="line-number">4</span>
  566. <span class="line-number">5</span>
  567. </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>docker-compose ps
  568. </span><span class="line"> Name Command State Ports
  569. </span><span class="line">--------------------------------------------------------------------------------------------------
  570. </span><span class="line">fitterhappierdocker_redis_1 /entrypoint.sh redis-server Up 0.0.0.0:6379-&gt;6379/tcp
  571. </span><span class="line">fitterhappierdocker_web_1 python app.py Up 0.0.0.0:80-&gt;5000/tcp, 80/tcp
  572. </span></code></pre></td></tr></table></div></figure>
  573. <blockquote><p>Both processes are running in a different container, connected via Docker Compose!</p></blockquote>
  574. <h2>Next Steps</h2>
  575. <p>Once done, kill the processes via <code>docker-compose stop</code>, then run <code>boot2docker down</code> to gracefully shutdown the VM. Commit your changes locally, and then push to Github.</p>
  576. <p>So, what did we accomplish?</p>
  577. <p>We set up our local environment, detailing the basic process of building an <em>image</em> from a <em>Dockerfile</em> and then creating an instance of the <em>image</em> called a <em>container</em>. We tied everything together with Docker Compose to build and connect different containers for both the Flask app and Redis process.</p>
  578. <p><strong>Now, let’s look at a nice continuous integration workflow powered by <a href="https://circleci.com/">CircleCI</a></strong>.</p>
  579. <blockquote><p>Still with me? You can grab the updated code from the <a href="https://github.com/realpython/fitter-happier-docker/releases/tag/added-compose">repository</a>.</p></blockquote>
  580. <h2>Docker Hub</h2>
  581. <p>Thus far we’ve worked with Dockerfiles, images, and containers (abstracted by Docker Compose, of course).</p>
  582. <p>Are you familiar with the Git workflow? Images are like Git repositories while containers are similar to a cloned repository. Sticking with that metaphor, <a href="https://hub.docker.com/">Docker Hub</a>, which is repository of Docker images, is akin to Github.</p>
  583. <ol>
  584. <li>Signup <a href="https://hub.docker.com/account/signup/">here</a>, using your Github credentials.</li>
  585. <li>Then add a new automated build. And add your Github repo that you created and pushed to. Just accept all the default options, except for the “Dockerfile Location” – change this to “/web”.</li>
  586. </ol>
  587. <p>Once added, this will trigger an initial build. Make sure the build is successful.</p>
  588. <h3>Docker Hub for CI</h3>
  589. <p>Docker Hub, in itself, acts as a continuous integration server since you can configure it to create an <a href="https://docs.docker.com/userguide/dockerrepos/#automated-builds">automated build</a> every time you push a new commit to Github. In other words, it ensures you do not cause a regression that completely breaks the build process when the code base is updated.</p>
  590. <blockquote><p>There are some drawbacks to this approach – namely that you cannot push (via <code>docker push</code>) updated images directly to Docker Hub. Docker Hub must pull in changes from your repo and create the images itself to ensure that their are no errors. Keep this in mind as you go through this workflow. The Docker documentation is not clear with regard to this matter.</p></blockquote>
  591. <p>Let’s test this out. Add an assert to the test suite:</p>
  592. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  593. </pre></td><td class="code"><pre><code class="python"><span class="line"><span class="bp">self</span><span class="o">.</span><span class="n">assertNotEqual</span><span class="p">(</span><span class="n">four</span><span class="p">,</span> <span class="mi">102</span><span class="p">)</span>
  594. </span></code></pre></td></tr></table></div></figure>
  595. <p>Commit and push to Github to generate a new build on Docker Hub. Success?</p>
  596. <p><strong>Bottom-line:</strong> It’s good to know that if a commit does cause a regression that Docker Hub will catch it, but since this is the last line of defense before deploying (to either staging or production) you ideally want to catch any breaks before generating a new build on Docker Hub. Plus, you also want to run your unit and integration tests from a <em>true</em> continuous integration server – which is exactly where CircleCI comes into play.</p>
  597. <h2>CircleCI</h2>
  598. <div class="center-text">
  599. <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/circleci.png" alt="circleci"/>
  600. </div>
  601. <p><a href="https://circleci.com/">CircleCI</a> is a continuous integration and delivery platform that supports testing within Docker containers. Given a Dockerfile, CircleCI builds an image, starts a new container, and then runs tests inside that container.</p>
  602. <p>Remember the workflow we want? <a href="https://realpython.com/blog/python/docker-in-action-fitter-happier-more-productive/#workflow">Link</a>.</p>
  603. <p>Let’s take a look at how to achieve just that…</p>
  604. <h3>Setup</h3>
  605. <p>The best place to start is the excellent <a href="https://circleci.com/docs/getting-started">Getting started with CircleCI</a> guide…</p>
  606. <p>Sign up with your Github account, then add the Github repo to create a new project. This will automatically add a webhook to the repo so that anytime you push to Github a new build is triggered. You should receive an email once the hook is added.</p>
  607. <p>Next we need to add a configuration file to the root folder of repo so that CircleCI can properly create the build.</p>
  608. <h3><em>circle.yml</em></h3>
  609. <p>Add the following build commands/steps:</p>
  610. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  611. <span class="line-number">2</span>
  612. <span class="line-number">3</span>
  613. <span class="line-number">4</span>
  614. <span class="line-number">5</span>
  615. <span class="line-number">6</span>
  616. <span class="line-number">7</span>
  617. <span class="line-number">8</span>
  618. <span class="line-number">9</span>
  619. <span class="line-number">10</span>
  620. <span class="line-number">11</span>
  621. <span class="line-number">12</span>
  622. </pre></td><td class="code"><pre><code class="yaml"><span class="line"><span class="l-Scalar-Plain">machine</span><span class="p-Indicator">:</span>
  623. </span><span class="line"> <span class="l-Scalar-Plain">services</span><span class="p-Indicator">:</span>
  624. </span><span class="line"> <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">docker</span>
  625. </span><span class="line">
  626. </span><span class="line"><span class="l-Scalar-Plain">dependencies</span><span class="p-Indicator">:</span>
  627. </span><span class="line"> <span class="l-Scalar-Plain">override</span><span class="p-Indicator">:</span>
  628. </span><span class="line"> <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">pip install -r requirements.txt</span>
  629. </span><span class="line">
  630. </span><span class="line"><span class="l-Scalar-Plain">test</span><span class="p-Indicator">:</span>
  631. </span><span class="line"> <span class="l-Scalar-Plain">override</span><span class="p-Indicator">:</span>
  632. </span><span class="line"> <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">docker-compose run -d --no-deps web</span>
  633. </span><span class="line"> <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">python web/tests.py</span>
  634. </span></code></pre></td></tr></table></div></figure>
  635. <p>Essentially, we create a new image, run the container, then run your unit tests.</p>
  636. <blockquote><p>Notice how we’re using the command <code>docker-compose run -d --no-deps web</code>, to run the web process, instead of <code>docker-compose up</code>. This is because CircleCI already has Redis <a href="https://circleci.com/docs/environment#databases">running</a> and available to us for our tests. So, we just need to run the web process.</p></blockquote>
  637. <p>With the <em>circle.yml</em> file created, push the changes to Github to trigger a new build. <em>Remember: this will also trigger a new build on Docker Hub.</em></p>
  638. <p>Success?</p>
  639. <p>Before moving on, we need to change our workflow since we won’t be pushing directly to the master branch anymore.</p>
  640. <h3>Feature Branch Workflow</h3>
  641. <blockquote><p>For these unfamiliar with the Feature Branch workflow, check out <a href="https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow">this</a> excellent introduction.</p></blockquote>
  642. <p>Let’s run through a quick example…</p>
  643. <h3>Create the feature branch</h3>
  644. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  645. <span class="line-number">2</span>
  646. </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>git checkout -b circle-test master
  647. </span><span class="line">Switched to a new branch <span class="s1">'circle-test'</span>
  648. </span></code></pre></td></tr></table></div></figure>
  649. <h3>Update the app</h3>
  650. <p>Add a new assert in <em>tests.py</em>:</p>
  651. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  652. </pre></td><td class="code"><pre><code class="python"><span class="line"><span class="bp">self</span><span class="o">.</span><span class="n">assertNotEqual</span><span class="p">(</span><span class="n">four</span><span class="p">,</span> <span class="mi">60</span><span class="p">)</span>
  653. </span></code></pre></td></tr></table></div></figure>
  654. <h3>Issue a Pull Request</h3>
  655. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  656. <span class="line-number">2</span>
  657. <span class="line-number">3</span>
  658. </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>git add web/tests.py
  659. </span><span class="line"><span class="nv">$ </span>git commit -m <span class="s2">"circle-test"</span>
  660. </span><span class="line"><span class="nv">$ </span>git push origin circle-test
  661. </span></code></pre></td></tr></table></div></figure>
  662. <p>Even before you create the actual pull request, CircleCI starts creating the build. Go ahead and create the pull request, then once the tests pass on CircleCI, press the Merge button. Once merged, the build is triggered on Docker Hub.</p>
  663. <h3>Refactoring the workflow</h3>
  664. <p>If you jump back to the overall workflow at the <a href="https://realpython.com/blog/python/docker-in-action-fitter-happier-more-productive/#workflow">top of this post</a>, you’ll see that we don’t actually want to trigger a new build on Docker Hub until the tests pass against the master branch. So, let’s make some quick changes to the workflow:</p>
  665. <ol>
  666. <li>Open your repository on Docker Hub, and then under <em>Settings</em> click <em>Automated Build</em>.</li>
  667. <li>Uncheck the Active box: “When active we will build when new pushes occur”.</li>
  668. <li>Save.</li>
  669. <li>Click <em>Build Triggers</em> under <em>Settings</em></li>
  670. <li>Change the status to on.</li>
  671. <li>Copy the example curl command – i.e., <code>$ curl --data "build=true" -X POST https://registry.hub.docker.com/u/mjhea0/fitter-happier-docker/trigger/84957124-2b85-410d-b602-b48193853b66/</code></li>
  672. </ol>
  673. <p>Now add the following code to the bottom of your <em>circle.yml</em> file:</p>
  674. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  675. <span class="line-number">2</span>
  676. <span class="line-number">3</span>
  677. <span class="line-number">4</span>
  678. <span class="line-number">5</span>
  679. </pre></td><td class="code"><pre><code class="yaml"><span class="line"><span class="l-Scalar-Plain">deployment</span><span class="p-Indicator">:</span>
  680. </span><span class="line"> <span class="l-Scalar-Plain">hub</span><span class="p-Indicator">:</span>
  681. </span><span class="line"> <span class="l-Scalar-Plain">branch</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">master</span>
  682. </span><span class="line"> <span class="l-Scalar-Plain">commands</span><span class="p-Indicator">:</span>
  683. </span><span class="line"> <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">$DEPLOY</span>
  684. </span></code></pre></td></tr></table></div></figure>
  685. <p>Here we fire the <code>$DEPLOY</code> variable <em>after</em> we merge to master <em>and</em> the tests pass. We’ll add the actual value of this variable as an environment variable on CircleCI:</p>
  686. <ol>
  687. <li>Open up the <em>Project Settings</em>, and click <em>Environment variables</em>.</li>
  688. <li>Add a new variable with the name “DEPLOY” and paste the example curl command (that you copied) from Docker Hub as the value.</li>
  689. </ol>
  690. <p>Now let’s test.</p>
  691. <figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
  692. <span class="line-number">2</span>
  693. <span class="line-number">3</span>
  694. </pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>git add circle.yml
  695. </span><span class="line"><span class="nv">$ </span>git commit -m <span class="s2">"circle-test"</span>
  696. </span><span class="line"><span class="nv">$ </span>git push origin circle-test
  697. </span></code></pre></td></tr></table></div></figure>
  698. <p>Open a new pull request, and then once the tests pass on Circle CI, merge to master. Another build is trigged. Then once the tests pass again, a new build will be triggered on Docker Hub via the curl command. Nice.</p>
  699. <blockquote><p>Remember how I said that I configured Docker Hub to pull updated code to create a new image? Well, you could also set it up to where you can push images directly to Docker Hub. So once these test pass, you could simply push the image to update Docker Hub and then deploy to staging or production, directly from CircleCI. Since I have it set up differently, I handle the push to production from Docker Hub, not CircleCI. There’s positives and negatives to both approaches, as you will soon find out.</p></blockquote>
  700. <h2>Conclusion</h2>
  701. <p>So, we went over a nice development workflow that included setting up a local environment coupled with continuous integration via <a href="https://circleci.com/">CircleCI</a> (steps 1 through 6):</p>
  702. <ol>
  703. <li>Code locally on a feature branch</li>
  704. <li>Open a pull request on Github against the master branch</li>
  705. <li>Run automated tests against the Docker container</li>
  706. <li>If the tests pass, manually merge the pull request into master</li>
  707. <li>Once merged, the automated tests run again</li>
  708. <li>If the second round of tests pass, a build is created on Docker Hub</li>
  709. <li>Once the build is created, it’s then automatically (err, automagically) deployed to production</li>
  710. </ol>
  711. <p>What about the final piece – delivering this app to the production environment (step 7)? You can actually follow another one of my Docker blog <a href="https://blog.rainforestqa.com/2015-01-15-docker-in-action-from-deployment-to-delivery-part-3-continuous-delivery">posts</a> to extend this workflow to include delivery.</p>
  712. <p>Comment below if you have questions. Grab the final code <a href="https://github.com/realpython/fitter-happier-docker/releases/tag/final-code">here</a>. Cheers!</p>
  713. <hr/>
  714. <p><strong>If you have a workflow of your own, please let us know. I am currently experimenting with <a href="https://github.com/saltstack/salt">Salt</a> as well as <a href="https://www.tutum.co/">Tutum</a> to better handle orchestration and delivery on Digital Ocean and Linode.</strong></p>
  715. </article>
  716. </section>
  717. <nav id="jumpto">
  718. <p>
  719. <a href="/david/blog/">Accueil du blog</a> |
  720. <a href="https://realpython.com/blog/python/docker-in-action-fitter-happier-more-productive/">Source originale</a> |
  721. <a href="/david/stream/2019/">Accueil du flux</a>
  722. </p>
  723. </nav>
  724. <footer>
  725. <div>
  726. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  727. <p>
  728. Bonjour/Hi!
  729. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  730. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  731. </p>
  732. <p>
  733. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  734. </p>
  735. <p>
  736. Voici quelques articles choisis :
  737. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  738. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  739. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  740. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  741. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  742. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  743. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  744. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  745. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  746. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  747. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  748. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  749. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  750. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  751. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  752. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  753. </p>
  754. <p>
  755. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  756. </p>
  757. <p>
  758. Je ne traque pas ta navigation mais mon
  759. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  760. conserve des logs d’accès.
  761. </p>
  762. </div>
  763. </footer>
  764. <script type="text/javascript">
  765. ;(_ => {
  766. const jumper = document.getElementById('jumper')
  767. jumper.addEventListener('click', e => {
  768. e.preventDefault()
  769. const anchor = e.target.getAttribute('href')
  770. const targetEl = document.getElementById(anchor.substring(1))
  771. targetEl.scrollIntoView({behavior: 'smooth'})
  772. })
  773. })()
  774. </script>