-
Notifications
You must be signed in to change notification settings - Fork 1
/
Dockerfile学习总结.html
995 lines (758 loc) · 108 KB
/
Dockerfile学习总结.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
<meta name="theme-color" content="#222">
<meta name="generator" content="Hexo 5.4.2">
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32-next.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16-next.png">
<link rel="mask-icon" href="/images/logo.svg" color="#222">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/lib/font-awesome/css/all.min.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/gh/fancyapps/fancybox@3/dist/jquery.fancybox.min.css">
<script id="hexo-configurations">
var NexT = window.NexT || {};
var CONFIG = {"hostname":"leeyuxun.github.io","root":"/","scheme":"Gemini","version":"7.8.0","exturl":false,"sidebar":{"position":"left","display":"post","padding":18,"offset":12,"onmobile":false},"copycode":{"enable":true,"show_result":true,"style":"mac"},"back2top":{"enable":true,"sidebar":true,"scrollpercent":true},"bookmark":{"enable":false,"color":"#222","save":"auto"},"fancybox":true,"mediumzoom":false,"lazyload":false,"pangu":true,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},"algolia":{"hits":{"per_page":10},"labels":{"input_placeholder":"Search for Posts","hits_empty":"We didn't find any results for the search: ${query}","hits_stats":"${hits} results found in ${time} ms"}},"localsearch":{"enable":true,"trigger":"auto","top_n_per_article":1,"unescape":false,"preload":false},"motion":{"enable":true,"async":false,"transition":{"post_block":"fadeIn","post_header":"slideDownIn","post_body":"slideDownIn","coll_header":"slideLeftIn","sidebar":"slideUpIn"}},"path":"./public/search.xml"};
</script>
<meta name="description" content="前言Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了构建镜像所需的指令和说明以及如何通过Dockerfile构建镜像。">
<meta property="og:type" content="article">
<meta property="og:title" content="Dockerfile学习总结">
<meta property="og:url" content="https://leeyuxun.github.io/Dockerfile%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.html">
<meta property="og:site_name" content="Leeyuxun の note">
<meta property="og:description" content="前言Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了构建镜像所需的指令和说明以及如何通过Dockerfile构建镜像。">
<meta property="og:locale" content="zh_CN">
<meta property="article:published_time" content="2022-01-17T15:17:45.000Z">
<meta property="article:modified_time" content="2023-05-07T07:37:53.524Z">
<meta property="article:author" content="李钰璕">
<meta property="article:tag" content="docker">
<meta property="article:tag" content="Dockerfile">
<meta name="twitter:card" content="summary">
<link rel="canonical" href="https://leeyuxun.github.io/Dockerfile%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.html">
<script id="page-configurations">
// https://hexo.io/docs/variables.html
CONFIG.page = {
sidebar: "",
isHome : false,
isPost : true,
lang : 'zh-CN'
};
</script>
<title>Dockerfile学习总结 | Leeyuxun の note</title>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V3499K2XZY"></script>
<script>
if (CONFIG.hostname === location.hostname) {
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-V3499K2XZY');
}
</script>
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?4d72a66931dff6410b32974da2e3df61";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<noscript>
<style>
.use-motion .brand,
.use-motion .menu-item,
.sidebar-inner,
.use-motion .post-block,
.use-motion .pagination,
.use-motion .comments,
.use-motion .post-header,
.use-motion .post-body,
.use-motion .collection-header { opacity: initial; }
.use-motion .site-title,
.use-motion .site-subtitle {
opacity: initial;
top: initial;
}
.use-motion .logo-line-before i { left: initial; }
.use-motion .logo-line-after i { right: initial; }
</style>
</noscript>
<style>mjx-container[jax="SVG"] {
direction: ltr;
}
mjx-container[jax="SVG"] > svg {
overflow: visible;
}
mjx-container[jax="SVG"][display="true"] {
display: block;
text-align: center;
margin: 1em 0;
}
mjx-container[jax="SVG"][justify="left"] {
text-align: left;
}
mjx-container[jax="SVG"][justify="right"] {
text-align: right;
}
g[data-mml-node="merror"] > g {
fill: red;
stroke: red;
}
g[data-mml-node="merror"] > rect[data-background] {
fill: yellow;
stroke: none;
}
g[data-mml-node="mtable"] > line[data-line] {
stroke-width: 70px;
fill: none;
}
g[data-mml-node="mtable"] > rect[data-frame] {
stroke-width: 70px;
fill: none;
}
g[data-mml-node="mtable"] > .mjx-dashed {
stroke-dasharray: 140;
}
g[data-mml-node="mtable"] > .mjx-dotted {
stroke-linecap: round;
stroke-dasharray: 0,140;
}
g[data-mml-node="mtable"] > svg {
overflow: visible;
}
[jax="SVG"] mjx-tool {
display: inline-block;
position: relative;
width: 0;
height: 0;
}
[jax="SVG"] mjx-tool > mjx-tip {
position: absolute;
top: 0;
left: 0;
}
mjx-tool > mjx-tip {
display: inline-block;
padding: .2em;
border: 1px solid #888;
font-size: 70%;
background-color: #F8F8F8;
color: black;
box-shadow: 2px 2px 5px #AAAAAA;
}
g[data-mml-node="maction"][data-toggle] {
cursor: pointer;
}
mjx-status {
display: block;
position: fixed;
left: 1em;
bottom: 1em;
min-width: 25%;
padding: .2em .4em;
border: 1px solid #888;
font-size: 90%;
background-color: #F8F8F8;
color: black;
}
foreignObject[data-mjx-xml] {
font-family: initial;
line-height: normal;
overflow: visible;
}
.MathJax path {
stroke-width: 3;
}
mjx-container[display="true"] {
overflow: auto hidden;
}
mjx-container[display="true"] + br {
display: none;
}
</style></head>
<body itemscope itemtype="http://schema.org/WebPage">
<div class="container use-motion">
<div class="headband"></div>
<header class="header" itemscope itemtype="http://schema.org/WPHeader">
<div class="header-inner"><div class="site-brand-container">
<div class="site-nav-toggle">
<div class="toggle" aria-label="切换导航栏">
<span class="toggle-line toggle-line-first"></span>
<span class="toggle-line toggle-line-middle"></span>
<span class="toggle-line toggle-line-last"></span>
</div>
</div>
<div class="site-meta">
<a href="/" class="brand" rel="start">
<span class="logo-line-before"><i></i></span>
<h1 class="site-title">Leeyuxun の note</h1>
<span class="logo-line-after"><i></i></span>
</a>
<p class="site-subtitle" itemprop="description">BUPT | SCSS</p>
</div>
<div class="site-nav-right">
<div class="toggle popup-trigger">
<i class="fa fa-search fa-fw fa-lg"></i>
</div>
</div>
</div>
<nav class="site-nav">
<ul id="menu" class="main-menu menu">
<li class="menu-item menu-item-home">
<a href="/" rel="section"><i class="fa fa-home fa-fw"></i>首页</a>
</li>
<li class="menu-item menu-item-tags">
<a href="/tags/" rel="section"><i class="fa fa-tags fa-fw"></i>标签</a>
</li>
<li class="menu-item menu-item-categories">
<a href="/categories/" rel="section"><i class="fa fa-th fa-fw"></i>分类</a>
</li>
<li class="menu-item menu-item-archives">
<a href="/archives/" rel="section"><i class="fa fa-archive fa-fw"></i>归档</a>
</li>
<li class="menu-item menu-item-links">
<a href="/links/" rel="section"><i class="fa fa-link fa-fw"></i>友链</a>
</li>
<li class="menu-item menu-item-search">
<a role="button" class="popup-trigger"><i class="fa fa-search fa-fw"></i>搜索
</a>
</li>
</ul>
</nav>
<div class="search-pop-overlay">
<div class="popup search-popup">
<div class="search-header">
<span class="search-icon">
<i class="fa fa-search"></i>
</span>
<div class="search-input-container">
<input autocomplete="off" autocapitalize="off"
placeholder="搜索..." spellcheck="false"
type="search" class="search-input">
</div>
<span class="popup-btn-close">
<i class="fa fa-times-circle"></i>
</span>
</div>
<div id="search-result">
<div id="no-result">
<i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>
</div>
</div>
</div>
</div>
</div>
</header>
<main class="main">
<div class="main-inner">
<div class="content-wrap">
<div class="content post posts-expand">
<article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
<link itemprop="mainEntityOfPage" href="https://leeyuxun.github.io/Dockerfile%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.html">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="image" content="/images/avatar.png">
<meta itemprop="name" content="李钰璕">
<meta itemprop="description" content="安全学习笔记">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Leeyuxun の note">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
Dockerfile学习总结
</h1>
<div class="post-meta">
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建时间:2022-01-17 23:17:45" itemprop="dateCreated datePublished" datetime="2022-01-17T23:17:45+08:00">2022-01-17</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar-check"></i>
</span>
<span class="post-meta-item-text">更新于</span>
<time title="修改时间:2023-05-07 15:37:53" itemprop="dateModified" datetime="2023-05-07T15:37:53+08:00">2023-05-07</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-folder"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/docker-learning/" itemprop="url" rel="index"><span itemprop="name">docker learning</span></a>
</span>
</span>
<span class="post-meta-item" title="阅读次数" id="busuanzi_container_page_pv" style="display: none;">
<span class="post-meta-item-icon">
<i class="fa fa-eye"></i>
</span>
<span class="post-meta-item-text">阅读次数:</span>
<span id="busuanzi_value_page_pv"></span>
</span>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了构建镜像所需的指令和说明以及如何通过Dockerfile构建镜像。<span id="more"></span></p>
<h2 id="Dockerfile-指令介绍"><a href="#Dockerfile-指令介绍" class="headerlink" title="Dockerfile 指令介绍"></a>Dockerfile 指令介绍</h2><p>以定制nginx镜像为例(构建好的镜像内会有一个 /usr/share/nginx/html/index.html 文件)</p>
<ul>
<li><p>在一个空白目录中,建立一个文本文件,并命名为 <code>Dockerfile</code>:</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mkdir mynginx</span><br><span class="line">cd mynginx</span><br><span class="line">touch Dockerfile</span><br></pre></td></tr></table></figure></li>
<li><p>其内容为:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> nginx</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">echo</span> <span class="string">'<h1>Hello, Docker!</h1>'</span> > /usr/share/nginx/html/index.html</span></span><br></pre></td></tr></table></figure>
<p> 这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,<code>FROM</code> 和 <code>RUN</code>。</p>
</li>
</ul>
<h3 id="FROM指定基础镜像"><a href="#FROM指定基础镜像" class="headerlink" title="FROM指定基础镜像"></a><code>FROM</code>指定基础镜像</h3><p>定制镜像,是以一个镜像为基础,在其上进行定制,而 <code>FROM</code> 就是指定<strong>基础镜像</strong>,因此一个 <code>Dockerfile</code> 中 <code>FROM</code> 是必备的指令,并且必须是第一条指令。在DockerHub上有非常多的高质量的官方镜像,包括服务类、语言环境类、操作系统。</p>
<p>除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 <code>scratch</code>。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> scratch</span><br><span class="line">...</span><br></pre></td></tr></table></figure>
<p>以 <code>scratch</code> 为基础镜像,意味着不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。</p>
<p>不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 <code>FROM scratch</code> 会让镜像体积更加小巧。(使用<strong>Go语言</strong>开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一)</p>
<h3 id="RUN执行命令"><a href="#RUN执行命令" class="headerlink" title="RUN执行命令"></a><code>RUN</code>执行命令</h3><p><code>RUN</code> 指令是用来执行命令行命令的。由于命令行的强大能力,<code>RUN</code> 指令在定制镜像时是最常用的指令之一。其格式有两种:</p>
<ol>
<li><p><strong>shell格式</strong>:<code>RUN <命令></code>,就像直接在命令行中输入的命令一样,即在Dockerfile中直接使用<code>RUN</code>指令。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">echo</span> <span class="string">'<h1>Hello, Docker!</h1>'</span> > /usr/share/nginx/html/index.html</span></span><br></pre></td></tr></table></figure></li>
<li><p> <strong>exec格式</strong>:<code>RUN ["可执行文件", "参数1", "参数2"]</code>,更像是函数调用中的格式。</p>
</li>
</ol>
<p>Dockerfile 中每一个指令都会建立一层,<code>RUN</code> 也不例外。每一个 <code>RUN</code> 的行为都会新建立一层,在其上执行这些命令,执行结束后,<code>commit</code> 这一层的修改,构成新的镜像。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> debian:stretch</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get install -y gcc libc6-dev make wget</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> wget -O redis.tar.gz <span class="string">"http://download.redis.io/releases/redis-5.0.3.tar.gz"</span></span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> -p /usr/src/redis</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> make -C /usr/src/redis</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> make -C /usr/src/redis install</span></span><br></pre></td></tr></table></figure>
<p>而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 (UFS是有最大层数限制的,比如 AUFS,曾经是最大不得超过 127 层)</p>
<p>上面的 <code>Dockerfile</code> 正确的写法应该是这样:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> debian:stretch</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">set</span> -x; buildDeps=<span class="string">'gcc libc6-dev make wget'</span> \</span></span><br><span class="line"><span class="language-bash"> && apt-get update \</span></span><br><span class="line"><span class="language-bash"> && apt-get install -y <span class="variable">$buildDeps</span> \</span></span><br><span class="line"><span class="language-bash"> && wget -O redis.tar.gz <span class="string">"http://download.redis.io/releases/redis-5.0.3.tar.gz"</span> \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">mkdir</span> -p /usr/src/redis \</span></span><br><span class="line"><span class="language-bash"> && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \</span></span><br><span class="line"><span class="language-bash"> && make -C /usr/src/redis \</span></span><br><span class="line"><span class="language-bash"> && make -C /usr/src/redis install \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">rm</span> -rf /var/lib/apt/lists/* \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">rm</span> redis.tar.gz \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">rm</span> -r /usr/src/redis \</span></span><br><span class="line"><span class="language-bash"> && apt-get purge -y --auto-remove <span class="variable">$buildDeps</span></span></span><br></pre></td></tr></table></figure>
<p>首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,没有使用多个 <code>RUN</code> 一一对应不同的命令,而是仅仅使用一个 <code>RUN</code> 指令,并使用 <code>&&</code> 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。</p>
<p>并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 <code>\</code> 的命令换行方式,以及行首 <code>#</code> 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。</p>
<p>此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 <code>apt</code> 缓存文件。这是很重要的一步,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。</p>
<h3 id="COPY复制文件"><a href="#COPY复制文件" class="headerlink" title="COPY复制文件"></a><code>COPY</code>复制文件</h3><p><code>COPY</code> 指令将从构建上下文目录中 <code><源路径></code> 的文件或目录复制到新的一层的镜像内的 <code><目标路径></code> 位置。</p>
<p>和 <code>RUN</code> 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。</p>
<ul>
<li>```dockerfile<br> COPY [–chown=<user>:<group>] <源路径>… <目标路径> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">- ```dockerfile</span><br><span class="line"> COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]</span><br></pre></td></tr></table></figure></group></user></li>
</ul>
<p>举例如下</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">COPY</span><span class="language-bash"> package.json /usr/src/app/</span></span><br></pre></td></tr></table></figure>
<p><code><源路径></code> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 <a target="_blank" rel="noopener" href="https://golang.org/pkg/path/filepath/#Match"><code>filepath.Match</code></a> 规则,如:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">COPY</span><span class="language-bash"> hom* /mydir/</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> hom?.txt /mydir/</span></span><br></pre></td></tr></table></figure>
<p><code><目标路径></code> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 <code>WORKDIR</code> 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。</p>
<p>此外,还需要注意一点,使用 <code>COPY</code> 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。</p>
<p>在使用该指令的时候还可以加上 <code>--chown=<user>:<group></code> 选项来改变文件的所属用户及所属组。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --<span class="built_in">chown</span>=55:mygroup files* /mydir/</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --<span class="built_in">chown</span>=bin files* /mydir/</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --<span class="built_in">chown</span>=1 files* /mydir/</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --<span class="built_in">chown</span>=10:11 files* /mydir/</span></span><br></pre></td></tr></table></figure>
<p>如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。</p>
<h3 id="ADD更高级的复制文件"><a href="#ADD更高级的复制文件" class="headerlink" title="ADD更高级的复制文件"></a><code>ADD</code>更高级的复制文件</h3><p><code>ADD</code> 指令和 <code>COPY</code> 的格式和性质基本一致。但是在 <code>COPY</code> 基础上增加了一些功能。</p>
<p>比如 <code><源路径></code> 可以是一个 <code>URL</code>,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <code><目标路径></code> 去。下载后的文件权限自动设置为 <code>600</code>,如果这并不是想要的权限,那么还需要增加额外的一层 <code>RUN</code> 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 <code>RUN</code> 指令进行解压缩。所以不如直接使用 <code>RUN</code> 指令,然后使用 <code>wget</code> 或者 <code>curl</code> 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。</p>
<p>如果 <code><源路径></code> 为一个 <code>tar</code> 压缩文件的话,压缩格式: <code>gzip</code>, <code>bzip2</code> 以及 <code>xz</code> 的情况下,<code>ADD</code> 指令将会自动解压缩这个压缩文件到 <code><目标路径></code> 去。</p>
<p>在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 <code>ubuntu</code> 中:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> scratch</span><br><span class="line"><span class="keyword">ADD</span><span class="language-bash"> ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /</span></span><br><span class="line">...</span><br></pre></td></tr></table></figure>
<p>但在某些情况下,如果希望复制个压缩文件进去,而不解压缩,这时就不可以使用 <code>ADD</code> 命令了。</p>
<p>在 Docker 官方的 <a href="">Dockerfile 最佳实践文档</a> 中要求,尽可能的使用 <code>COPY</code>,因为 <code>COPY</code> 的语义很明确,就是复制文件而已,而 <code>ADD</code> 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 <code>ADD</code> 的场合,就是所提及的需要自动解压缩的场合。</p>
<p>另外需要注意的是,<code>ADD</code> 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。</p>
<p>因此在 <code>COPY</code> 和 <code>ADD</code> 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 <code>COPY</code> 指令,仅在需要自动解压缩的场合使用 <code>ADD</code>。</p>
<p>在使用该指令的时候还可以加上 <code>--chown=<user>:<group></code> 选项来改变文件的所属用户及所属组。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ADD</span><span class="language-bash"> --<span class="built_in">chown</span>=55:mygroup files* /mydir/</span></span><br><span class="line"><span class="keyword">ADD</span><span class="language-bash"> --<span class="built_in">chown</span>=bin files* /mydir/</span></span><br><span class="line"><span class="keyword">ADD</span><span class="language-bash"> --<span class="built_in">chown</span>=1 files* /mydir/</span></span><br><span class="line"><span class="keyword">ADD</span><span class="language-bash"> --<span class="built_in">chown</span>=10:11 files* /mydir/</span></span><br></pre></td></tr></table></figure>
<h3 id="CMD容器启动命令"><a href="#CMD容器启动命令" class="headerlink" title="CMD容器启动命令"></a><code>CMD</code>容器启动命令</h3><p><code>CMD</code> 指令的格式和 <code>RUN</code> 相似,也是两种格式:</p>
<ul>
<li> <code>shell</code> 格式:<code>CMD <命令></code></li>
<li> <code>exec</code> 格式:<code>CMD ["可执行文件", "参数1", "参数2"...]</code></li>
<li> 参数列表格式:<code>CMD ["参数1", "参数2"...]</code>。在指定了 <code>ENTRYPOINT</code> 指令后,用 <code>CMD</code> 指定具体的参数。</li>
</ul>
<p>Docker 不是虚拟机,容器就是进程,在启动容器的时候,需要指定所运行的程序及参数。<code>CMD</code> 指令就是用于指定默认的容器主进程的启动命令的。</p>
<p>在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,<code>ubuntu</code> 镜像默认的 <code>CMD</code> 是 <code>/bin/bash</code>,如果直接 <code>docker run -it ubuntu</code> 的话,会直接进入 <code>bash</code>。也可以在运行时指定运行别的命令,如 <code>docker run -it ubuntu cat /etc/os-release</code>。这就是用 <code>cat /etc/os-release</code> 命令替换了默认的 <code>/bin/bash</code> 命令了,输出了系统版本信息。</p>
<p>在指令格式上,一般推荐使用 <code>exec</code> 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 <code>"</code>,而不要使用单引号。</p>
<p>如果使用 <code>shell</code> 格式,实际的命令会被包装为 <code>sh -c</code> 的参数的形式进行执行。比如:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CMD</span><span class="language-bash"> <span class="built_in">echo</span> <span class="variable">$HOME</span></span></span><br></pre></td></tr></table></figure>
<p>在实际执行中,会将其变更为:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [ <span class="string">"sh"</span>, <span class="string">"-c"</span>, <span class="string">"echo <span class="variable">$HOME</span>"</span> ]</span></span><br></pre></td></tr></table></figure>
<p>这就是为什么可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。</p>
<p>提到 <code>CMD</code> 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。</p>
<p>Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 <code>systemd</code> 去启动后台服务,容器内没有后台服务的概念。</p>
<p>一些初学者将 <code>CMD</code> 写为:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CMD</span><span class="language-bash"> service nginx start</span></span><br></pre></td></tr></table></figure>
<p>然后发现容器执行后就立即退出了。甚至在容器内去使用 <code>systemctl</code> 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。</p>
<p>对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。</p>
<p>而使用 <code>service nginx start</code> 命令,则是希望 upstart 来以后台守护进程形式启动 <code>nginx</code> 服务。而刚才说了 <code>CMD service nginx start</code> 会被理解为 <code>CMD [ "sh", "-c", "service nginx start"]</code>,因此主进程实际上是 <code>sh</code>。那么当 <code>service nginx start</code> 命令结束后,<code>sh</code> 也就结束了,<code>sh</code> 作为主进程退出了,自然就会令容器退出。</p>
<p>正确的做法是直接执行 <code>nginx</code> 可执行文件,并且要求以前台形式运行。比如:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [<span class="string">"nginx"</span>, <span class="string">"-g"</span>, <span class="string">"daemon off;"</span>]</span></span><br></pre></td></tr></table></figure>
<h3 id="ENTRYPOINT入口点"><a href="#ENTRYPOINT入口点" class="headerlink" title="ENTRYPOINT入口点"></a><code>ENTRYPOINT</code>入口点</h3><p><code>ENTRYPOINT</code> 的格式和 <code>RUN</code> 指令格式一样,分为 <code>exec</code> 格式和 <code>shell</code> 格式。</p>
<p><code>ENTRYPOINT</code> 的目的和 <code>CMD</code> 一样,都是在指定容器启动程序及参数。<code>ENTRYPOINT</code> 在运行时也可以替代,不过比 <code>CMD</code> 要略显繁琐,需要通过 <code>docker run</code> 的参数 <code>--entrypoint</code> 来指定。</p>
<p>当指定了 <code>ENTRYPOINT</code> 后,<code>CMD</code> 的含义就发生了改变,不再是直接的运行其命令,而是将 <code>CMD</code> 的内容作为参数传给 <code>ENTRYPOINT</code> 指令,换句话说实际执行时,将变为:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><<span class="keyword">ENTRYPOINT</span><span class="language-bash">> <span class="string">"<CMD>"</span></span></span><br></pre></td></tr></table></figure>
<p><code>ENTRYPOINT</code> 有如下使用场景</p>
<ul>
<li><p>让镜像变成像命令一样使用</p>
<p> 假设需要一个得知自己当前公网 IP 的镜像,可以先用 <code>CMD</code> 来实现:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> ubuntu:<span class="number">18.04</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update \</span></span><br><span class="line"><span class="language-bash"> && apt-get install -y curl \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">rm</span> -rf /var/lib/apt/lists/*</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [ <span class="string">"curl"</span>, <span class="string">"-s"</span>, <span class="string">"http://myip.ipip.net"</span> ]</span></span><br></pre></td></tr></table></figure>
<p> 假如使用 <code>docker build -t myip .</code> 构建镜像,如果需要查询当前公网 IP,只需要执行:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker run myip</span><br><span class="line">当前 IP:xxx.xxx.xxx.xxx 来自:xxx xx</span><br></pre></td></tr></table></figure>
<p> 这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果希望加参数呢?比如从上面的 <code>CMD</code> 中可以看到实质的命令是 <code>curl</code>,那么如果希望显示 HTTP 头信息,就需要加上 <code>-i</code> 参数。如果直接加 <code>-i</code> 参数给 <code>docker run myip</code> :</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker run myip -i</span><br><span class="line">docker: Error response from daemon: invalid header field value <span class="string">"oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in <span class="variable">$PATH</span>\"\n"</span>.</span><br></pre></td></tr></table></figure>
<p> 会出现可执行文件找不到的报错,<code>executable file not found</code>。已知跟在镜像名后面的是 <code>command</code>,运行时会替换 <code>CMD</code> 的默认值。因此这里的 <code>-i</code> 替换了原来的 <code>CMD</code>,而不是添加在原来的 <code>curl -s http://myip.ipip.net</code> 后面。而 <code>-i</code> 根本不是命令,所以自然找不到。</p>
<p> 那么如果希望加入 <code>-i</code> 这参数,就必须重新完整的输入这个命令:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run myip curl -s http://myip.ipip.net -i</span><br></pre></td></tr></table></figure>
<p> 这显然不是很好的解决方案,而使用 <code>ENTRYPOINT</code> 就可以解决这个问题。现在重新用 <code>ENTRYPOINT</code> 来实现这个镜像:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> ubuntu:<span class="number">18.04</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update \</span></span><br><span class="line"><span class="language-bash"> && apt-get install -y curl \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">rm</span> -rf /var/lib/apt/lists/*</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> [ <span class="string">"curl"</span>, <span class="string">"-s"</span>, <span class="string">"http://myip.ipip.net"</span> ]</span></span><br></pre></td></tr></table></figure>
<p> 再次尝试直接使用 <code>docker run myip -i</code></p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">docker run myip -i</span><br><span class="line">HTTP/1.1 200 OK</span><br><span class="line">Server: nginx/1.8.0</span><br><span class="line">Date: Tue, 22 Nov 2016 05:12:40 GMT</span><br><span class="line">Content-Type: text/html; charset=UTF-8</span><br><span class="line">Vary: Accept-Encoding</span><br><span class="line">X-Powered-By: PHP/5.6.24-1~dotdeb+7.1</span><br><span class="line">X-Cache: MISS from cache-2</span><br><span class="line">X-Cache-Lookup: MISS from cache-2:80</span><br><span class="line">X-Cache: MISS from proxy-2_6</span><br><span class="line">Transfer-Encoding: chunked</span><br><span class="line">Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006</span><br><span class="line">Connection: keep-alive</span><br><span class="line"></span><br><span class="line">当前 IP:xxx.xxx.xxx.xxx 来自:xxx xx</span><br></pre></td></tr></table></figure>
<p> 当存在 <code>ENTRYPOINT</code> 后,<code>CMD</code> 的内容将会作为参数传给 <code>ENTRYPOINT</code>,而这里 <code>-i</code> 就是新的 <code>CMD</code>,因此会作为参数传给 <code>curl</code>,从而达到了预期的效果。</p>
</li>
<li><p>应用运行前的准备工作</p>
<p> 启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。</p>
<p> 比如 <code>mysql</code> 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。</p>
<p> 此外,可能希望避免使用 <code>root</code> 用户去启动服务,从而提高安全性,而在启动服务前还需要以 <code>root</code> 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 <code>root</code> 身份执行,方便调试等。</p>
<p> 这些准备工作是和容器 <code>CMD</code> 无关的,无论 <code>CMD</code> 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 <code>ENTRYPOINT</code> 中去执行,而这个脚本会将接到的参数(也就是 <code><CMD></code>)作为命令,在脚本最后执行。比如官方镜像 <code>redis</code> 中就是这么做的:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> alpine:<span class="number">3.4</span></span><br><span class="line">...</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> addgroup -S redis && adduser -S -G redis redis</span></span><br><span class="line">...</span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> [<span class="string">"docker-entrypoint.sh"</span>]</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">6379</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [ <span class="string">"redis-server"</span> ]</span></span><br></pre></td></tr></table></figure>
<p> 可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 <code>ENTRYPOINT</code> 为 <code>docker-entrypoint.sh</code> 脚本。</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line">...</span><br><span class="line"><span class="comment"># allow the container to be started with `--user`</span></span><br><span class="line"><span class="keyword">if</span> [ <span class="string">"<span class="variable">$1</span>"</span> = <span class="string">'redis-server'</span> -a <span class="string">"<span class="subst">$(id -u)</span>"</span> = <span class="string">'0'</span> ]; <span class="keyword">then</span></span><br><span class="line"> find . \! -user redis -<span class="built_in">exec</span> <span class="built_in">chown</span> redis <span class="string">'{}'</span> +</span><br><span class="line"> <span class="built_in">exec</span> gosu redis <span class="string">"<span class="variable">$0</span>"</span> <span class="string">"<span class="variable">$@</span>"</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">exec</span> <span class="string">"<span class="variable">$@</span>"</span></span><br></pre></td></tr></table></figure>
<p> 该脚本的内容就是根据 <code>CMD</code> 的内容来判断,如果是 <code>redis-server</code> 的话,则切换到 <code>redis</code> 用户身份启动服务器,否则依旧使用 <code>root</code> 身份执行。比如:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker run -it redis <span class="built_in">id</span></span><br><span class="line">uid=0(root) gid=0(root) <span class="built_in">groups</span>=0(root)</span><br></pre></td></tr></table></figure></li>
</ul>
<h3 id="ENV设置环境变量"><a href="#ENV设置环境变量" class="headerlink" title="ENV设置环境变量"></a><code>ENV</code>设置环境变量</h3><p>格式有两种:</p>
<ul>
<li> <code>ENV <key> <value></code></li>
<li> <code>ENV <key1>=<value1> <key2>=<value2>...</code></li>
</ul>
<p>这个指令就是设置环境变量,无论是后面的其它指令,如 <code>RUN</code>,还是运行时的应用,都可以直接使用这里定义的环境变量。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ENV</span> VERSION=<span class="number">1.0</span> DEBUG=on \</span><br><span class="line"> NAME=<span class="string">"Happy Feet"</span></span><br></pre></td></tr></table></figure>
<p>上述例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。</p>
<p>定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 <code>node</code> 镜像 <code>Dockerfile</code> 中,就有类似这样的代码:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ENV</span> NODE_VERSION <span class="number">7.2</span>.<span class="number">0</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> curl -SLO <span class="string">"https://nodejs.org/dist/v<span class="variable">$NODE_VERSION</span>/node-v<span class="variable">$NODE_VERSION</span>-linux-x64.tar.xz"</span> \</span></span><br><span class="line"><span class="language-bash"> && curl -SLO <span class="string">"https://nodejs.org/dist/v<span class="variable">$NODE_VERSION</span>/SHASUMS256.txt.asc"</span> \</span></span><br><span class="line"><span class="language-bash"> && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \</span></span><br><span class="line"><span class="language-bash"> && grep <span class="string">" node-v<span class="variable">$NODE_VERSION</span>-linux-x64.tar.xz\$"</span> SHASUMS256.txt | <span class="built_in">sha256sum</span> -c - \</span></span><br><span class="line"><span class="language-bash"> && tar -xJf <span class="string">"node-v<span class="variable">$NODE_VERSION</span>-linux-x64.tar.xz"</span> -C /usr/local --strip-components=1 \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">rm</span> <span class="string">"node-v<span class="variable">$NODE_VERSION</span>-linux-x64.tar.xz"</span> SHASUMS256.txt.asc SHASUMS256.txt \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">ln</span> -s /usr/local/bin/node /usr/local/bin/nodejs</span></span><br></pre></td></tr></table></figure>
<p>先定义了环境变量 <code>NODE_VERSION</code>,其后的 <code>RUN</code> 这层里,多次使用 <code>$NODE_VERSION</code> 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 <code>7.2.0</code> 即可,<code>Dockerfile</code> 构建维护变得更轻松了。</p>
<p>下列指令可以支持环境变量展开: <code>ADD</code>、<code>COPY</code>、<code>ENV</code>、<code>EXPOSE</code>、<code>FROM</code>、<code>LABEL</code>、<code>USER</code>、<code>WORKDIR</code>、<code>VOLUME</code>、<code>STOPSIGNAL</code>、<code>ONBUILD</code>、<code>RUN</code>。</p>
<p>可以从指令列表里发现,环境变量可以使用的地方很多,很强大。通过环境变量,可以让一份 <code>Dockerfile</code> 制作更多的镜像,只需使用不同的环境变量即可。</p>
<h3 id="ARG构建参数"><a href="#ARG构建参数" class="headerlink" title="ARG构建参数"></a><code>ARG</code>构建参数</h3><p>格式:<code>ARG <参数名>[=<默认值>]</code></p>
<p>构建参数和 <code>ENV</code> 的效果一样,都是设置环境变量。所不同的是,<code>ARG</code> 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 <code>ARG</code> 保存密码之类的信息,因为 <code>docker history</code> 还是可以看到所有值的。</p>
<p><code>Dockerfile</code> 中的 <code>ARG</code> 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 <code>docker build</code> 中用 <code>--build-arg <参数名>=<值></code> 来覆盖。</p>
<p>灵活的使用 <code>ARG</code> 指令,能够在不修改 Dockerfile 的情况下,构建出不同的镜像。</p>
<p>ARG 指令有生效范围,如果在 <code>FROM</code> 指令之前指定,那么只能用于 <code>FROM</code> 指令中。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ARG</span> DOCKER_USERNAME=library</span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> ${DOCKER_USERNAME}/alpine</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">set</span> -x ; <span class="built_in">echo</span> <span class="variable">${DOCKER_USERNAME}</span></span></span><br></pre></td></tr></table></figure>
<p>使用上述 Dockerfile 会发现无法输出 <code>${DOCKER_USERNAME}</code> 变量的值,要想正常输出,必须在 <code>FROM</code> 之后再次指定 <code>ARG</code></p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 只在 FROM 中生效</span></span><br><span class="line"><span class="keyword">ARG</span> DOCKER_USERNAME=library</span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> ${DOCKER_USERNAME}/alpine</span><br><span class="line"></span><br><span class="line"><span class="comment"># 要想在 FROM 之后使用,必须再次指定</span></span><br><span class="line"><span class="keyword">ARG</span> DOCKER_USERNAME=library</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">set</span> -x ; <span class="built_in">echo</span> <span class="variable">${DOCKER_USERNAME}</span></span></span><br></pre></td></tr></table></figure>
<p>对于多阶段构建,尤其要注意这个问题</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 这个变量在每个 FROM 中都生效</span></span><br><span class="line"><span class="keyword">ARG</span> DOCKER_USERNAME=library</span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> ${DOCKER_USERNAME}/alpine</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">set</span> -x ; <span class="built_in">echo</span> 1</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> ${DOCKER_USERNAME}/alpine</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">set</span> -x ; <span class="built_in">echo</span> 2</span></span><br></pre></td></tr></table></figure>
<p>对于上述 Dockerfile 两个 <code>FROM</code> 指令都可以使用 <code>${DOCKER_USERNAME}</code>,对于在各个阶段中使用的变量都必须在每个阶段分别指定:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ARG</span> DOCKER_USERNAME=library</span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> ${DOCKER_USERNAME}/alpine</span><br><span class="line"></span><br><span class="line"><span class="comment"># 在FROM 之后使用变量,必须在每个阶段分别指定</span></span><br><span class="line"><span class="keyword">ARG</span> DOCKER_USERNAME=library</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">set</span> -x ; <span class="built_in">echo</span> <span class="variable">${DOCKER_USERNAME}</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">FROM</span> ${DOCKER_USERNAME}/alpine</span><br><span class="line"></span><br><span class="line"><span class="comment"># 在FROM 之后使用变量,必须在每个阶段分别指定</span></span><br><span class="line"><span class="keyword">ARG</span> DOCKER_USERNAME=library</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">set</span> -x ; <span class="built_in">echo</span> <span class="variable">${DOCKER_USERNAME}</span></span></span><br></pre></td></tr></table></figure>
<h3 id="VOLUME定义匿名卷"><a href="#VOLUME定义匿名卷" class="headerlink" title="VOLUME定义匿名卷"></a><code>VOLUME</code>定义匿名卷</h3><p>格式::</p>
<ul>
<li> <code>VOLUME ["<路径1>", "<路径2>"...]</code></li>
<li> <code>VOLUME <路径></code></li>
</ul>
<p>容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 <code>Dockerfile</code> 中,可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">VOLUME</span><span class="language-bash"> /data</span></span><br></pre></td></tr></table></figure>
<p>这里的 <code>/data</code> 目录就会在容器运行时自动挂载为匿名卷,任何向 <code>/data</code> 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -d -v mydata:/data xxxx</span><br></pre></td></tr></table></figure>
<p>在这行命令中,就使用了 <code>mydata</code> 这个命名卷挂载到了 <code>/data</code> 这个位置,替代了 <code>Dockerfile</code> 中定义的匿名卷的挂载配置。</p>
<h3 id="EXPOSE暴露端口"><a href="#EXPOSE暴露端口" class="headerlink" title="EXPOSE暴露端口"></a><code>EXPOSE</code>暴露端口</h3><p>格式: <code>EXPOSE <端口1> [<端口2>...]</code></p>
<p><code>EXPOSE</code> 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 <code>docker run -P</code> 时,会自动随机映射 <code>EXPOSE</code> 的端口。</p>
<p>要将 <code>EXPOSE</code> 和在运行时使用 <code>-p <宿主端口>:<容器端口></code> 区分开来。<code>-p</code>,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 <code>EXPOSE</code> 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。</p>
<h3 id="WORKDIR指定工作目录"><a href="#WORKDIR指定工作目录" class="headerlink" title="WORKDIR指定工作目录"></a><code>WORKDIR</code>指定工作目录</h3><p>格式: <code>WORKDIR <工作目录路径></code></p>
<p>使用 <code>WORKDIR</code> 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,<code>WORKDIR</code> 会建立目录。</p>
<p>之前提到一些初学者常犯的错误是把 <code>Dockerfile</code> 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">cd</span> /app</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">echo</span> <span class="string">"hello"</span> > world.txt</span></span><br></pre></td></tr></table></figure>
<p>如果将这个 <code>Dockerfile</code> 进行构建镜像运行后,会发现找不到 <code>/app/world.txt</code> 文件,或者其内容不是 <code>hello</code>。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 <code>Dockerfile</code> 中,这两行 <code>RUN</code> 命令的执行环境根本不同,是两个完全不同的容器。这就是对 <code>Dockerfile</code> 构建分层存储的概念不了解所导致的错误。</p>
<p>每一个 <code>RUN</code> 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 <code>RUN cd /app</code> 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。</p>
<p>因此如果需要改变以后各层的工作目录的位置,那么应该使用 <code>WORKDIR</code> 指令。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">echo</span> <span class="string">"hello"</span> > world.txt</span></span><br></pre></td></tr></table></figure>
<p>如果 <code>WORKDIR</code> 指令使用的相对路径,那么所切换的路径与之前的 <code>WORKDIR</code> 有关:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /a</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> b</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> c</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">pwd</span></span></span><br></pre></td></tr></table></figure>
<p><code>RUN pwd</code> 的工作目录为 <code>/a/b/c</code></p>
<h3 id="USER指定当前用户"><a href="#USER指定当前用户" class="headerlink" title="USER指定当前用户"></a><code>USER</code>指定当前用户</h3><p>格式:<code>USER <用户名>[:<用户组>]</code></p>
<p><code>USER</code> 指令和 <code>WORKDIR</code> 相似,都是改变环境状态并影响以后的层。<code>WORKDIR</code> 是改变工作目录,<code>USER</code> 则是改变之后层的执行 <code>RUN</code>, <code>CMD</code> 以及 <code>ENTRYPOINT</code> 这类命令的身份。</p>
<p>注意,<code>USER</code> 只是帮助切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">RUN</span><span class="language-bash"> groupadd -r redis && useradd -r -g redis redis</span></span><br><span class="line"><span class="keyword">USER</span> redis</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> [ <span class="string">"redis-server"</span> ]</span></span><br></pre></td></tr></table></figure>
<p>如果以 <code>root</code> 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 <code>su</code> 或者 <code>sudo</code>,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 <a target="_blank" rel="noopener" href="https://github.com/tianon/gosu"><code>gosu</code></a>。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 建立 redis 用户,并使用 gosu 换另一个用户执行命令</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> groupadd -r redis && useradd -r -g redis redis</span></span><br><span class="line"><span class="comment"># 下载 gosu</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> wget -O /usr/local/bin/gosu <span class="string">"https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64"</span> \</span></span><br><span class="line"><span class="language-bash"> && <span class="built_in">chmod</span> +x /usr/local/bin/gosu \</span></span><br><span class="line"><span class="language-bash"> && gosu nobody <span class="literal">true</span></span></span><br><span class="line"><span class="comment"># 设置 CMD,并以另外的用户执行</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [ <span class="string">"exec"</span>, <span class="string">"gosu"</span>, <span class="string">"redis"</span>, <span class="string">"redis-server"</span> ]</span></span><br></pre></td></tr></table></figure>
<h3 id="HEALTHCHECK健康检查"><a href="#HEALTHCHECK健康检查" class="headerlink" title="HEALTHCHECK健康检查"></a><code>HEALTHCHECK</code>健康检查</h3><p>格式:</p>
<ul>
<li> <code>HEALTHCHECK [选项] CMD <命令></code>:设置检查容器健康状况的命令</li>
<li> <code>HEALTHCHECK NONE</code>:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令</li>
</ul>
<p><code>HEALTHCHECK</code> 指令是告诉 Docker 应该如何进行判断容器的状态是否正常。</p>
<p>在没有 <code>HEALTHCHECK</code> 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。</p>
<p>而自 1.12 之后,Docker 提供了 <code>HEALTHCHECK</code> 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。</p>
<p>当在一个镜像指定了 <code>HEALTHCHECK</code> 指令后,用其启动容器,初始状态会为 <code>starting</code>,在 <code>HEALTHCHECK</code> 指令检查成功后变为 <code>healthy</code>,如果连续一定次数失败,则会变为 <code>unhealthy</code>。</p>
<p><code>HEALTHCHECK</code> 支持下列选项:</p>
<ul>
<li> <code>--interval=<间隔></code>:两次健康检查的间隔,默认为 30 秒;</li>
<li> <code>--timeout=<时长></code>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;</li>
<li> <code>--retries=<次数></code>:当连续失败指定次数后,则将容器状态视为 <code>unhealthy</code>,默认 3 次。</li>
</ul>
<p>和 <code>CMD</code>, <code>ENTRYPOINT</code> 一样,<code>HEALTHCHECK</code> 只可以出现一次,如果写了多个,只有最后一个生效。</p>
<p>在 <code>HEALTHCHECK [选项] CMD</code> 后面的命令,格式和 <code>ENTRYPOINT</code> 一样,分为 <code>shell</code> 格式,和 <code>exec</code> 格式。命令的返回值决定了该次健康检查的成功与否:<code>0</code>:成功;<code>1</code>:失败;<code>2</code>:保留,不要使用这个值。</p>
<p>假设有个镜像是个最简单的 Web 服务,希望增加健康检查来判断其 Web 服务是否在正常工作,可以用 <code>curl</code> 来帮助判断,其 <code>Dockerfile</code> 的 <code>HEALTHCHECK</code> 可以这么写:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> nginx</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update && apt-get install -y curl && <span class="built_in">rm</span> -rf /var/lib/apt/lists/*</span></span><br><span class="line"><span class="keyword">HEALTHCHECK</span><span class="language-bash"> --interval=5s --<span class="built_in">timeout</span>=3s \</span></span><br><span class="line"><span class="language-bash"> CMD curl -fs http://localhost/ || <span class="built_in">exit</span> 1</span></span><br></pre></td></tr></table></figure>
<p>设置每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 <code>curl -fs http://localhost/ || exit 1</code> 作为健康检查命令。</p>
<p>使用 <code>docker build</code> 来构建这个镜像:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -t myweb:v1 .</span><br></pre></td></tr></table></figure>
<p>构建好了后,启动一个容器:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="language-bash"> -d --name web -p 80:80 myweb:v1</span></span><br></pre></td></tr></table></figure>
<p>当运行该镜像后,可以通过 <code>docker container ls</code> 看到最初的状态为 <code>(health: starting)</code>:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker container ls</span><br><span class="line">CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES</span><br><span class="line"><span class="number">03</span>e28eb00bd0 myweb:v1 <span class="string">"nginx -g 'daemon off"</span> <span class="number">3</span> seconds ago Up <span class="number">2</span> seconds (health: starting) <span class="number">80</span>/tcp, <span class="number">443</span>/tcp web</span><br></pre></td></tr></table></figure>
<p>在等待几秒钟后,再次 <code>docker container ls</code>,就会看到健康状态变化为了 <code>(healthy)</code>:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker container ls</span><br><span class="line">CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES</span><br><span class="line"><span class="number">03</span>e28eb00bd0 myweb:v1 <span class="string">"nginx -g 'daemon off"</span> <span class="number">18</span> seconds ago Up <span class="number">16</span> seconds (healthy) <span class="number">80</span>/tcp, <span class="number">443</span>/tcp web</span><br></pre></td></tr></table></figure>
<p>如果健康检查连续失败超过了重试次数,状态就会变为 <code>(unhealthy)</code>。</p>
<p>为了帮助排障,健康检查命令的输出(包括 <code>stdout</code> 以及 <code>stderr</code>)都会被存储于健康状态里,可以用 <code>docker inspect</code> 来查看。</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">docker inspect --format <span class="string">'{{json .State.Health}}'</span> web | python -m json.tool</span><br><span class="line">{</span><br><span class="line"> <span class="string">"FailingStreak"</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="string">"Log"</span>: [</span><br><span class="line"> {</span><br><span class="line"> <span class="string">"End"</span>: <span class="string">"2016-11-25T14:35:37.940957051Z"</span>,</span><br><span class="line"> <span class="string">"ExitCode"</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="string">"Output"</span>: <span class="string">"<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n"</span>,</span><br><span class="line"> <span class="string">"Start"</span>: <span class="string">"2016-11-25T14:35:37.780192565Z"</span></span><br><span class="line"> }</span><br><span class="line"> ],</span><br><span class="line"> <span class="string">"Status"</span>: <span class="string">"healthy"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="ONBUILD延迟构建命令"><a href="#ONBUILD延迟构建命令" class="headerlink" title="ONBUILD延迟构建命令"></a><code>ONBUILD</code>延迟构建命令</h3><p>格式:<code>ONBUILD <其它指令></code></p>
<p><code>ONBUILD</code> 是一个特殊的指令,它后面跟的是其它指令,比如 <code>RUN</code>, <code>COPY</code> 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。</p>
<p><code>Dockerfile</code> 中的其它指令都是为了定制当前镜像而准备的,唯有 <code>ONBUILD</code> 是为了帮助别人定制而准备的。</p>
<p>假设制作 Node.js 所写的应用的镜像。已知 Node.js 使用 <code>npm</code> 进行包管理,所有依赖、配置、启动信息等会放到 <code>package.json</code> 文件里。在拿到程序代码后,需要先进行 <code>npm install</code> 才可以获得所有需要的依赖。然后就可以通过 <code>npm start</code> 来启动应用。因此,一般来说会这样写 <code>Dockerfile</code>:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> node:slim</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> /app</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./package.json /app</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> [ <span class="string">"npm"</span>, <span class="string">"install"</span> ]</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . /app/</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [ <span class="string">"npm"</span>, <span class="string">"start"</span> ]</span></span><br></pre></td></tr></table></figure>
<p>把这个 <code>Dockerfile</code> 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果还有第二个 Node.js 项目,那就再把这个 <code>Dockerfile</code> 复制到第二个项目里。那如果有第三个项目就需要再复制一次。文件的副本越多,版本控制就越困难。</p>
<p>如果第一个 Node.js 项目在开发过程中,发现这个 <code>Dockerfile</code> 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 <code>Dockerfile</code>,再次构建,问题解决。第一个项目没问题了,但是第二个项目仍然存在相同的问题,并不会被自动修复。</p>
<p>一个解决方法是做一个基础镜像,然后各个项目使用这个基础镜像。这样基础镜像更新,各个项目不用同步 <code>Dockerfile</code> 的变化,重新构建后就继承了基础镜像的更新。那么上面的这个 <code>Dockerfile</code> 就会变为:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> node:slim</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> /app</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [ <span class="string">"npm"</span>, <span class="string">"start"</span> ]</span></span><br></pre></td></tr></table></figure>
<p>把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 <code>my-node</code> 的话,各个项目内的自己的 <code>Dockerfile</code> 就变为:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> my-node</span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./package.json /app</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> [ <span class="string">"npm"</span>, <span class="string">"install"</span> ]</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . /app/</span></span><br></pre></td></tr></table></figure>
<p>基础镜像变化后,各个项目都用这个 <code>Dockerfile</code> 重新构建镜像,会继承基础镜像的更新。</p>
<p>那么,问题解决了么?没有。准确说,只解决了一半。如果这个 <code>Dockerfile</code> 里面有些东西需要调整呢?比如 <code>npm install</code> 都需要加一些参数,那怎么办?这一行 <code>RUN</code> 是不可能放入基础镜像的,因为涉及到了当前项目的 <code>./package.json</code>,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 <code>Dockerfile</code> 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。</p>
<p><code>ONBUILD</code> 可以解决这个问题。使用 <code>ONBUILD</code> 重新写一下基础镜像的 <code>Dockerfile</code>:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> node:slim</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> /app</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">ONBUILD</span> <span class="keyword">COPY</span><span class="language-bash"> ./package.json /app</span></span><br><span class="line"><span class="keyword">ONBUILD</span> <span class="keyword">RUN</span><span class="language-bash"> [ <span class="string">"npm"</span>, <span class="string">"install"</span> ]</span></span><br><span class="line"><span class="keyword">ONBUILD</span> <span class="keyword">COPY</span><span class="language-bash"> . /app/</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [ <span class="string">"npm"</span>, <span class="string">"start"</span> ]</span></span><br></pre></td></tr></table></figure>
<p>回到原始的 <code>Dockerfile</code>,这次将项目相关的指令加上 <code>ONBUILD</code>,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 <code>Dockerfile</code> 就变成:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> my-node</span><br></pre></td></tr></table></figure>
<p>当在各个项目目录中,用这个只有一行的 <code>Dockerfile</code> 构建镜像时,之前基础镜像的那三行 <code>ONBUILD</code> 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 <code>npm install</code>,生成应用镜像。</p>
<h3 id="LABEL为镜像添加元数据"><a href="#LABEL为镜像添加元数据" class="headerlink" title="LABEL为镜像添加元数据"></a><code>LABEL</code>为镜像添加元数据</h3><p><code>LABEL</code> 指令用来给镜像以键值对的形式添加一些元数据(metadata),格式如下:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">LABEL</span><span class="language-bash"> <key>=<value> <key>=<value> <key>=<value> ...</span></span><br></pre></td></tr></table></figure>
<p>可以用一些标签来申明镜像的作者、文档地址等:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">LABEL</span><span class="language-bash"> org.opencontainers.image.authors=<span class="string">"yeasy"</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">LABEL</span><span class="language-bash"> org.opencontainers.image.documentation=<span class="string">"https://yeasy.gitbooks.io"</span></span></span><br></pre></td></tr></table></figure>
<p>具体可以参考 <a target="_blank" rel="noopener" href="https://github.com/opencontainers/image-spec/blob/master/annotations.md">https://github.com/opencontainers/image-spec/blob/master/annotations.md</a></p>
<h3 id="SHELL指令"><a href="#SHELL指令" class="headerlink" title="SHELL指令"></a><code>SHELL</code>指令</h3><p>格式:<code>SHELL ["executable", "parameters"]</code></p>
<p><code>SHELL</code> 指令可以指定 <code>RUN</code> <code>ENTRYPOINT</code> <code>CMD</code> 指令的 shell,Linux 中默认为 `[“/bin/sh”, “-c”]</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHELL</span><span class="language-bash"> [<span class="string">"/bin/sh"</span>, <span class="string">"-c"</span>]</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> lll ; <span class="built_in">ls</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">SHELL</span><span class="language-bash"> [<span class="string">"/bin/sh"</span>, <span class="string">"-cex"</span>]</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> lll ; <span class="built_in">ls</span></span></span><br></pre></td></tr></table></figure>
<p>两个 <code>RUN</code> 运行同一命令,第二个 <code>RUN</code> 运行的命令会打印出每条命令并当遇到错误时退出。</p>
<p>当 <code>ENTRYPOINT</code> <code>CMD</code> 以 shell 格式指定时,<code>SHELL</code> 指令所指定的 shell 也会成为这两个指令的 shell</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHELL</span><span class="language-bash"> [<span class="string">"/bin/sh"</span>, <span class="string">"-cex"</span>]</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># /bin/sh -cex "nginx"</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> nginx</span></span><br></pre></td></tr></table></figure>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHELL</span><span class="language-bash"> [<span class="string">"/bin/sh"</span>, <span class="string">"-cex"</span>]</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># /bin/sh -cex "nginx"</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> nginx</span></span><br></pre></td></tr></table></figure>
<h2 id="Dockerfile构建镜像"><a href="#Dockerfile构建镜像" class="headerlink" title="Dockerfile构建镜像"></a>Dockerfile构建镜像</h2><p>以定制nginx镜像为例(构建好的镜像内会有一个 /usr/share/nginx/html/index.html 文件)</p>
<ul>
<li><p>在一个空白目录中,建立一个文本文件,并命名为 <code>Dockerfile</code>:</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mkdir mynginx</span><br><span class="line">cd mynginx</span><br><span class="line">touch Dockerfile</span><br></pre></td></tr></table></figure></li>
<li><p>其内容为:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> nginx</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">echo</span> <span class="string">'<h1>Hello, Docker!</h1>'</span> > /usr/share/nginx/html/index.html</span></span><br></pre></td></tr></table></figure></li>
<li><p>在 <code>Dockerfile</code> 文件所在目录执行</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">docker build -t nginx:v3 .</span><br><span class="line">Sending build context to Docker daemon 2.048 kB</span><br><span class="line">Step 1 : FROM nginx</span><br><span class="line"> ---> e43d811ce2f4</span><br><span class="line">Step 2 : RUN <span class="built_in">echo</span> <span class="string">'<h1>Hello, Docker!</h1>'</span> > /usr/share/nginx/html/index.html</span><br><span class="line"> ---> Running <span class="keyword">in</span> 9cdc27646c7b</span><br><span class="line"> ---> 44aa4490ce2c</span><br><span class="line">Removing intermediate container 9cdc27646c7b</span><br><span class="line">Successfully built 44aa4490ce2c</span><br></pre></td></tr></table></figure>
<p> 从命令的输出结果中,可以清晰的看到镜像的构建过程。在 <code>Step 2</code> 中,<code>RUN</code> 指令启动了一个容器 <code>9cdc27646c7b</code>,执行了所要求的命令,并最后提交了这一层 <code>44aa4490ce2c</code>,随后删除了所用到的这个容器 <code>9cdc27646c7b</code>。</p>
</li>
<li><p><code>docker build</code> 命令进行镜像构建的格式为</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build [选项] <上下文路径/URL/-></span><br></pre></td></tr></table></figure></li>
</ul>
<h3 id="镜像构建上下文(Context)"><a href="#镜像构建上下文(Context)" class="headerlink" title="镜像构建上下文(Context)"></a>镜像构建上下文(Context)</h3><p><code>docker build</code> 命令最后有一个 <code>.</code>。<code>.</code> 表示当前目录,而 <code>Dockerfile</code> 就在当前目录,因此不少初学者以为这个路径是在指定 <code>Dockerfile</code> 所在路径,这么理解其实是不准确的。对应上面的命令格式,这是在指定 <strong>上下文路径</strong>。</p>
<p>要理解上下文路径,首先要理解 <code>docker build</code> 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 <a target="_blank" rel="noopener" href="https://docs.docker.com/develop/sdk/">Docker Remote API</a>,而如 <code>docker</code> 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上好像是在本机执行各种 <code>docker</code> 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,使得操作远程服务器的 Docker 引擎变得轻而易举。</p>
<p>进行镜像构建时,并非所有定制都会通过 <code>RUN</code> 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 <code>COPY</code> 指令、<code>ADD</code> 指令等。而 <code>docker build</code> 命令构建镜像,其实并非在本地构建,而是在服务端即 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?</p>
<p>这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,<code>docker build</code> 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。</p>
<p>如果在 <code>Dockerfile</code> 中使用如下命令:</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./package.json /app/</span></span><br></pre></td></tr></table></figure>
<p>并不是要复制执行 <code>docker build</code> 命令所在的目录下的 <code>package.json</code>,也不是复制 <code>Dockerfile</code> 所在目录下的 <code>package.json</code>,而是复制 <strong>上下文(context)</strong> 目录下的 <code>package.json</code>。</p>
<p>因此,<code>COPY</code> 这类指令中的源文件的路径都是<em>相对路径</em>。这也是 <code>COPY ../package.json /app</code> 或者 <code>COPY /opt/xxxx /app</code> 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。</p>
<p>现在就可以理解刚才的命令 <code>docker build -t nginx:v3 .</code> 中的这个 <code>.</code>,实际上是在指定上下文的目录,<code>docker build</code> 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。</p>
<p>如果观察 <code>docker build</code> 输出,其实已经看到了这个发送上下文的过程:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker build -t nginx:v3 .</span><br><span class="line">Sending build context to Docker daemon 2.048 kB</span><br><span class="line">...</span><br></pre></td></tr></table></figure>
<p>理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 <code>COPY /opt/xxxx /app</code> 不工作后,于是干脆将 <code>Dockerfile</code> 放到了硬盘根目录去构建,结果发现 <code>docker build</code> 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 <code>docker build</code> 打包整个硬盘,这显然是使用错误。</p>
<p>一般来说,应该会将 <code>Dockerfile</code> 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 <code>.gitignore</code> 一样的语法写一个 <code>.dockerignore</code>,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。</p>
<p>在默认情况下,如果不额外指定 <code>Dockerfile</code> 的话,会将上下文目录下的名为 <code>Dockerfile</code> 的文件作为 Dockerfile。</p>
<p>实际上 <code>Dockerfile</code> 的文件名并不要求必须为 <code>Dockerfile</code>,而且并不要求必须位于上下文目录中,比如可以用 <code>-f ../Dockerfile.php</code> 参数指定某个文件作为 <code>Dockerfile</code>。</p>
<h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>[1] <a target="_blank" rel="noopener" href="https://docs.docker.com/engine/reference/builder/">https://docs.docker.com/engine/reference/builder/</a></p>
<p>[2] <a target="_blank" rel="noopener" href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/">https://docs.docker.com/develop/develop-images/dockerfile_best-practices/</a></p>
<p>[3] <a target="_blank" rel="noopener" href="https://yeasy.gitbook.io/docker_practice/image/dockerfile">https://yeasy.gitbook.io/docker_practice/image/dockerfile</a></p>
</div>
<div>
<ul class="post-copyright">
<li class="post-copyright-author">
<strong>本文作者: </strong>李钰璕
</li>
<li class="post-copyright-link">
<strong>本文链接:</strong>
<a href="https://leeyuxun.github.io/Dockerfile%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.html" title="Dockerfile学习总结">https://leeyuxun.github.io/Dockerfile学习总结.html</a>
</li>
<li class="post-copyright-license">
<strong>版权声明: </strong>本博客所有文章除特别声明外,均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/zh-cn" rel="noopener" target="_blank"><i class="fab fa-fw fa-creative-commons"></i>BY-NC-SA</a> 许可协议。转载请注明出处!
</li>
</ul>
</div>
<footer class="post-footer">
<div class="post-tags">
<a href="/tags/docker/" rel="tag"><i class="fa fa-tag"></i> docker</a>
<a href="/tags/Dockerfile/" rel="tag"><i class="fa fa-tag"></i> Dockerfile</a>
</div>
<div class="post-nav">
<div class="post-nav-item">
<a href="/Docker%E5%91%BD%E4%BB%A4%E6%B1%87%E6%80%BB.html" rel="prev" title="Docker命令汇总">
<i class="fa fa-chevron-left"></i> Docker命令汇总
</a></div>
<div class="post-nav-item">
<a href="/Docker-compose%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.html" rel="next" title="Docker-compose学习总结">
Docker-compose学习总结 <i class="fa fa-chevron-right"></i>
</a></div>
</div>
</footer>
</article>
</div>
<script>
window.addEventListener('tabs:register', () => {
let { activeClass } = CONFIG.comments;
if (CONFIG.comments.storage) {
activeClass = localStorage.getItem('comments_active') || activeClass;
}
if (activeClass) {
let activeTab = document.querySelector(`a[href="#comment-${activeClass}"]`);
if (activeTab) {
activeTab.click();
}
}
});
if (CONFIG.comments.storage) {
window.addEventListener('tabs:click', event => {
if (!event.target.matches('.tabs-comment .tab-content .tab-pane')) return;
let commentClass = event.target.classList[1];
localStorage.setItem('comments_active', commentClass);
});
}
</script>
</div>
<div class="toggle sidebar-toggle">
<span class="toggle-line toggle-line-first"></span>
<span class="toggle-line toggle-line-middle"></span>
<span class="toggle-line toggle-line-last"></span>
</div>
<aside class="sidebar">
<div class="sidebar-inner">
<ul class="sidebar-nav motion-element">
<li class="sidebar-nav-toc">
文章目录
</li>
<li class="sidebar-nav-overview">
站点概览
</li>
</ul>
<!--noindex-->
<div class="post-toc-wrap sidebar-panel">
<div class="post-toc motion-element"><ol class="nav"><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%89%8D%E8%A8%80"><span class="nav-number">1.</span> <span class="nav-text">前言</span></a></li><li class="nav-item nav-level-2"><a class="nav-link" href="#Dockerfile-%E6%8C%87%E4%BB%A4%E4%BB%8B%E7%BB%8D"><span class="nav-number">2.</span> <span class="nav-text">Dockerfile 指令介绍</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#FROM%E6%8C%87%E5%AE%9A%E5%9F%BA%E7%A1%80%E9%95%9C%E5%83%8F"><span class="nav-number">2.1.</span> <span class="nav-text">FROM指定基础镜像</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#RUN%E6%89%A7%E8%A1%8C%E5%91%BD%E4%BB%A4"><span class="nav-number">2.2.</span> <span class="nav-text">RUN执行命令</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#COPY%E5%A4%8D%E5%88%B6%E6%96%87%E4%BB%B6"><span class="nav-number">2.3.</span> <span class="nav-text">COPY复制文件</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#ADD%E6%9B%B4%E9%AB%98%E7%BA%A7%E7%9A%84%E5%A4%8D%E5%88%B6%E6%96%87%E4%BB%B6"><span class="nav-number">2.4.</span> <span class="nav-text">ADD更高级的复制文件</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#CMD%E5%AE%B9%E5%99%A8%E5%90%AF%E5%8A%A8%E5%91%BD%E4%BB%A4"><span class="nav-number">2.5.</span> <span class="nav-text">CMD容器启动命令</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#ENTRYPOINT%E5%85%A5%E5%8F%A3%E7%82%B9"><span class="nav-number">2.6.</span> <span class="nav-text">ENTRYPOINT入口点</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#ENV%E8%AE%BE%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F"><span class="nav-number">2.7.</span> <span class="nav-text">ENV设置环境变量</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#ARG%E6%9E%84%E5%BB%BA%E5%8F%82%E6%95%B0"><span class="nav-number">2.8.</span> <span class="nav-text">ARG构建参数</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#VOLUME%E5%AE%9A%E4%B9%89%E5%8C%BF%E5%90%8D%E5%8D%B7"><span class="nav-number">2.9.</span> <span class="nav-text">VOLUME定义匿名卷</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#EXPOSE%E6%9A%B4%E9%9C%B2%E7%AB%AF%E5%8F%A3"><span class="nav-number">2.10.</span> <span class="nav-text">EXPOSE暴露端口</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#WORKDIR%E6%8C%87%E5%AE%9A%E5%B7%A5%E4%BD%9C%E7%9B%AE%E5%BD%95"><span class="nav-number">2.11.</span> <span class="nav-text">WORKDIR指定工作目录</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#USER%E6%8C%87%E5%AE%9A%E5%BD%93%E5%89%8D%E7%94%A8%E6%88%B7"><span class="nav-number">2.12.</span> <span class="nav-text">USER指定当前用户</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#HEALTHCHECK%E5%81%A5%E5%BA%B7%E6%A3%80%E6%9F%A5"><span class="nav-number">2.13.</span> <span class="nav-text">HEALTHCHECK健康检查</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#ONBUILD%E5%BB%B6%E8%BF%9F%E6%9E%84%E5%BB%BA%E5%91%BD%E4%BB%A4"><span class="nav-number">2.14.</span> <span class="nav-text">ONBUILD延迟构建命令</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#LABEL%E4%B8%BA%E9%95%9C%E5%83%8F%E6%B7%BB%E5%8A%A0%E5%85%83%E6%95%B0%E6%8D%AE"><span class="nav-number">2.15.</span> <span class="nav-text">LABEL为镜像添加元数据</span></a></li><li class="nav-item nav-level-3"><a class="nav-link" href="#SHELL%E6%8C%87%E4%BB%A4"><span class="nav-number">2.16.</span> <span class="nav-text">SHELL指令</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#Dockerfile%E6%9E%84%E5%BB%BA%E9%95%9C%E5%83%8F"><span class="nav-number">3.</span> <span class="nav-text">Dockerfile构建镜像</span></a><ol class="nav-child"><li class="nav-item nav-level-3"><a class="nav-link" href="#%E9%95%9C%E5%83%8F%E6%9E%84%E5%BB%BA%E4%B8%8A%E4%B8%8B%E6%96%87%EF%BC%88Context%EF%BC%89"><span class="nav-number">3.1.</span> <span class="nav-text">镜像构建上下文(Context)</span></a></li></ol></li><li class="nav-item nav-level-2"><a class="nav-link" href="#%E5%8F%82%E8%80%83"><span class="nav-number">4.</span> <span class="nav-text">参考</span></a></li></ol></div>
</div>
<!--/noindex-->
<div class="site-overview-wrap sidebar-panel">
<div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
<img class="site-author-image" itemprop="image" alt="李钰璕"
src="/images/avatar.png">
<p class="site-author-name" itemprop="name">李钰璕</p>
<div class="site-description" itemprop="description">安全学习笔记</div>
</div>
<div class="site-state-wrap motion-element">
<nav class="site-state">
<div class="site-state-item site-state-posts">
<a href="/archives/">
<span class="site-state-item-count">89</span>
<span class="site-state-item-name">日志</span>
</a>
</div>
<div class="site-state-item site-state-categories">
<a href="/categories/">
<span class="site-state-item-count">17</span>
<span class="site-state-item-name">分类</span></a>
</div>
<div class="site-state-item site-state-tags">
<a href="/tags/">
<span class="site-state-item-count">115</span>
<span class="site-state-item-name">标签</span></a>
</div>
</nav>
</div>
<div class="links-of-author motion-element">
<span class="links-of-author-item">
<a href="https://github.com/Leeyuxun" title="GitHub → https://github.com/Leeyuxun" rel="noopener" target="_blank"><i class="fab fa-github fa-fw"></i></a>
</span>
<span class="links-of-author-item">
<a href="mailto:leeyuxun@163.com" title="E-Mail → mailto:leeyuxun@163.com" rel="noopener" target="_blank"><i class="fa fa-envelope fa-fw"></i></a>
</span>
</div>
</div>
<div class="back-to-top motion-element">
<i class="fa fa-arrow-up"></i>
<span>0%</span>
</div>
</div>
</aside>
<div id="sidebar-dimmer"></div>
</div>
</main>
<footer class="footer">
<div class="footer-inner">
<!--
<div class="copyright">
©
<span itemprop="copyrightYear">2023</span>
<span class="with-love">
<i class="fa fa-heart"></i>
</span>
<span class="author" itemprop="copyrightHolder">李钰璕</span>
</div>
-->
<div class="busuanzi-count">
<script async src="https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
</div>
</div>
</footer>
</div>
<script src="/lib/anime.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js"></script>
<script src="//cdn.jsdelivr.net/gh/fancyapps/fancybox@3/dist/jquery.fancybox.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/pangu@4/dist/browser/pangu.min.js"></script>
<script src="/lib/velocity/velocity.min.js"></script>
<script src="/lib/velocity/velocity.ui.min.js"></script>
<script src="/js/utils.js"></script>
<script src="/js/motion.js"></script>
<script src="/js/schemes/pisces.js"></script>
<script src="/js/next-boot.js"></script>
<script src="/js/local-search.js"></script>
<script>
if (typeof MathJax === 'undefined') {
window.MathJax = {
loader: {
load: ['[tex]/mhchem'],
source: {
'[tex]/amsCd': '[tex]/amscd',
'[tex]/AMScd': '[tex]/amscd'
}
},
tex: {
inlineMath: {'[+]': [['$', '$']]},
packages: {'[+]': ['mhchem']},
tags: 'ams'
},
options: {
renderActions: {
findScript: [10, doc => {
document.querySelectorAll('script[type^="math/tex"]').forEach(node => {
const display = !!node.type.match(/; *mode=display/);
const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
const text = document.createTextNode('');
node.parentNode.replaceChild(text, node);
math.start = {node: text, delim: '', n: 0};
math.end = {node: text, delim: '', n: 0};
doc.math.push(math);
});
}, '', false],
insertedScript: [200, () => {
document.querySelectorAll('mjx-container').forEach(node => {
let target = node.parentNode;
if (target.nodeName.toLowerCase() === 'li') {
target.parentNode.classList.add('has-jax');
}
});
}, '', false]
}
}
};
(function () {
var script = document.createElement('script');
script.src = 'true';
script.defer = true;
document.head.appendChild(script);
})();
} else {
MathJax.startup.document.state(0);
MathJax.texReset();
MathJax.typeset();
}
</script>
</body>
</html>