summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/css/dark-mode.css302
-rw-r--r--docs/css/styles.css53
-rw-r--r--docs/fonts/hinted-Geomanist-Book.ttfbin0 -> 73568 bytes
-rw-r--r--docs/images/docker-labs-logo.svg19
-rw-r--r--docs/images/pwd-badge.pngbin0 -> 36167 bytes
-rw-r--r--docs/index.md2
-rw-r--r--docs/tutorial/image-building-best-practices/hvs.pngbin0 -> 127965 bytes
-rw-r--r--docs/tutorial/image-building-best-practices/index.md270
-rw-r--r--docs/tutorial/index.md72
-rw-r--r--docs/tutorial/multi-container-apps/dashboard-multi-container-app.pngbin0 -> 124358 bytes
-rw-r--r--docs/tutorial/multi-container-apps/index.md290
-rw-r--r--docs/tutorial/multi-container-apps/multi-app-architecture.pngbin0 -> 4990 bytes
-rw-r--r--docs/tutorial/our-application/dashboard-two-containers.pngbin0 -> 115788 bytes
-rw-r--r--docs/tutorial/our-application/ide-screenshot.pngbin0 -> 166213 bytes
-rw-r--r--docs/tutorial/our-application/index.md114
-rw-r--r--docs/tutorial/our-application/todo-list-empty.pngbin0 -> 23360 bytes
-rw-r--r--docs/tutorial/our-application/todo-list-sample.pngbin0 -> 70475 bytes
-rw-r--r--docs/tutorial/persisting-our-data/dashboard-open-cli-ubuntu.pngbin0 -> 170038 bytes
-rw-r--r--docs/tutorial/persisting-our-data/index.md161
-rw-r--r--docs/tutorial/persisting-our-data/items-added.pngbin0 -> 63754 bytes
-rw-r--r--docs/tutorial/sharing-our-app/index.md92
-rw-r--r--docs/tutorial/sharing-our-app/push-command.pngbin0 -> 39595 bytes
-rw-r--r--docs/tutorial/sharing-our-app/pwd-add-new-instance.pngbin0 -> 171310 bytes
-rw-r--r--docs/tutorial/tutorial-in-dashboard.pngbin0 -> 109800 bytes
-rw-r--r--docs/tutorial/updating-our-app/dashboard-removing-container.pngbin0 -> 120206 bytes
-rw-r--r--docs/tutorial/updating-our-app/index.md114
-rw-r--r--docs/tutorial/updating-our-app/todo-list-updated-empty-text.pngbin0 -> 25368 bytes
-rw-r--r--docs/tutorial/using-bind-mounts/index.md127
-rw-r--r--docs/tutorial/using-bind-mounts/updated-add-button.pngbin0 -> 21838 bytes
-rw-r--r--docs/tutorial/using-docker-compose/dashboard-app-project-collapsed.pngbin0 -> 107349 bytes
-rw-r--r--docs/tutorial/using-docker-compose/dashboard-app-project-expanded.pngbin0 -> 128949 bytes
-rw-r--r--docs/tutorial/using-docker-compose/index.md359
-rw-r--r--docs/tutorial/what-next/index.md26
33 files changed, 2001 insertions, 0 deletions
diff --git a/docs/css/dark-mode.css b/docs/css/dark-mode.css
new file mode 100644
index 0000000..bee053d
--- /dev/null
+++ b/docs/css/dark-mode.css
@@ -0,0 +1,302 @@
+@media (prefers-color-scheme: dark) {
+ .md-main {
+ color: rgba(255, 255, 255, 0.75) !important;
+ background-color: #36393e !important;
+ }
+
+ article img {
+ box-shadow: 0 0 1em #000;
+ }
+
+ .md-main h1 {
+ color: rgba(255, 255, 255, 0.8) !important;
+ }
+ blockquote {
+ color: rgba(255, 255, 255, 0.75) !important;
+ }
+ table {
+ background-color: #616161 !important;
+ }
+ tbody {
+ background-color: #484848 !important;
+ }
+ .md-sidebar__scrollwrap::-webkit-scrollbar-thumb {
+ background-color: #e0e0e0 !important;
+ }
+ .md-nav {
+ color: rgba(255, 255, 255, 0.8) !important;
+ background-color: #36393e !important;
+ }
+ html .md-nav--primary .md-nav__title:before {
+ color: #fafafa !important;
+ }
+ .md-nav__title {
+ color: rgba(255, 255, 255, 0.9) !important;
+ background-color: #36393e !important;
+ }
+ .md-nav--primary .md-nav__link:after {
+ color: #fafafa !important;
+ }
+ .md-nav__list {
+ color: rgba(255, 255, 255, 0.8) !important;
+ background-color: #36393e !important;
+ }
+ .md-nav__item {
+ color: rgba(255, 255, 255, 0.7) !important;
+ background-color: #36393e !important;
+ }
+ .md-search__scrollwrap::-webkit-scrollbar-thumb {
+ background-color: #e0e0e0 !important;
+ }
+ .md-search__scrollwrap {
+ background-color: #44484e !important;
+ }
+ .md-search-result__article--document:before {
+ color: #eee !important;
+ }
+ .md-search-result__list {
+ color: #eee !important;
+ background-color: #36393e !important;
+ }
+ .md-search-result__meta {
+ background-color: #eee !important;
+ }
+ .md-search-result__teaser {
+ color: #bdbdbd !important;
+ }
+ .md-typeset code {
+ color: white !important;
+/* box-shadow: 0.29412em 0 0 hsla(0, 0%, 100%, 0.07),
+ -0.29412em 0 0 hsla(0, 0%, 100%, 0.1);*/
+ }
+ .md-typeset a code {
+ color: #94acff !important;
+ }
+ .md-typeset a:hover code {
+ text-decoration: underline;
+ }
+ .linenos {
+ color: #f5f5f5 !important;
+ background-color: #313131 !important;
+ }
+ .codehilite {
+ background-color: #44484e !important;
+ }
+ .md-typeset .codehilite::-webkit-scrollbar {
+ height: 1rem !important;
+ }
+ .codehilite pre {
+ color: #fafafa !important;
+ background-color: transparent !important;
+ }
+ .codehilite .hll {
+ background-color: #272822 !important;
+ }
+ .codehilite .c {
+ color: #8a8f98 !important;
+ }
+ .codehilite .err {
+ color: #960050 !important;
+ background-color: #1e0010 !important;
+ }
+ .codehilite .k {
+ color: #66d9ef !important;
+ }
+ .codehilite .l {
+ color: #ae81ff !important;
+ }
+ .codehilite .n {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .o {
+ color: #f92672 !important;
+ }
+ .codehilite .p {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .cm {
+ color: #8a8f98 !important;
+ }
+ .codehilite .cp {
+ color: #8a8f98 !important;
+ }
+ .codehilite .c1 {
+ color: #8a8f98 !important;
+ }
+ .codehilite .cs {
+ color: #8a8f98 !important;
+ }
+ .codehilite .ge {
+ font-style: italic !important;
+ }
+ .codehilite .gs {
+ font-weight: bold !important;
+ }
+ .codehilite .kc {
+ color: #66d9ef !important;
+ }
+ .codehilite .kd {
+ color: #66d9ef !important;
+ }
+ .codehilite .kn {
+ color: #f92672 !important;
+ }
+ .codehilite .kp {
+ color: #66d9ef !important;
+ }
+ .codehilite .kr {
+ color: #66d9ef !important;
+ }
+ .codehilite .kt {
+ color: #66d9ef !important;
+ }
+ .codehilite .ld {
+ color: #e6db74 !important;
+ }
+ .codehilite .m {
+ color: #ae81ff !important;
+ }
+ .codehilite .s {
+ color: #e6db74 !important;
+ }
+ .codehilite .na {
+ color: #a6e22e !important;
+ }
+ .codehilite .nb {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .nc {
+ color: #a6e22e !important;
+ }
+ .codehilite .no {
+ color: #66d9ef !important;
+ }
+ .codehilite .nd {
+ color: #a6e22e !important;
+ }
+ .codehilite .ni {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .ne {
+ color: #a6e22e !important;
+ }
+ .codehilite .nf {
+ color: #a6e22e !important;
+ }
+ .codehilite .nl {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .nn {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .nx {
+ color: #a6e22e !important;
+ }
+ .codehilite .py {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .nt {
+ color: #f92672 !important;
+ }
+ .codehilite .nv {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .ow {
+ color: #f92672 !important;
+ }
+ .codehilite .w {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .mf {
+ color: #ae81ff !important;
+ }
+ .codehilite .mh {
+ color: #ae81ff !important;
+ }
+ .codehilite .mi {
+ color: #ae81ff !important;
+ }
+ .codehilite .mo {
+ color: #ae81ff !important;
+ }
+ .codehilite .sb {
+ color: #e6db74 !important;
+ }
+ .codehilite .sc {
+ color: #e6db74 !important;
+ }
+ .codehilite .sd {
+ color: #e6db74 !important;
+ }
+ .codehilite .s2 {
+ color: #e6db74 !important;
+ }
+ .codehilite .se {
+ color: #ae81ff !important;
+ }
+ .codehilite .sh {
+ color: #e6db74 !important;
+ }
+ .codehilite .si {
+ color: #e6db74 !important;
+ }
+ .codehilite .sx {
+ color: #e6db74 !important;
+ }
+ .codehilite .sr {
+ color: #e6db74 !important;
+ }
+ .codehilite .s1 {
+ color: #e6db74 !important;
+ }
+ .codehilite .ss {
+ color: #e6db74 !important;
+ }
+ .codehilite .bp {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .vc {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .vg {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .vi {
+ color: #f8f8f2 !important;
+ }
+ .codehilite .il {
+ color: #ae81ff !important;
+ }
+ .codehilite .gu {
+ color: #8a8f98 !important;
+ }
+ .codehilite .gd {
+ color: #9c1042 !important;
+ background-color: #eaa;
+ }
+ .codehilite .gi {
+ color: #364c0a !important;
+ background-color: #91e891;
+ }
+ .md-clipboard:before {
+ color: rgba(255, 255, 255, 0.31);
+ }
+ .codehilite:hover .md-clipboard:before, .md-typeset .highlight:hover .md-clipboard:before, pre:hover .md-clipboard:before {
+ color: rgba(255, 255, 255, 0.6);
+ }
+ .md-typeset summary:after {
+ color: rgba(255, 255, 255, 0.26);
+ }
+ .md-typeset .admonition.example > .admonition-title, .md-typeset .admonition.example > summary, .md-typeset details.example > .admonition-title, .md-typeset details.example > summary {
+ background-color: rgba(154, 109, 255, 0.21);
+ }
+ .md-nav__link[data-md-state='blur'] {
+ color: #aec0ff;
+ }
+ .md-typeset .footnote {
+ color: #888484 !important;
+ }
+ .md-typeset .footnote-ref:before {
+ border-color: #888484 !important;
+ }
+}
diff --git a/docs/css/styles.css b/docs/css/styles.css
new file mode 100644
index 0000000..b44b61f
--- /dev/null
+++ b/docs/css/styles.css
@@ -0,0 +1,53 @@
+.text-center {
+ text-align: center;
+}
+
+article img {
+ border: 1px solid #eee;
+ box-shadow: 0 0 1em #ccc;
+ margin: 30px auto 10px;
+}
+
+article img.emojione {
+ border: none;
+ box-shadow: none;
+ margin: 0;
+}
+
+.md-footer-nav {
+ background-color: rgba(0,0,0,.57);
+}
+
+
+/* Docker Branding */
+.md-header {
+ background-color: #0d9cec !important;
+}
+
+@font-face{
+ font-family: "Geomanist";
+ src: url("../fonts/hinted-Geomanist-Book.ttf")
+}
+
+body {
+ font-family: "Open Sans", sans-serif;
+ font-size: 15px;
+ font-weight: normal;
+ font-style: normal;
+ font-stretch: normal;
+ line-height: 1.5;
+ letter-spacing: normal;
+ color: #577482;
+}
+
+h1, h2, h3, h4, .md-footer-nav__inner, .md-header-nav__title, footer.md-footer {
+ font-family: Geomanist;
+}
+
+.md-header-nav__title {
+ line-height: 2.9rem;
+}
+
+.md-header-nav__button img {
+ width: 145px;
+} \ No newline at end of file
diff --git a/docs/fonts/hinted-Geomanist-Book.ttf b/docs/fonts/hinted-Geomanist-Book.ttf
new file mode 100644
index 0000000..9f6e84c
--- /dev/null
+++ b/docs/fonts/hinted-Geomanist-Book.ttf
Binary files differ
diff --git a/docs/images/docker-labs-logo.svg b/docs/images/docker-labs-logo.svg
new file mode 100644
index 0000000..f6cfe99
--- /dev/null
+++ b/docs/images/docker-labs-logo.svg
@@ -0,0 +1,19 @@
+<svg width="145" height="26.000000000000004" xmlns="http://www.w3.org/2000/svg" class="dicon " preserveAspectRatio="xMidYMid meet">
+ <g>
+ <title>background</title>
+ <rect fill="none" id="canvas_background" height="402" width="582" y="-1" x="-1"></rect>
+ </g>
+ <g>
+ <title>Layer 1</title>
+ <g id="svg_1">
+ <g id="svg_2" fill-rule="evenodd" fill="#FFF">
+ <path id="svg_3" d="m63.12,14.601c0.292,-0.29 0.633,-0.519 1.023,-0.687c0.389,-0.168 0.806,-0.252 1.25,-0.252c0.402,0 0.773,0.067 1.114,0.202c0.341,0.134 0.667,0.333 0.977,0.595c0.183,0.147 0.39,0.22 0.62,0.22c0.275,0 0.501,-0.091 0.68,-0.275a0.943,0.943 0 0 0 0.27,-0.687a0.932,0.932 0 0 0 -0.329,-0.724c-0.937,-0.83 -2.048,-1.245 -3.332,-1.245c-1.412,0 -2.617,0.5 -3.615,1.502c-0.998,1.002 -1.497,2.211 -1.497,3.628c0,1.417 0.499,2.626 1.497,3.628c0.998,1.001 2.203,1.502 3.615,1.502c1.278,0 2.39,-0.415 3.332,-1.245a0.968,0.968 0 0 0 0.302,-0.706a0.92,0.92 0 0 0 -0.95,-0.953a1.021,1.021 0 0 0 -0.602,0.202c-0.305,0.263 -0.627,0.46 -0.968,0.591c-0.34,0.131 -0.712,0.197 -1.114,0.197c-0.444,0 -0.861,-0.084 -1.25,-0.252a3.199,3.199 0 0 1 -1.963,-2.964a3.194,3.194 0 0 1 0.94,-2.277zm15.771,-2.267a1.055,1.055 0 0 0 -0.205,-0.307a0.893,0.893 0 0 0 -0.301,-0.206a0.951,0.951 0 0 0 -0.374,-0.073a0.926,0.926 0 0 0 -0.512,0.146l-5.46,3.564l0,-7.146a0.93,0.93 0 0 0 -0.278,-0.683a0.913,0.913 0 0 0 -0.67,-0.279a0.924,0.924 0 0 0 -0.68,0.28a0.929,0.929 0 0 0 -0.28,0.682l0,12.735c0,0.262 0.093,0.488 0.28,0.678c0.185,0.189 0.412,0.283 0.68,0.283a0.906,0.906 0 0 0 0.67,-0.283a0.935,0.935 0 0 0 0.279,-0.678l0,-3.308l1.114,-0.733l4.218,4.755a0.88,0.88 0 0 0 0.639,0.247a0.951,0.951 0 0 0 0.374,-0.073a0.902,0.902 0 0 0 0.301,-0.206c0.085,-0.088 0.154,-0.19 0.205,-0.307a0.885,0.885 0 0 0 0.078,-0.367a0.97,0.97 0 0 0 -0.265,-0.668l-3.925,-4.434l3.825,-2.492c0.244,-0.165 0.365,-0.418 0.365,-0.76a0.887,0.887 0 0 0 -0.078,-0.367zm-21.838,5.785a3.255,3.255 0 0 1 -1.702,1.718a3.08,3.08 0 0 1 -1.251,0.257c-0.45,0 -0.87,-0.086 -1.26,-0.257a3.225,3.225 0 0 1 -1.013,-0.691a3.284,3.284 0 0 1 -0.68,-1.022a3.128,3.128 0 0 1 -0.252,-1.246c0,-0.44 0.084,-0.855 0.251,-1.246c0.168,-0.39 0.395,-0.731 0.68,-1.022c0.286,-0.29 0.624,-0.52 1.014,-0.691c0.39,-0.171 0.81,-0.257 1.26,-0.257c0.444,0 0.86,0.086 1.25,0.257a3.257,3.257 0 0 1 1.703,1.717c0.168,0.388 0.251,0.802 0.251,1.242c0,0.44 -0.083,0.854 -0.251,1.241zm0.662,-4.869c-1.01,-1.002 -2.215,-1.502 -3.615,-1.502c-1.412,0 -2.617,0.5 -3.615,1.502c-0.998,1.002 -1.498,2.211 -1.498,3.628c0,1.417 0.5,2.626 1.498,3.628c0.998,1.001 2.203,1.502 3.615,1.502c1.4,0 2.605,-0.5 3.615,-1.502c0.998,-0.99 1.497,-2.199 1.497,-3.628a5.3,5.3 0 0 0 -0.379,-1.97a5.031,5.031 0 0 0 -1.118,-1.658zm41.03,-0.861a1.797,1.797 0 0 0 -0.644,-0.39a3.775,3.775 0 0 0 -0.85,-0.197a7.268,7.268 0 0 0 -0.862,-0.054a4.97,4.97 0 0 0 -1.716,0.293a5.234,5.234 0 0 0 -1.489,0.842l0,-0.183a0.92,0.92 0 0 0 -0.278,-0.673a0.913,0.913 0 0 0 -0.671,-0.28a0.923,0.923 0 0 0 -0.68,0.28a0.92,0.92 0 0 0 -0.279,0.673l0,8.355a0.92,0.92 0 0 0 0.279,0.674c0.185,0.186 0.412,0.28 0.68,0.28a0.914,0.914 0 0 0 0.671,-0.28a0.92,0.92 0 0 0 0.278,-0.674l0,-4.177a3.232,3.232 0 0 1 0.936,-2.277c0.29,-0.29 0.629,-0.519 1.018,-0.687c0.39,-0.168 0.807,-0.252 1.25,-0.252c0.451,0 0.868,0.077 1.252,0.23c0.152,0.067 0.286,0.1 0.401,0.1a0.95,0.95 0 0 0 0.375,-0.073a0.89,0.89 0 0 0 0.3,-0.207c0.086,-0.088 0.154,-0.19 0.206,-0.306a0.913,0.913 0 0 0 0.078,-0.376a0.853,0.853 0 0 0 -0.256,-0.641l0.001,0zm-16.708,3.536c0.097,-0.336 0.247,-0.643 0.448,-0.92c0.2,-0.278 0.438,-0.516 0.711,-0.715c0.274,-0.199 0.576,-0.353 0.904,-0.463a3.175,3.175 0 0 1 2.023,0a3.279,3.279 0 0 1 1.606,1.177c0.204,0.278 0.358,0.585 0.461,0.921l-6.153,0zm6.692,-2.675c-1.01,-1.002 -2.216,-1.502 -3.615,-1.502c-1.412,0 -2.618,0.5 -3.616,1.502c-0.998,1.002 -1.497,2.211 -1.497,3.628c0,1.417 0.5,2.626 1.497,3.628c0.998,1.001 2.204,1.502 3.616,1.502c1.284,0 2.398,-0.415 3.341,-1.245a0.954,0.954 0 0 0 0.274,-0.688a0.927,0.927 0 0 0 -0.27,-0.682a0.918,0.918 0 0 0 -0.68,-0.27a0.995,0.995 0 0 0 -0.63,0.238a3.011,3.011 0 0 1 -0.93,0.55a3.202,3.202 0 0 1 -1.105,0.183c-0.353,0 -0.693,-0.055 -1.018,-0.165a3.28,3.28 0 0 1 -0.895,-0.463a3.197,3.197 0 0 1 -1.164,-1.635l7.23,0a0.94,0.94 0 0 0 0.959,-0.953c0,-0.708 -0.125,-1.367 -0.374,-1.974a4.991,4.991 0 0 0 -1.123,-1.654zm-42.988,4.87a3.245,3.245 0 0 1 -1.703,1.718c-0.389,0.17 -0.806,0.256 -1.25,0.256c-0.45,0 -0.87,-0.086 -1.26,-0.257a3.227,3.227 0 0 1 -1.013,-0.691a3.272,3.272 0 0 1 -0.68,-1.022a3.134,3.134 0 0 1 -0.251,-1.246c0,-0.44 0.083,-0.855 0.25,-1.246c0.168,-0.39 0.395,-0.731 0.68,-1.022c0.287,-0.29 0.624,-0.52 1.014,-0.691c0.39,-0.171 0.81,-0.257 1.26,-0.257c0.444,0 0.861,0.086 1.25,0.257a3.246,3.246 0 0 1 1.703,1.717c0.168,0.388 0.251,0.802 0.251,1.242a3.1,3.1 0 0 1 -0.25,1.241l-0.001,0.001zm1.2,-10.77a0.922,0.922 0 0 0 -0.949,0.953l0,4.571c-0.925,-0.751 -1.993,-1.126 -3.204,-1.126c-1.412,0 -2.617,0.5 -3.615,1.502c-0.999,1.002 -1.497,2.211 -1.497,3.628c0,1.417 0.498,2.626 1.497,3.628c0.998,1.001 2.203,1.502 3.615,1.502c1.4,0 2.605,-0.5 3.615,-1.502c0.999,-0.99 1.498,-2.199 1.498,-3.628l0,-8.575a0.94,0.94 0 0 0 -0.959,-0.953l-0.001,0zm-26.46,4.136l3.74,0l0,-3.378l-3.74,0l0,3.378zm-4.419,0l3.74,0l0,-3.378l-3.74,0l0,3.378zm-4.418,0l3.739,0l0,-3.378l-3.74,0l0,3.378l0.001,0zm-4.42,0l3.74,0l0,-3.378l-3.74,0l0,3.378zm-4.418,0l3.739,0l0,-3.378l-3.739,0l0,3.378zm4.419,-4.054l3.739,0l0,-3.378l-3.74,0l0,3.378l0.001,0zm4.419,0l3.739,0l0,-3.378l-3.74,0l0,3.378l0.001,0zm4.418,0l3.74,0l0,-3.378l-3.74,0l0,3.378zm0,-4.054l3.74,0l0,-3.378l-3.74,0l0,3.378zm15.258,5.668c-0.186,-1.352 -0.944,-2.524 -2.323,-3.584l-0.792,-0.525l-0.53,0.789c-0.675,1.014 -1.015,2.42 -0.903,3.769c0.05,0.474 0.207,1.323 0.698,2.069c-0.49,0.262 -1.456,0.623 -2.739,0.598l-24.591,0l-0.049,0.282c-0.23,1.355 -0.226,5.583 2.537,8.833c2.099,2.47 5.247,3.723 9.356,3.723c8.906,0 15.495,-4.075 18.58,-11.482c1.213,0.024 3.827,0.007 5.17,-2.541c0.034,-0.058 0.115,-0.212 0.349,-0.695l0.129,-0.264l-0.755,-0.501c-0.817,-0.543 -2.693,-0.742 -4.137,-0.471z"></path>
+ <g font-weight="normal" font-size="18" font-family="Geomanist Book ,sans-serif" opacity="0.5" fill-rule="evenodd" fill="none" id="Page-1">
+ <text fill="#FFFFFF" id="labs">
+ <tspan id="svg_4" y="22" x="101">Labs</tspan>
+ </text>
+ </g>
+ </g>
+ </g>
+ </g>
+ </svg> \ No newline at end of file
diff --git a/docs/images/pwd-badge.png b/docs/images/pwd-badge.png
new file mode 100644
index 0000000..278ea96
--- /dev/null
+++ b/docs/images/pwd-badge.png
Binary files differ
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..6d9c155
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,2 @@
+redirect: /tutorial/
+
diff --git a/docs/tutorial/image-building-best-practices/hvs.png b/docs/tutorial/image-building-best-practices/hvs.png
new file mode 100644
index 0000000..bce851b
--- /dev/null
+++ b/docs/tutorial/image-building-best-practices/hvs.png
Binary files differ
diff --git a/docs/tutorial/image-building-best-practices/index.md b/docs/tutorial/image-building-best-practices/index.md
new file mode 100644
index 0000000..49b51ca
--- /dev/null
+++ b/docs/tutorial/image-building-best-practices/index.md
@@ -0,0 +1,270 @@
+## Security Scanning
+
+When you have built an image, it is good practice to scan it for security vulnerabilities using the `docker scan` command.
+Docker has partnered with [Snyk](http://snyk.io) to provide the vulnerability scanning service.
+
+For example, to scan the `getting-started` image you created earlier in the tutorial, you can just type
+
+```bash
+docker scan getting-started
+```
+
+The scan uses a constantly updated database of vulnerabilities, so the output you see will vary as new
+vulnerabilities are discovered, but it might look something like this:
+
+```plaintext
+✗ Low severity vulnerability found in freetype/freetype
+ Description: CVE-2020-15999
+ Info: https://snyk.io/vuln/SNYK-ALPINE310-FREETYPE-1019641
+ Introduced through: freetype/freetype@2.10.0-r0, gd/libgd@2.2.5-r2
+ From: freetype/freetype@2.10.0-r0
+ From: gd/libgd@2.2.5-r2 > freetype/freetype@2.10.0-r0
+ Fixed in: 2.10.0-r1
+
+✗ Medium severity vulnerability found in libxml2/libxml2
+ Description: Out-of-bounds Read
+ Info: https://snyk.io/vuln/SNYK-ALPINE310-LIBXML2-674791
+ Introduced through: libxml2/libxml2@2.9.9-r3, libxslt/libxslt@1.1.33-r3, nginx-module-xslt/nginx-module-xslt@1.17.9-r1
+ From: libxml2/libxml2@2.9.9-r3
+ From: libxslt/libxslt@1.1.33-r3 > libxml2/libxml2@2.9.9-r3
+ From: nginx-module-xslt/nginx-module-xslt@1.17.9-r1 > libxml2/libxml2@2.9.9-r3
+ Fixed in: 2.9.9-r4
+```
+
+The output lists the type of vulnerability, a URL to learn more, and importantly which version of the relevant library
+fixes the vulnerability.
+
+There are several other options, which you can read about in the [docker scan documentation](https://docs.docker.com/engine/scan/).
+
+As well as scanning your newly built image on the command line, you can also [configure Docker Hub](https://docs.docker.com/docker-hub/vulnerability-scanning/)
+to scan all newly pushed images automatically, and you can then see the results in both Docker Hub and Docker Desktop.
+
+![Hub vulnerability scanning](hvs.png){: style=width:75% }
+{: .text-center }
+
+## Image Layering
+
+Did you know that you can look at what makes up an image? Using the `docker image history`
+command, you can see the command that was used to create each layer within an image.
+
+1. Use the `docker image history` command to see the layers in the `getting-started` image you
+ created earlier in the tutorial.
+
+ ```bash
+ docker image history getting-started
+ ```
+
+ You should get output that looks something like this (dates/IDs may be different).
+
+ ```plaintext
+ IMAGE CREATED CREATED BY SIZE COMMENT
+ a78a40cbf866 18 seconds ago /bin/sh -c #(nop) CMD ["node" "src/index.j… 0B
+ f1d1808565d6 19 seconds ago /bin/sh -c yarn install --production 85.4MB
+ a2c054d14948 36 seconds ago /bin/sh -c #(nop) COPY dir:5dc710ad87c789593… 198kB
+ 9577ae713121 37 seconds ago /bin/sh -c #(nop) WORKDIR /app 0B
+ b95baba1cfdb 13 days ago /bin/sh -c #(nop) CMD ["node"] 0B
+ <missing> 13 days ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entry… 0B
+ <missing> 13 days ago /bin/sh -c #(nop) COPY file:238737301d473041… 116B
+ <missing> 13 days ago /bin/sh -c apk add --no-cache --virtual .bui… 5.35MB
+ <missing> 13 days ago /bin/sh -c #(nop) ENV YARN_VERSION=1.21.1 0B
+ <missing> 13 days ago /bin/sh -c addgroup -g 1000 node && addu… 74.3MB
+ <missing> 13 days ago /bin/sh -c #(nop) ENV NODE_VERSION=12.14.1 0B
+ <missing> 13 days ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
+ <missing> 13 days ago /bin/sh -c #(nop) ADD file:e69d441d729412d24… 5.59MB
+ ```
+
+ Each of the lines represents a layer in the image. The display here shows the base at the bottom with
+ the newest layer at the top. Using this, you can also quickly see the size of each layer, helping
+ diagnose large images.
+
+1. You'll notice that several of the lines are truncated. If you add the `--no-trunc` flag, you'll get the
+ full output (yes... funny how you use a truncated flag to get untruncated output, huh?)
+
+ ```bash
+ docker image history --no-trunc getting-started
+ ```
+
+
+## Layer Caching
+
+Now that you've seen the layering in action, there's an important lesson to learn to help decrease build
+times for your container images.
+
+> Once a layer changes, all downstream layers have to be recreated as well
+
+Let's look at the Dockerfile we were using one more time...
+
+```dockerfile
+FROM node:12-alpine
+WORKDIR /app
+COPY . .
+RUN yarn install --production
+CMD ["node", "src/index.js"]
+```
+
+Going back to the image history output, we see that each command in the Dockerfile becomes a new layer in the image.
+You might remember that when we made a change to the image, the yarn dependencies had to be reinstalled. Is there a
+way to fix this? It doesn't make much sense to ship around the same dependencies every time we build, right?
+
+To fix this, we need to restructure our Dockerfile to help support the caching of the dependencies. For Node-based
+applications, those dependencies are defined in the `package.json` file. So, what if we copied only that file in first,
+install the dependencies, and _then_ copy in everything else? Then, we only recreate the yarn dependencies if there was
+a change to the `package.json`. Make sense?
+
+1. Update the Dockerfile to copy in the `package.json` first, install dependencies, and then copy everything else in.
+
+ ```dockerfile hl_lines="3 4 5"
+ FROM node:12-alpine
+ WORKDIR /app
+ COPY package.json yarn.lock ./
+ RUN yarn install --production
+ COPY . .
+ CMD ["node", "src/index.js"]
+ ```
+
+1. Create a file named `.dockerignore` in the same folder as the Dockerfile with the following contents.
+
+ ```ignore
+ node_modules
+ ```
+
+ `.dockerignore` files are an easy way to selectively copy only image relevant files.
+ You can read more about this
+ [here](https://docs.docker.com/engine/reference/builder/#dockerignore-file).
+ In this case, the `node_modules` folder should be omitted in the second `COPY` step because otherwise,
+ it would possibly overwrite files which were created by the command in the `RUN` step.
+ For further details on why this is recommended for Node.js applications and other best practices,
+ have a look at their guide on
+ [Dockerizing a Node.js web app](https://nodejs.org/en/docs/guides/nodejs-docker-webapp/).
+
+1. Build a new image using `docker build`.
+
+ ```bash
+ docker build -t getting-started .
+ ```
+
+ You should see output like this...
+
+ ```plaintext
+ Sending build context to Docker daemon 219.1kB
+ Step 1/6 : FROM node:12-alpine
+ ---> b0dc3a5e5e9e
+ Step 2/6 : WORKDIR /app
+ ---> Using cache
+ ---> 9577ae713121
+ Step 3/6 : COPY package.json yarn.lock ./
+ ---> bd5306f49fc8
+ Step 4/6 : RUN yarn install --production
+ ---> Running in d53a06c9e4c2
+ yarn install v1.17.3
+ [1/4] Resolving packages...
+ [2/4] Fetching packages...
+ info fsevents@1.2.9: The platform "linux" is incompatible with this module.
+ info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
+ [3/4] Linking dependencies...
+ [4/4] Building fresh packages...
+ Done in 10.89s.
+ Removing intermediate container d53a06c9e4c2
+ ---> 4e68fbc2d704
+ Step 5/6 : COPY . .
+ ---> a239a11f68d8
+ Step 6/6 : CMD ["node", "src/index.js"]
+ ---> Running in 49999f68df8f
+ Removing intermediate container 49999f68df8f
+ ---> e709c03bc597
+ Successfully built e709c03bc597
+ Successfully tagged getting-started:latest
+ ```
+
+ You'll see that all layers were rebuilt. Perfectly fine since we changed the Dockerfile quite a bit.
+
+1. Now, make a change to the `src/static/index.html` file (like change the `<title>` to say "The Awesome Todo App").
+
+1. Build the Docker image now using `docker build -t getting-started .` again. This time, your output should look a little different.
+
+ ```plaintext hl_lines="5 8 11"
+ Sending build context to Docker daemon 219.1kB
+ Step 1/6 : FROM node:12-alpine
+ ---> b0dc3a5e5e9e
+ Step 2/6 : WORKDIR /app
+ ---> Using cache
+ ---> 9577ae713121
+ Step 3/6 : COPY package.json yarn.lock ./
+ ---> Using cache
+ ---> bd5306f49fc8
+ Step 4/6 : RUN yarn install --production
+ ---> Using cache
+ ---> 4e68fbc2d704
+ Step 5/6 : COPY . .
+ ---> cccde25a3d9a
+ Step 6/6 : CMD ["node", "src/index.js"]
+ ---> Running in 2be75662c150
+ Removing intermediate container 2be75662c150
+ ---> 458e5c6f080c
+ Successfully built 458e5c6f080c
+ Successfully tagged getting-started:latest
+ ```
+
+ First off, you should notice that the build was MUCH faster! And, you'll see that steps 1-4 all have
+ `Using cache`. So, hooray! We're using the build cache. Pushing and pulling this image and updates to it
+ will be much faster as well. Hooray!
+
+
+## Multi-Stage Builds
+
+While we're not going to dive into it too much in this tutorial, multi-stage builds are an incredibly powerful
+tool to help use multiple stages to create an image. There are several advantages for them:
+
+- Separate build-time dependencies from runtime dependencies
+- Reduce overall image size by shipping _only_ what your app needs to run
+
+### Maven/Tomcat Example
+
+When building Java-based applications, a JDK is needed to compile the source code to Java bytecode. However,
+that JDK isn't needed in production. Also, you might be using tools like Maven or Gradle to help build the app.
+Those also aren't needed in our final image. Multi-stage builds help.
+
+```dockerfile
+FROM maven AS build
+WORKDIR /app
+COPY . .
+RUN mvn package
+
+FROM tomcat
+COPY --from=build /app/target/file.war /usr/local/tomcat/webapps
+```
+
+In this example, we use one stage (called `build`) to perform the actual Java build using Maven. In the second
+stage (starting at `FROM tomcat`), we copy in files from the `build` stage. The final image is only the last stage
+being created (which can be overridden using the `--target` flag).
+
+
+### React Example
+
+When building React applications, we need a Node environment to compile the JS code (typically JSX), SASS stylesheets,
+and more into static HTML, JS, and CSS. If we aren't doing server-side rendering, we don't even need a Node environment
+for our production build. Why not ship the static resources in a static nginx container?
+
+```dockerfile
+FROM node:12 AS build
+WORKDIR /app
+COPY package* yarn.lock ./
+RUN yarn install
+COPY public ./public
+COPY src ./src
+RUN yarn run build
+
+FROM nginx:alpine
+COPY --from=build /app/build /usr/share/nginx/html
+```
+
+Here, we are using a `node:12` image to perform the build (maximizing layer caching) and then copying the output
+into an nginx container. Cool, huh?
+
+
+## Recap
+
+By understanding a little bit about how images are structured, we can build images faster and ship fewer changes.
+Scanning images gives us confidence that the containers we are running and distributing are secure.
+Multi-stage builds also help us reduce overall image size and increase final container security by separating
+build-time dependencies from runtime dependencies.
diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md
new file mode 100644
index 0000000..863ba6d
--- /dev/null
+++ b/docs/tutorial/index.md
@@ -0,0 +1,72 @@
+---
+next_page: app.md
+---
+
+## The command you just ran
+
+Congratulations! You have started the container for this tutorial!
+Let's first explain the command that you just ran. In case you forgot,
+here's the command:
+
+```cli
+docker run -d -p 80:80 docker/getting-started
+```
+
+You'll notice a few flags being used. Here's some more info on them:
+
+- `-d` - run the container in detached mode (in the background)
+- `-p 80:80` - map port 80 of the host to port 80 in the container
+- `docker/getting-started` - the image to use
+
+!!! info "Pro tip"
+ You can combine single character flags to shorten the full command.
+ As an example, the command above could be written as:
+ ```
+ docker run -dp 80:80 docker/getting-started
+ ```
+
+## The Docker Dashboard
+
+Before going too far, we want to highlight the Docker Dashboard, which gives
+you a quick view of the containers running on your machine. It gives you quick
+access to container logs, lets you get a shell inside the container, and lets you
+easily manage container lifecycle (stop, remove, etc.).
+
+To access the dashboard, follow the instructions in the
+[Docker Desktop manual](https://docs.docker.com/desktop/). If you open the dashboard
+now, you will see this tutorial running! The container name (`jolly_bouman` below) is a
+randomly created name. So, you'll most likely have a different name.
+
+![Tutorial container running in Docker Dashboard](tutorial-in-dashboard.png)
+
+
+## What is a container?
+
+Now that you've run a container, what _is_ a container? Simply put, a container is
+simply another process on your machine that has been isolated from all other processes
+on the host machine. That isolation leverages [kernel namespaces and cgroups](https://medium.com/@saschagrunert/demystifying-containers-part-i-kernel-space-2c53d6979504), features that have been
+in Linux for a long time. Docker has worked to make these capabilities approachable and easy to use.
+
+!!! info "Creating Containers from Scratch"
+ If you'd like to see how containers are built from scratch, Liz Rice from Aqua Security
+ has a fantastic talk in which she creates a container from scratch in Go. While she makes
+ a simple container, this talk doesn't go into networking, using images for the filesystem,
+ and more. But, it gives a _fantastic_ deep dive into how things are working.
+
+ <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/8fi7uSYlOdc" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
+
+## What is a container image?
+
+When running a container, it uses an isolated filesystem. This custom filesystem is provided
+by a **container image**. Since the image contains the container's filesystem, it must contain everything
+needed to run an application - all dependencies, configuration, scripts, binaries, etc. The
+image also contains other configuration for the container, such as environment variables,
+a default command to run, and other metadata.
+
+We'll dive deeper into images later on, covering topics such as layering, best practices, and more.
+
+!!! info
+ If you're familiar with `chroot`, think of a container as an extended version of `chroot`. The
+ filesystem is simply coming from the image. But, a container adds additional isolation not
+ available when simply using chroot.
+
diff --git a/docs/tutorial/multi-container-apps/dashboard-multi-container-app.png b/docs/tutorial/multi-container-apps/dashboard-multi-container-app.png
new file mode 100644
index 0000000..89fba77
--- /dev/null
+++ b/docs/tutorial/multi-container-apps/dashboard-multi-container-app.png
Binary files differ
diff --git a/docs/tutorial/multi-container-apps/index.md b/docs/tutorial/multi-container-apps/index.md
new file mode 100644
index 0000000..0e4026d
--- /dev/null
+++ b/docs/tutorial/multi-container-apps/index.md
@@ -0,0 +1,290 @@
+
+Up to this point, we have been working with single container apps. But, we now want to add MySQL to the
+application stack. The following question often arises - "Where will MySQL run? Install it in the same
+container or run it separately?" In general, **each container should do one thing and do it well.** A few
+reasons:
+
+- There's a good chance you'd have to scale APIs and front-ends differently than databases.
+- Separate containers let you version and update versions in isolation.
+- While you may use a container for the database locally, you may want to use a managed service
+ for the database in production. You don't want to ship your database engine with your app then.
+- Running multiple processes will require a process manager (the container only starts one process),
+ which adds complexity to container startup/shutdown.
+
+And there are more reasons. So, we will update our application to work like this:
+
+![Todo App connected to MySQL container](multi-app-architecture.png)
+{: .text-center }
+
+
+## Container Networking
+
+Remember that containers, by default, run in isolation and don't know anything about other processes
+or containers on the same machine. So, how do we allow one container to talk to another? The answer is
+**networking**. Now, you don't have to be a network engineer (hooray!). Simply remember this rule...
+
+> If two containers are on the same network, they can talk to each other. If they aren't, they can't.
+
+
+## Starting MySQL
+
+There are two ways to put a container on a network: 1) Assign it at start or 2) connect an existing container.
+For now, we will create the network first and attach the MySQL container at startup.
+
+1. Create the network.
+
+ ```bash
+ docker network create todo-app
+ ```
+
+1. Start a MySQL container and attach it to the network. We're also going to define a few environment variables that the
+ database will use to initialize the database (see the "Environment Variables" section in the [MySQL Docker Hub listing](https://hub.docker.com/_/mysql/)).
+
+ ```bash
+ docker run -d \
+ --network todo-app --network-alias mysql \
+ -v todo-mysql-data:/var/lib/mysql \
+ -e MYSQL_ROOT_PASSWORD=secret \
+ -e MYSQL_DATABASE=todos \
+ mysql:5.7
+ ```
+
+ If you are using PowerShell then use this command.
+
+ ```powershell
+ docker run -d `
+ --network todo-app --network-alias mysql `
+ -v todo-mysql-data:/var/lib/mysql `
+ -e MYSQL_ROOT_PASSWORD=secret `
+ -e MYSQL_DATABASE=todos `
+ mysql:5.7
+ ```
+
+ You'll also see we specified the `--network-alias` flag. We'll come back to that in just a moment.
+
+ !!! info "Pro-tip"
+ You'll notice we're using a volume named `todo-mysql-data` here and mounting it at `/var/lib/mysql`, which is
+ where MySQL stores its data. However, we never ran a `docker volume create` command. Docker recognizes we want
+ to use a named volume and creates one automatically for us.
+
+ !!! info "Troubleshooting"
+ If you see a `docker: no matching manifest` error, it's because you're trying to run the container in a different
+ architecture than amd64, which is the only supported architecture for the mysql image at the moment. To solve this
+ add the flag `--platform linux/amd64` in the previous command. So your new command should look like this:
+
+ ```bash
+ docker run -d \
+ --network todo-app --network-alias mysql --platform linux/amd64 \
+ -v todo-mysql-data:/var/lib/mysql \
+ -e MYSQL_ROOT_PASSWORD=secret \
+ -e MYSQL_DATABASE=todos \
+ mysql:5.7
+ ```
+
+1. To confirm we have the database up and running, connect to the database and verify it connects.
+
+ ```bash
+ docker exec -it <mysql-container-id> mysql -p
+ ```
+
+ When the password prompt comes up, type in **secret**. In the MySQL shell, list the databases and verify
+ you see the `todos` database.
+
+ ```cli
+ mysql> SHOW DATABASES;
+ ```
+
+ You should see output that looks like this:
+
+ ```plaintext
+ +--------------------+
+ | Database |
+ +--------------------+
+ | information_schema |
+ | mysql |
+ | performance_schema |
+ | sys |
+ | todos |
+ +--------------------+
+ 5 rows in set (0.00 sec)
+ ```
+
+ Hooray! We have our `todos` database and it's ready for us to use!
+
+ To exit the sql terminal type `exit` in the terminal.
+
+
+## Connecting to MySQL
+
+Now that we know MySQL is up and running, let's use it! But, the question is... how? If we run
+another container on the same network, how do we find the container (remember each container has its own IP
+address)?
+
+To figure it out, we're going to make use of the [nicolaka/netshoot](https://github.com/nicolaka/netshoot) container,
+which ships with a _lot_ of tools that are useful for troubleshooting or debugging networking issues.
+
+1. Start a new container using the nicolaka/netshoot image. Make sure to connect it to the same network.
+
+ ```bash
+ docker run -it --network todo-app nicolaka/netshoot
+ ```
+
+1. Inside the container, we're going to use the `dig` command, which is a useful DNS tool. We're going to look up
+ the IP address for the hostname `mysql`.
+
+ ```bash
+ dig mysql
+ ```
+
+ And you'll get an output like this...
+
+ ```text
+ ; <<>> DiG 9.14.1 <<>> mysql
+ ;; global options: +cmd
+ ;; Got answer:
+ ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32162
+ ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
+
+ ;; QUESTION SECTION:
+ ;mysql. IN A
+
+ ;; ANSWER SECTION:
+ mysql. 600 IN A 172.23.0.2
+
+ ;; Query time: 0 msec
+ ;; SERVER: 127.0.0.11#53(127.0.0.11)
+ ;; WHEN: Tue Oct 01 23:47:24 UTC 2019
+ ;; MSG SIZE rcvd: 44
+ ```
+
+ In the "ANSWER SECTION", you will see an `A` record for `mysql` that resolves to `172.23.0.2`
+ (your IP address will most likely have a different value). While `mysql` isn't normally a valid hostname,
+ Docker was able to resolve it to the IP address of the container that had that network alias (remember the
+ `--network-alias` flag we used earlier?).
+
+ What this means is... our app only simply needs to connect to a host named `mysql` and it'll talk to the
+ database! It doesn't get much simpler than that!
+
+
+## Running our App with MySQL
+
+The todo app supports the setting of a few environment variables to specify MySQL connection settings. They are:
+
+- `MYSQL_HOST` - the hostname for the running MySQL server
+- `MYSQL_USER` - the username to use for the connection
+- `MYSQL_PASSWORD` - the password to use for the connection
+- `MYSQL_DB` - the database to use once connected
+
+!!! warning Setting Connection Settings via Env Vars
+ While using env vars to set connection settings is generally ok for development, it is **HIGHLY DISCOURAGED**
+ when running applications in production. Diogo Monica, the former lead of security at Docker,
+ [wrote a fantastic blog post](https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/)
+ explaining why.
+
+ A more secure mechanism is to use the secret support provided by your container orchestration framework. In most cases,
+ these secrets are mounted as files in the running container. You'll see many apps (including the MySQL image and the todo app)
+ also support env vars with a `_FILE` suffix to point to a file containing the variable.
+
+ As an example, setting the `MYSQL_PASSWORD_FILE` var will cause the app to use the contents of the referenced file
+ as the connection password. Docker doesn't do anything to support these env vars. Your app will need to know to look for
+ the variable and get the file contents.
+
+
+With all of that explained, let's start our dev-ready container!
+
+1. We'll specify each of the environment variables above, as well as connect the container to our app network.
+
+ ```bash hl_lines="3 4 5 6 7"
+ docker run -dp 3000:3000 \
+ -w /app -v "$(pwd):/app" \
+ --network todo-app \
+ -e MYSQL_HOST=mysql \
+ -e MYSQL_USER=root \
+ -e MYSQL_PASSWORD=secret \
+ -e MYSQL_DB=todos \
+ node:12-alpine \
+ sh -c "yarn install && yarn run dev"
+ ```
+
+ If you updated your docker file in the Bind Mount section of the tutorial use the updated command:
+
+ ```bash hl_lines="3 4 5 6 7"
+ docker run -dp 3000:3000 \
+ -w /app -v "$(pwd):/app" \
+ --network todo-app \
+ -e MYSQL_HOST=mysql \
+ -e MYSQL_USER=root \
+ -e MYSQL_PASSWORD=secret \
+ -e MYSQL_DB=todos \
+ node:12-alpine \
+ sh -c "apk --no-cache --virtual build-dependencies add python2 make g++ && yarn install && yarn run dev"
+ ```
+
+ If you are using PowerShell then use this command.
+
+ ```powershell hl_lines="3 4 5 6 7"
+ docker run -dp 3000:3000 `
+ -w /app -v "$(pwd):/app" `
+ --network todo-app `
+ -e MYSQL_HOST=mysql `
+ -e MYSQL_USER=root `
+ -e MYSQL_PASSWORD=secret `
+ -e MYSQL_DB=todos `
+ node:12-alpine `
+ sh -c "yarn install && yarn run dev"
+ ```
+
+1. If we look at the logs for the container (`docker logs <container-id>`), we should see a message indicating it's
+ using the mysql database.
+
+ ```plaintext hl_lines="7"
+ # Previous log messages omitted
+ $ nodemon src/index.js
+ [nodemon] 1.19.2
+ [nodemon] to restart at any time, enter `rs`
+ [nodemon] watching dir(s): *.*
+ [nodemon] starting `node src/index.js`
+ Connected to mysql db at host mysql
+ Listening on port 3000
+ ```
+
+1. Open the app in your browser and add a few items to your todo list.
+
+1. Connect to the mysql database and prove that the items are being written to the database. Remember, the password
+ is **secret**.
+
+ ```bash
+ docker exec -it <mysql-container-id> mysql -p todos
+ ```
+
+ And in the mysql shell, run the following:
+
+ ```plaintext
+ mysql> select * from todo_items;
+ +--------------------------------------+--------------------+-----------+
+ | id | name | completed |
+ +--------------------------------------+--------------------+-----------+
+ | c906ff08-60e6-44e6-8f49-ed56a0853e85 | Do amazing things! | 0 |
+ | 2912a79e-8486-4bc3-a4c5-460793a575ab | Be awesome! | 0 |
+ +--------------------------------------+--------------------+-----------+
+ ```
+
+ Obviously, your table will look different because it has your items. But, you should see them stored there!
+
+If you take a quick look at the Docker Dashboard, you'll see that we have two app containers running. But, there's
+no real indication that they are grouped together in a single app. We'll see how to make that better shortly!
+
+![Docker Dashboard showing two ungrouped app containers](dashboard-multi-container-app.png)
+
+## Recap
+
+At this point, we have an application that now stores its data in an external database running in a separate
+container. We learned a little bit about container networking and saw how service discovery can be performed
+using DNS.
+
+But, there's a good chance you are starting to feel a little overwhelmed with everything you need to do to start up
+this application. We have to create a network, start containers, specify all of the environment variables, expose
+ports, and more! That's a lot to remember and it's certainly making things harder to pass along to someone else.
+
+In the next section, we'll talk about Docker Compose. With Docker Compose, we can share our application stacks in a
+much easier way and let others spin them up with a single (and simple) command!
diff --git a/docs/tutorial/multi-container-apps/multi-app-architecture.png b/docs/tutorial/multi-container-apps/multi-app-architecture.png
new file mode 100644
index 0000000..463cd95
--- /dev/null
+++ b/docs/tutorial/multi-container-apps/multi-app-architecture.png
Binary files differ
diff --git a/docs/tutorial/our-application/dashboard-two-containers.png b/docs/tutorial/our-application/dashboard-two-containers.png
new file mode 100644
index 0000000..14fada7
--- /dev/null
+++ b/docs/tutorial/our-application/dashboard-two-containers.png
Binary files differ
diff --git a/docs/tutorial/our-application/ide-screenshot.png b/docs/tutorial/our-application/ide-screenshot.png
new file mode 100644
index 0000000..6b0468b
--- /dev/null
+++ b/docs/tutorial/our-application/ide-screenshot.png
Binary files differ
diff --git a/docs/tutorial/our-application/index.md b/docs/tutorial/our-application/index.md
new file mode 100644
index 0000000..2814534
--- /dev/null
+++ b/docs/tutorial/our-application/index.md
@@ -0,0 +1,114 @@
+
+For the rest of this tutorial, we will be working with a simple todo
+list manager that is running in Node.js. If you're not familiar with Node.js,
+don't worry! No real JavaScript experience is needed!
+
+At this point, your development team is quite small and you're simply
+building an app to prove out your MVP (minimum viable product). You want
+to show how it works and what it's capable of doing without needing to
+think about how it will work for a large team, multiple developers, etc.
+
+![Todo List Manager Screenshot](todo-list-sample.png){: style="width:50%;" }
+{ .text-center }
+
+## Getting our App
+
+Before we can run the application, we need to get the application source code onto
+our machine. For real projects, you will typically clone the repo. But, for this tutorial,
+we have created a ZIP file containing the application.
+
+1. [Download the ZIP](/assets/app.zip). Open the ZIP file and make sure you extract the
+ contents.
+
+1. Once extracted, use your favorite code editor to open the project. If you're in need of
+ an editor, you can use [Visual Studio Code](https://code.visualstudio.com/). You should
+ see the `package.json` and two subdirectories (`src` and `spec`).
+
+ ![Screenshot of Visual Studio Code opened with the app loaded](ide-screenshot.png){: style="width:650px;margin-top:20px;"}
+ {: .text-center }
+
+## Building the App's Container Image
+
+In order to build the application, we need to use a `Dockerfile`. A
+Dockerfile is simply a text-based script of instructions that is used to
+create a container image. If you've created Dockerfiles before, you might
+see a few flaws in the Dockerfile below. But, don't worry! We'll go over them.
+
+1. Create a file named `Dockerfile` in the same folder as the file `package.json` with the following contents.
+
+ ```dockerfile
+ FROM node:12-alpine
+ # Adding build tools to make yarn install work on Apple silicon / arm64 machines
+ RUN apk add --no-cache python2 g++ make
+ WORKDIR /app
+ COPY . .
+ RUN yarn install --production
+ CMD ["node", "src/index.js"]
+ ```
+
+ Please check that the file `Dockerfile` has no file extension like `.txt`. Some editors may append this file extension automatically and this would result in an error in the next step.
+
+1. If you haven't already done so, open a terminal and go to the `app` directory with the `Dockerfile`. Now build the container image using the `docker build` command.
+
+ ```bash
+ docker build -t getting-started .
+ ```
+
+ This command used the Dockerfile to build a new container image. You might
+ have noticed that a lot of "layers" were downloaded. This is because we instructed
+ the builder that we wanted to start from the `node:12-alpine` image. But, since we
+ didn't have that on our machine, that image needed to be downloaded.
+
+ After the image was downloaded, we copied in our application and used `yarn` to
+ install our application's dependencies. The `CMD` directive specifies the default
+ command to run when starting a container from this image.
+
+ Finally, the `-t` flag tags our image. Think of this simply as a human-readable name
+ for the final image. Since we named the image `getting-started`, we can refer to that
+ image when we run a container.
+
+ The `.` at the end of the `docker build` command tells that Docker should look for the `Dockerfile` in the current directory.
+
+## Starting an App Container
+
+Now that we have an image, let's run the application! To do so, we will use the `docker run`
+command (remember that from earlier?).
+
+1. Start your container using the `docker run` command and specify the name of the image we
+ just created:
+
+ ```bash
+ docker run -dp 3000:3000 getting-started
+ ```
+
+ Remember the `-d` and `-p` flags? We're running the new container in "detached" mode (in the
+ background) and creating a mapping between the host's port 3000 to the container's port 3000.
+ Without the port mapping, we wouldn't be able to access the application.
+
+1. After a few seconds, open your web browser to [http://localhost:3000](http://localhost:3000).
+ You should see our app!
+
+ ![Empty Todo List](todo-list-empty.png){: style="width:450px;margin-top:20px;"}
+ {: .text-center }
+
+1. Go ahead and add an item or two and see that it works as you expect. You can mark items as
+ complete and remove items. Your frontend is successfully storing items in the backend!
+ Pretty quick and easy, huh?
+
+
+At this point, you should have a running todo list manager with a few items, all built by you!
+Now, let's make a few changes and learn about managing our containers.
+
+If you take a quick look at the Docker Dashboard, you should see your two containers running now
+(this tutorial and your freshly launched app container)!
+
+![Docker Dashboard with tutorial and app containers running](dashboard-two-containers.png)
+
+
+## Recap
+
+In this short section, we learned the very basics about building a container image and created a
+Dockerfile to do so. Once we built an image, we started the container and saw the running app!
+
+Next, we're going to make a modification to our app and learn how to update our running application
+with a new image. Along the way, we'll learn a few other useful commands.
diff --git a/docs/tutorial/our-application/todo-list-empty.png b/docs/tutorial/our-application/todo-list-empty.png
new file mode 100644
index 0000000..81fa302
--- /dev/null
+++ b/docs/tutorial/our-application/todo-list-empty.png
Binary files differ
diff --git a/docs/tutorial/our-application/todo-list-sample.png b/docs/tutorial/our-application/todo-list-sample.png
new file mode 100644
index 0000000..681f2ad
--- /dev/null
+++ b/docs/tutorial/our-application/todo-list-sample.png
Binary files differ
diff --git a/docs/tutorial/persisting-our-data/dashboard-open-cli-ubuntu.png b/docs/tutorial/persisting-our-data/dashboard-open-cli-ubuntu.png
new file mode 100644
index 0000000..6ca16d3
--- /dev/null
+++ b/docs/tutorial/persisting-our-data/dashboard-open-cli-ubuntu.png
Binary files differ
diff --git a/docs/tutorial/persisting-our-data/index.md b/docs/tutorial/persisting-our-data/index.md
new file mode 100644
index 0000000..19bdcfa
--- /dev/null
+++ b/docs/tutorial/persisting-our-data/index.md
@@ -0,0 +1,161 @@
+
+In case you didn't notice, our todo list is being wiped clean every single time
+we launch the container. Why is this? Let's dive into how the container is working.
+
+## The Container's Filesystem
+
+When a container runs, it uses the various layers from an image for its filesystem.
+Each container also gets its own "scratch space" to create/update/remove files. Any
+changes won't be seen in another container, _even if_ they are using the same image.
+
+### Seeing this in Practice
+
+To see this in action, we're going to start two containers and create a file in each.
+What you'll see is that the files created in one container aren't available in another.
+
+1. Start a `ubuntu` container that will create a file named `/data.txt` with a random number
+ between 1 and 10000.
+
+ ```bash
+ docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"
+ ```
+
+ In case you're curious about the command, we're starting a bash shell and invoking two
+ commands (why we have the `&&`). The first portion picks a single random number and writes
+ it to `/data.txt`. The second command is simply watching a file to keep the container running.
+
+1. Validate we can see the output by `exec`'ing into the container. To do so, open the Dashboard and click the first action of the container that is running the `ubuntu` image.
+
+ ![Dashboard open CLI into ubuntu container](dashboard-open-cli-ubuntu.png){: style=width:75% }
+{: .text-center }
+
+ You will see a terminal that is running a shell in the ubuntu container. Run the following command to see the content of the `/data.txt` file. Close this terminal afterwards again.
+
+ ```bash
+ cat /data.txt
+ ```
+
+ If you prefer the command line you can use the `docker exec` command to do the same. You need to get the
+ container's ID (use `docker ps` to get it) and get the content with the following command.
+
+ ```bash
+ docker exec <container-id> cat /data.txt
+ ```
+
+ You should see a random number!
+
+1. Now, let's start another `ubuntu` container (the same image) and we'll see we don't have the same
+ file.
+
+ ```bash
+ docker run -it ubuntu ls /
+ ```
+
+ And look! There's no `data.txt` file there! That's because it was written to the scratch space for
+ only the first container.
+
+1. Go ahead and remove the first container using the `docker rm -f <container-id>` command.
+ ```bash
+ docker rm -f <container-id>
+ ```
+
+## Container Volumes
+
+With the previous experiment, we saw that each container starts from the image definition each time it starts.
+While containers can create, update, and delete files, those changes are lost when the container is removed
+and all changes are isolated to that container. With volumes, we can change all of this.
+
+[Volumes](https://docs.docker.com/storage/volumes/) provide the ability to connect specific filesystem paths of
+the container back to the host machine. If a directory in the container is mounted, changes in that
+directory are also seen on the host machine. If we mount that same directory across container restarts, we'd see
+the same files.
+
+There are two main types of volumes. We will eventually use both, but we will start with **named volumes**.
+
+## Persisting our Todo Data
+
+By default, the todo app stores its data in a [SQLite Database](https://www.sqlite.org/index.html) at
+`/etc/todos/todo.db`. If you're not familiar with SQLite, no worries! It's simply a relational database in
+which all of the data is stored in a single file. While this isn't the best for large-scale applications,
+it works for small demos. We'll talk about switching this to a different database engine later.
+
+With the database being a single file, if we can persist that file on the host and make it available to the
+next container, it should be able to pick up where the last one left off. By creating a volume and attaching
+(often called "mounting") it to the directory the data is stored in, we can persist the data. As our container
+writes to the `todo.db` file, it will be persisted to the host in the volume.
+
+As mentioned, we are going to use a **named volume**. Think of a named volume as simply a bucket of data.
+Docker maintains the physical location on the disk and you only need to remember the name of the volume.
+Every time you use the volume, Docker will make sure the correct data is provided.
+
+1. Create a volume by using the `docker volume create` command.
+
+ ```bash
+ docker volume create todo-db
+ ```
+
+1. Stop the todo app container once again in the Dashboard (or with `docker rm -f <container-id>`), as it is still running without using the persistent volume.
+
+1. Start the todo app container, but add the `-v` flag to specify a volume mount. We will use the named volume and mount
+ it to `/etc/todos`, which will capture all files created at the path.
+
+ ```bash
+ docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
+ ```
+
+1. Once the container starts up, open the app and add a few items to your todo list.
+
+ ![Items added to todo list](items-added.png){: style="width: 55%; " }
+ {: .text-center }
+
+1. Remove the container for the todo app. Use the Dashboard or `docker ps` to get the ID and then `docker rm -f <container-id>` to remove it.
+
+1. Start a new container using the same command from above.
+
+1. Open the app. You should see your items still in your list!
+
+1. Go ahead and remove the container when you're done checking out your list.
+
+Hooray! You've now learned how to persist data!
+
+!!! info "Pro-tip"
+ While named volumes and bind mounts (which we'll talk about in a minute) are the two main types of volumes supported
+ by a default Docker engine installation, there are many volume driver plugins available to support NFS, SFTP, NetApp,
+ and more! This will be especially important once you start running containers on multiple hosts in a clustered
+ environment with Swarm, Kubernetes, etc.
+
+## Diving into our Volume
+
+A lot of people frequently ask "Where is Docker _actually_ storing my data when I use a named volume?" If you want to know,
+you can use the `docker volume inspect` command.
+
+```bash
+docker volume inspect todo-db
+[
+ {
+ "CreatedAt": "2019-09-26T02:18:36Z",
+ "Driver": "local",
+ "Labels": {},
+ "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
+ "Name": "todo-db",
+ "Options": {},
+ "Scope": "local"
+ }
+]
+```
+
+The `Mountpoint` is the actual location on the disk where the data is stored. Note that on most machines, you will
+need to have root access to access this directory from the host. But, that's where it is!
+
+!!! info "Accessing Volume data directly on Docker Desktop"
+ While running in Docker Desktop, the Docker commands are actually running inside a small VM on your machine.
+ If you wanted to look at the actual contents of the Mountpoint directory, you would need to first get inside
+ of the VM.
+
+## Recap
+
+At this point, we have a functioning application that can survive restarts! We can show it off to our investors and
+hope they can catch our vision!
+
+However, we saw earlier that rebuilding images for every change takes quite a bit of time. There's got to be a better
+way to make changes, right? With bind mounts (which we hinted at earlier), there is a better way! Let's take a look at that now!
diff --git a/docs/tutorial/persisting-our-data/items-added.png b/docs/tutorial/persisting-our-data/items-added.png
new file mode 100644
index 0000000..9cddfb5
--- /dev/null
+++ b/docs/tutorial/persisting-our-data/items-added.png
Binary files differ
diff --git a/docs/tutorial/sharing-our-app/index.md b/docs/tutorial/sharing-our-app/index.md
new file mode 100644
index 0000000..42a12f9
--- /dev/null
+++ b/docs/tutorial/sharing-our-app/index.md
@@ -0,0 +1,92 @@
+
+Now that we've built an image, let's share it! To share Docker images, you have to use a Docker
+registry. The default registry is Docker Hub and is where all of the images we've used have come from.
+
+## Create a Repo
+
+To push an image, we first need to create a repo on Docker Hub.
+
+1. Go to [Docker Hub](https://hub.docker.com) and log in if you need to.
+
+1. Click the **Create Repository** button.
+
+1. For the repo name, use `getting-started`. Make sure the Visibility is `Public`.
+
+1. Click the **Create** button!
+
+If you look on the right-side of the page, you'll see a section named **Docker commands**. This gives
+an example command that you will need to run to push to this repo.
+
+![Docker command with push example](push-command.png){: style=width:75% }
+{: .text-center }
+
+## Pushing our Image
+
+1. In the command line, try running the push command you see on Docker Hub. Note that your command
+ will be using your namespace, not "docker".
+
+ ```plaintext
+ $ docker push docker/getting-started
+ The push refers to repository [docker.io/docker/getting-started]
+ An image does not exist locally with the tag: docker/getting-started
+ ```
+
+ Why did it fail? The push command was looking for an image named docker/getting-started, but
+ didn't find one. If you run `docker image ls`, you won't see one either.
+
+ To fix this, we need to "tag" our existing image we've built to give it another name.
+
+1. Login to the Docker Hub using the command `docker login -u YOUR-USER-NAME`.
+
+1. Use the `docker tag` command to give the `getting-started` image a new name. Be sure to swap out
+ `YOUR-USER-NAME` with your Docker ID.
+
+ ```bash
+ docker tag getting-started YOUR-USER-NAME/getting-started
+ ```
+
+1. Now try your push command again. If you're copying the value from Docker Hub, you can drop the
+ `tagname` portion, as we didn't add a tag to the image name. If you don't specify a tag, Docker
+ will use a tag called `latest`.
+
+ ```bash
+ docker push YOUR-USER-NAME/getting-started
+ ```
+
+## Running our Image on a New Instance
+
+Now that our image has been built and pushed into a registry, let's try running our app on a brand
+new instance that has never seen this container image! To do this, we will use Play with Docker.
+
+1. Open your browser to [Play with Docker](https://labs.play-with-docker.com/).
+
+1. Log in with your Docker Hub account.
+
+1. Once you're logged in, click on the "+ ADD NEW INSTANCE" link in the left side bar. (If you don't see it, make your browser a little wider.) After a few seconds, a terminal window will be opened in your browser.
+
+ ![Play with Docker add new instance](pwd-add-new-instance.png){: style=width:75% }
+{: .text-center }
+
+
+1. In the terminal, start your freshly pushed app.
+
+ ```bash
+ docker run -dp 3000:3000 YOUR-USER-NAME/getting-started
+ ```
+
+ You should see the image get pulled down and eventually start up!
+
+1. Click on the 3000 badge when it comes up and you should see the app with your modifications! Hooray!
+ If the 3000 badge doesn't show up, you can click on the "Open Port" button and type in 3000.
+
+## Recap
+
+In this section, we learned how to share our images by pushing them to a registry. We then went to a
+brand new instance and were able to run the freshly pushed image. This is quite common in CI pipelines,
+where the pipeline will create the image and push it to a registry and then the production environment
+can use the latest version of the image.
+
+Now that we have that figured out, let's circle back around to what we noticed at the end of the last
+section. As a reminder, we noticed that when we restarted the app, we lost all of our todo list items.
+That's obviously not a great user experience, so let's learn how we can persist the data across
+restarts!
diff --git a/docs/tutorial/sharing-our-app/push-command.png b/docs/tutorial/sharing-our-app/push-command.png
new file mode 100644
index 0000000..0b0a2df
--- /dev/null
+++ b/docs/tutorial/sharing-our-app/push-command.png
Binary files differ
diff --git a/docs/tutorial/sharing-our-app/pwd-add-new-instance.png b/docs/tutorial/sharing-our-app/pwd-add-new-instance.png
new file mode 100644
index 0000000..944e286
--- /dev/null
+++ b/docs/tutorial/sharing-our-app/pwd-add-new-instance.png
Binary files differ
diff --git a/docs/tutorial/tutorial-in-dashboard.png b/docs/tutorial/tutorial-in-dashboard.png
new file mode 100644
index 0000000..002cd05
--- /dev/null
+++ b/docs/tutorial/tutorial-in-dashboard.png
Binary files differ
diff --git a/docs/tutorial/updating-our-app/dashboard-removing-container.png b/docs/tutorial/updating-our-app/dashboard-removing-container.png
new file mode 100644
index 0000000..f9813ec
--- /dev/null
+++ b/docs/tutorial/updating-our-app/dashboard-removing-container.png
Binary files differ
diff --git a/docs/tutorial/updating-our-app/index.md b/docs/tutorial/updating-our-app/index.md
new file mode 100644
index 0000000..92f752e
--- /dev/null
+++ b/docs/tutorial/updating-our-app/index.md
@@ -0,0 +1,114 @@
+
+As a small feature request, we've been asked by the product team to
+change the "empty text" when we don't have any todo list items. They
+would like to transition it to the following:
+
+> You have no todo items yet! Add one above!
+
+Pretty simple, right? Let's make the change.
+
+## Updating our Source Code
+
+1. In the `src/static/js/app.js` file, update line 56 to use the new empty text.
+
+ ```diff
+ - <p className="text-center">No items yet! Add one above!</p>
+ + <p className="text-center">You have no todo items yet! Add one above!</p>
+ ```
+
+1. Let's build our updated version of the image, using the same command we used before.
+
+ ```bash
+ docker build -t getting-started .
+ ```
+
+1. Let's start a new container using the updated code.
+
+ ```bash
+ docker run -dp 3000:3000 getting-started
+ ```
+
+**Uh oh!** You probably saw an error like this (the IDs will be different):
+
+```bash
+docker: Error response from daemon: driver failed programming external connectivity on endpoint laughing_burnell
+(bb242b2ca4d67eba76e79474fb36bb5125708ebdabd7f45c8eaf16caaabde9dd): Bind for 0.0.0.0:3000 failed: port is already allocated.
+```
+
+So, what happened? We aren't able to start the new container because our old container is still
+running. The reason this is a problem is because that container is using the host's port 3000 and
+only one process on the machine (containers included) can listen to a specific port. To fix this,
+we need to remove the old container.
+
+
+## Replacing our Old Container
+
+To remove a container, it first needs to be stopped. Once it has stopped, it can be removed. We have two
+ways that we can remove the old container. Feel free to choose the path that you're most comfortable with.
+
+
+### Removing a container using the CLI
+
+1. Get the ID of the container by using the `docker ps` command.
+
+ ```bash
+ docker ps
+ ```
+
+1. Use the `docker stop` command to stop the container.
+
+ ```bash
+ # Swap out <the-container-id> with the ID from docker ps
+ docker stop <the-container-id>
+ ```
+
+1. Once the container has stopped, you can remove it by using the `docker rm` command.
+
+ ```bash
+ docker rm <the-container-id>
+ ```
+
+!!! info "Pro tip"
+ You can stop and remove a container in a single command by adding the "force" flag
+ to the `docker rm` command. For example: `docker rm -f <the-container-id>`
+
+### Removing a container using the Docker Dashboard
+
+If you open the Docker dashboard, you can remove a container with two clicks! It's certainly
+much easier than having to look up the container ID and remove it.
+
+1. With the dashboard opened, hover over the app container and you'll see a collection of action
+ buttons appear on the right.
+
+1. Click on the trash can icon to delete the container.
+
+1. Confirm the removal and you're done!
+
+![Docker Dashboard - removing a container](dashboard-removing-container.png)
+
+
+### Starting our updated app container
+
+1. Now, start your updated app.
+
+ ```bash
+ docker run -dp 3000:3000 getting-started
+ ```
+
+1. Refresh your browser on [http://localhost:3000](http://localhost:3000) and you should see your updated help text!
+
+![Updated application with updated empty text](todo-list-updated-empty-text.png){: style="width:55%" }
+{: .text-center }
+
+
+
+## Recap
+
+While we were able to build an update, there were two things you might have noticed:
+
+- All of the existing items in our todo list are gone! That's not a very good app! We'll talk about that
+shortly.
+- There were _a lot_ of steps involved for such a small change. In an upcoming section, we'll talk about
+how to see code updates without needing to rebuild and start a new container every time we make a change.
+
+Before talking about persistence, we'll quickly see how to share these images with others.
diff --git a/docs/tutorial/updating-our-app/todo-list-updated-empty-text.png b/docs/tutorial/updating-our-app/todo-list-updated-empty-text.png
new file mode 100644
index 0000000..7017b68
--- /dev/null
+++ b/docs/tutorial/updating-our-app/todo-list-updated-empty-text.png
Binary files differ
diff --git a/docs/tutorial/using-bind-mounts/index.md b/docs/tutorial/using-bind-mounts/index.md
new file mode 100644
index 0000000..c4b61ae
--- /dev/null
+++ b/docs/tutorial/using-bind-mounts/index.md
@@ -0,0 +1,127 @@
+
+In the previous chapter, we talked about and used a **named volume** to persist the data in our database.
+Named volumes are great if we simply want to store data, as we don't have to worry about _where_ the data
+is stored.
+
+With **bind mounts**, we control the exact mountpoint on the host. We can use this to persist data, but is often
+used to provide additional data into containers. When working on an application, we can use a bind mount to
+mount our source code into the container to let it see code changes, respond, and let us see the changes right
+away.
+
+For Node-based applications, [nodemon](https://npmjs.com/package/nodemon) is a great tool to watch for file
+changes and then restart the application. There are equivalent tools in most other languages and frameworks.
+
+## Quick Volume Type Comparisons
+
+Bind mounts and named volumes are the two main types of volumes that come with the Docker engine. However, additional
+volume drivers are available to support other use cases ([SFTP](https://github.com/vieux/docker-volume-sshfs), [Ceph](https://ceph.com/geen-categorie/getting-started-with-the-docker-rbd-volume-plugin/), [NetApp](https://netappdvp.readthedocs.io/en/stable/), [S3](https://github.com/elementar/docker-s3-volume), and more).
+
+| | Named Volumes | Bind Mounts |
+| - | ------------- | ----------- |
+| Host Location | Docker chooses | You control |
+| Mount Example (using `-v`) | my-volume:/usr/local/data | /path/to/data:/usr/local/data |
+| Populates new volume with container contents | Yes | No |
+| Supports Volume Drivers | Yes | No |
+
+
+## Starting a Dev-Mode Container
+
+To run our container to support a development workflow, we will do the following:
+
+- Mount our source code into the container
+- Install all dependencies, including the "dev" dependencies
+- Start nodemon to watch for filesystem changes
+
+So, let's do it!
+
+1. Make sure you don't have any previous `getting-started` containers running.
+
+1. Also make sure you are in app source code directory, i.e. `/path/to/getting-started/app`. If you aren't, you can `cd` into it, .e.g:
+
+ ```bash
+ cd /path/to/getting-started/app
+ ```
+
+1. Now that you are in the `getting-started/app` directory, run the following command. We'll explain what's going on afterwards:
+
+ ```bash
+ docker run -dp 3000:3000 \
+ -w /app -v "$(pwd):/app" \
+ node:12-alpine \
+ sh -c "yarn install && yarn run dev"
+ ```
+
+ If you are using PowerShell then use this command.
+
+ ```powershell
+ docker run -dp 3000:3000 `
+ -w /app -v "$(pwd):/app" `
+ node:12-alpine `
+ sh -c "yarn install && yarn run dev"
+ ```
+
+ If you are using an Apple Silicon Mac or another ARM64 device then use this command.
+
+ ```bash
+ docker run -dp 3000:3000 \
+ -w /app -v "$(pwd):/app" \
+ node:12-alpine \
+ sh -c "apk add --no-cache python2 g++ make && yarn install && yarn run dev"
+ ```
+
+ - `-dp 3000:3000` - same as before. Run in detached (background) mode and create a port mapping
+ - `-w /app` - sets the container's present working directory where the command will run from
+ - `-v "$(pwd):/app"` - bind mount (link) the host's present `getting-started/app` directory to the container's `/app` directory. Note: Docker requires absolute paths for binding mounts, so in this example we use `pwd` for printing the absolute path of the working directory, i.e. the `app` directory, instead of typing it manually
+ - `node:12-alpine` - the image to use. Note that this is the base image for our app from the Dockerfile
+ - `sh -c "yarn install && yarn run dev"` - the command. We're starting a shell using `sh` (alpine doesn't have `bash`) and
+ running `yarn install` to install _all_ dependencies and then running `yarn run dev`. If we look in the `package.json`,
+ we'll see that the `dev` script is starting `nodemon`.
+
+1. You can watch the logs using `docker logs -f <container-id>`. You'll know you're ready to go when you see this...
+
+ ```bash
+ docker logs -f <container-id>
+ $ nodemon src/index.js
+ [nodemon] 1.19.2
+ [nodemon] to restart at any time, enter `rs`
+ [nodemon] watching dir(s): *.*
+ [nodemon] starting `node src/index.js`
+ Using sqlite database at /etc/todos/todo.db
+ Listening on port 3000
+ ```
+
+ When you're done watching the logs, exit out by hitting `Ctrl`+`C`.
+
+1. Now, let's make a change to the app. In the `src/static/js/app.js` file, let's change the "Add Item" button to simply say
+ "Add". This change will be on line 109 - remember to save the file.
+
+ ```diff
+ - {submitting ? 'Adding...' : 'Add Item'}
+ + {submitting ? 'Adding...' : 'Add'}
+ ```
+
+1. Simply refresh the page (or open it) and you should see the change reflected in the browser almost immediately. It might
+ take a few seconds for the Node server to restart, so if you get an error, just try refreshing after a few seconds.
+
+ ![Screenshot of updated label for Add button](updated-add-button.png){: style="width:75%;"}
+ {: .text-center }
+
+1. Feel free to make any other changes you'd like to make. When you're done, stop the container and build your new image
+ using `docker build -t getting-started .`.
+
+
+Using bind mounts is _very_ common for local development setups. The advantage is that the dev machine doesn't need to have
+all of the build tools and environments installed. With a single `docker run` command, the dev environment is pulled and ready
+to go. We'll talk about Docker Compose in a future step, as this will help simplify our commands (we're already getting a lot
+of flags).
+
+## Recap
+
+At this point, we can persist our database and respond rapidly to the needs and demands of our investors and founders. Hooray!
+But, guess what? We received great news!
+
+**Your project has been selected for future development!**
+
+In order to prepare for production, we need to migrate our database from working in SQLite to something that can scale a
+little better. For simplicity, we'll keep with a relational database and switch our application to use MySQL. But, how
+should we run MySQL? How do we allow the containers to talk to each other? We'll talk about that next!
diff --git a/docs/tutorial/using-bind-mounts/updated-add-button.png b/docs/tutorial/using-bind-mounts/updated-add-button.png
new file mode 100644
index 0000000..cce7a05
--- /dev/null
+++ b/docs/tutorial/using-bind-mounts/updated-add-button.png
Binary files differ
diff --git a/docs/tutorial/using-docker-compose/dashboard-app-project-collapsed.png b/docs/tutorial/using-docker-compose/dashboard-app-project-collapsed.png
new file mode 100644
index 0000000..6584b03
--- /dev/null
+++ b/docs/tutorial/using-docker-compose/dashboard-app-project-collapsed.png
Binary files differ
diff --git a/docs/tutorial/using-docker-compose/dashboard-app-project-expanded.png b/docs/tutorial/using-docker-compose/dashboard-app-project-expanded.png
new file mode 100644
index 0000000..a51a2b8
--- /dev/null
+++ b/docs/tutorial/using-docker-compose/dashboard-app-project-expanded.png
Binary files differ
diff --git a/docs/tutorial/using-docker-compose/index.md b/docs/tutorial/using-docker-compose/index.md
new file mode 100644
index 0000000..d2097db
--- /dev/null
+++ b/docs/tutorial/using-docker-compose/index.md
@@ -0,0 +1,359 @@
+
+[Docker Compose](https://docs.docker.com/compose/) is a tool that was developed to help define and
+share multi-container applications. With Compose, we can create a YAML file to define the services
+and with a single command, can spin everything up or tear it all down.
+
+The _big_ advantage of using Compose is you can define your application stack in a file, keep it at the root of
+your project repo (it's now version controlled), and easily enable someone else to contribute to your project.
+Someone would only need to clone your repo and start the compose app. In fact, you might see quite a few projects
+on GitHub/GitLab doing exactly this now.
+
+So, how do we get started?
+
+## Installing Docker Compose
+
+If you installed Docker Desktop/Toolbox for either Windows or Mac, you already have Docker Compose!
+Play-with-Docker instances already have Docker Compose installed as well. If you are on
+a Linux machine, you will need to install Docker Compose using
+[the instructions here](https://docs.docker.com/compose/install/).
+
+After installation, you should be able to run the following and see version information.
+
+```bash
+docker-compose version
+```
+
+
+## Creating our Compose File
+
+1. At the root of the app project, create a file named `docker-compose.yml`.
+
+1. In the compose file, we'll start off by defining the schema version. In most cases, it's best to use
+ the latest supported version. You can look at the [Compose file reference](https://docs.docker.com/compose/compose-file/)
+ for the current schema versions and the compatibility matrix.
+
+ ```yaml
+ version: "3.8"
+ ```
+
+1. Next, we'll define the list of services (or containers) we want to run as part of our application.
+
+ ```yaml hl_lines="3"
+ version: "3.8"
+
+ services:
+ ```
+
+And now, we'll start migrating a service at a time into the compose file.
+
+
+## Defining the App Service
+
+To remember, this was the command we were using to define our app container.
+
+```bash
+docker run -dp 3000:3000 \
+ -w /app -v "$(pwd):/app" \
+ --network todo-app \
+ -e MYSQL_HOST=mysql \
+ -e MYSQL_USER=root \
+ -e MYSQL_PASSWORD=secret \
+ -e MYSQL_DB=todos \
+ node:12-alpine \
+ sh -c "yarn install && yarn run dev"
+```
+
+If you are using PowerShell then use this command.
+
+```powershell
+docker run -dp 3000:3000 `
+ -w /app -v "$(pwd):/app" `
+ --network todo-app `
+ -e MYSQL_HOST=mysql `
+ -e MYSQL_USER=root `
+ -e MYSQL_PASSWORD=secret `
+ -e MYSQL_DB=todos `
+ node:12-alpine `
+ sh -c "yarn install && yarn run dev"
+```
+
+1. First, let's define the service entry and the image for the container. We can pick any name for the service.
+ The name will automatically become a network alias, which will be useful when defining our MySQL service.
+
+ ```yaml hl_lines="4 5"
+ version: "3.8"
+
+ services:
+ app:
+ image: node:12-alpine
+ ```
+
+1. Typically, you will see the command close to the `image` definition, although there is no requirement on ordering.
+ So, let's go ahead and move that into our file.
+
+ ```yaml hl_lines="6"
+ version: "3.8"
+
+ services:
+ app:
+ image: node:12-alpine
+ command: sh -c "yarn install && yarn run dev"
+ ```
+
+
+1. Let's migrate the `-p 3000:3000` part of the command by defining the `ports` for the service. We will use the
+ [short syntax](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1) here, but there is also a more verbose
+ [long syntax](https://docs.docker.com/compose/compose-file/compose-file-v3/#long-syntax-1) available as well.
+
+ ```yaml hl_lines="7 8"
+ version: "3.8"
+
+ services:
+ app:
+ image: node:12-alpine
+ command: sh -c "yarn install && yarn run dev"
+ ports:
+ - 3000:3000
+ ```
+
+1. Next, we'll migrate both the working directory (`-w /app`) and the volume mapping (`-v "$(pwd):/app"`) by using
+ the `working_dir` and `volumes` definitions. Volumes also has a [short](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3) and [long](https://docs.docker.com/compose/compose-file/compose-file-v3/#long-syntax-3) syntax.
+
+ One advantage of Docker Compose volume definitions is we can use relative paths from the current directory.
+
+ ```yaml hl_lines="9 10 11"
+ version: "3.8"
+
+ services:
+ app:
+ image: node:12-alpine
+ command: sh -c "yarn install && yarn run dev"
+ ports:
+ - 3000:3000
+ working_dir: /app
+ volumes:
+ - ./:/app
+ ```
+
+1. Finally, we need to migrate the environment variable definitions using the `environment` key.
+
+ ```yaml hl_lines="12 13 14 15 16"
+ version: "3.8"
+
+ services:
+ app:
+ image: node:12-alpine
+ command: sh -c "yarn install && yarn run dev"
+ ports:
+ - 3000:3000
+ working_dir: /app
+ volumes:
+ - ./:/app
+ environment:
+ MYSQL_HOST: mysql
+ MYSQL_USER: root
+ MYSQL_PASSWORD: secret
+ MYSQL_DB: todos
+ ```
+
+
+### Defining the MySQL Service
+
+Now, it's time to define the MySQL service. The command that we used for that container was the following:
+
+```bash
+docker run -d \
+ --network todo-app --network-alias mysql \
+ -v todo-mysql-data:/var/lib/mysql \
+ -e MYSQL_ROOT_PASSWORD=secret \
+ -e MYSQL_DATABASE=todos \
+ mysql:5.7
+```
+
+If you are using PowerShell then use this command.
+
+```powershell
+docker run -d `
+ --network todo-app --network-alias mysql `
+ -v todo-mysql-data:/var/lib/mysql `
+ -e MYSQL_ROOT_PASSWORD=secret `
+ -e MYSQL_DATABASE=todos `
+ mysql:5.7
+```
+
+1. We will first define the new service and name it `mysql` so it automatically gets the network alias. We'll
+ go ahead and specify the image to use as well.
+
+ ```yaml hl_lines="6 7"
+ version: "3.8"
+
+ services:
+ app:
+ # The app service definition
+ mysql:
+ image: mysql:5.7
+ ```
+
+1. Next, we'll define the volume mapping. When we ran the container with `docker run`, the named volume was created
+ automatically. However, that doesn't happen when running with Compose. We need to define the volume in the top-level
+ `volumes:` section and then specify the mountpoint in the service config. By simply providing only the volume name,
+ the default options are used. There are [many more options available](https://docs.docker.com/compose/compose-file/compose-file-v3/#volume-configuration-reference) though.
+
+ ```yaml hl_lines="8 9 10 11 12"
+ version: "3.8"
+
+ services:
+ app:
+ # The app service definition
+ mysql:
+ image: mysql:5.7
+ volumes:
+ - todo-mysql-data:/var/lib/mysql
+
+ volumes:
+ todo-mysql-data:
+ ```
+
+1. Finally, we only need to specify the environment variables.
+
+ ```yaml hl_lines="10 11 12"
+ version: "3.8"
+
+ services:
+ app:
+ # The app service definition
+ mysql:
+ image: mysql:5.7
+ volumes:
+ - todo-mysql-data:/var/lib/mysql
+ environment:
+ MYSQL_ROOT_PASSWORD: secret
+ MYSQL_DATABASE: todos
+
+ volumes:
+ todo-mysql-data:
+ ```
+
+At this point, our complete `docker-compose.yml` should look like this:
+
+
+```yaml
+version: "3.8"
+
+services:
+ app:
+ image: node:12-alpine
+ command: sh -c "yarn install && yarn run dev"
+ ports:
+ - 3000:3000
+ working_dir: /app
+ volumes:
+ - ./:/app
+ environment:
+ MYSQL_HOST: mysql
+ MYSQL_USER: root
+ MYSQL_PASSWORD: secret
+ MYSQL_DB: todos
+
+ mysql:
+ image: mysql:5.7
+ volumes:
+ - todo-mysql-data:/var/lib/mysql
+ environment:
+ MYSQL_ROOT_PASSWORD: secret
+ MYSQL_DATABASE: todos
+
+volumes:
+ todo-mysql-data:
+```
+
+
+## Running our Application Stack
+
+Now that we have our `docker-compose.yml` file, we can start it up!
+
+1. Make sure no other copies of the app/db are running first (`docker ps` and `docker rm -f <ids>`).
+
+1. Start up the application stack using the `docker-compose up` command. We'll add the `-d` flag to run everything in the
+ background.
+
+ ```bash
+ docker-compose up -d
+ ```
+
+ When we run this, we should see output like this:
+
+ ```plaintext
+ Creating network "app_default" with the default driver
+ Creating volume "app_todo-mysql-data" with default driver
+ Creating app_app_1 ... done
+ Creating app_mysql_1 ... done
+ ```
+
+ You'll notice that the volume was created as well as a network! By default, Docker Compose automatically creates a
+ network specifically for the application stack (which is why we didn't define one in the compose file).
+
+1. Let's look at the logs using the `docker-compose logs -f` command. You'll see the logs from each of the services interleaved
+ into a single stream. This is incredibly useful when you want to watch for timing-related issues. The `-f` flag "follows" the
+ log, so will give you live output as it's generated.
+
+ If you don't already, you'll see output that looks like this...
+
+ ```plaintext
+ mysql_1 | 2019-10-03T03:07:16.083639Z 0 [Note] mysqld: ready for connections.
+ mysql_1 | Version: '5.7.27' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
+ app_1 | Connected to mysql db at host mysql
+ app_1 | Listening on port 3000
+ ```
+
+ The service name is displayed at the beginning of the line (often colored) to help distinguish messages. If you want to
+ view the logs for a specific service, you can add the service name to the end of the logs command (for example,
+ `docker-compose logs -f app`).
+
+ !!! info "Pro tip - Waiting for the DB before starting the app"
+ When the app is starting up, it actually sits and waits for MySQL to be up and ready before trying to connect to it.
+ Docker doesn't have any built-in support to wait for another container to be fully up, running, and ready
+ before starting another container. For Node-based projects, you can use the
+ [wait-port](https://github.com/dwmkerr/wait-port) dependency. Similar projects exist for other languages/frameworks.
+
+1. At this point, you should be able to open your app and see it running. And hey! We're down to a single command!
+
+## Seeing our App Stack in Docker Dashboard
+
+If we look at the Docker Dashboard, we'll see that there is a group named **app**. This is the "project name" from Docker
+Compose and used to group the containers together. By default, the project name is simply the name of the directory that the
+`docker-compose.yml` was located in.
+
+![Docker Dashboard with app project](dashboard-app-project-collapsed.png)
+
+If you twirl down the app, you will see the two containers we defined in the compose file. The names are also a little
+more descriptive, as they follow the pattern of `<project-name>_<service-name>_<replica-number>`. So, it's very easy to
+quickly see what container is our app and which container is the mysql database.
+
+![Docker Dashboard with app project expanded](dashboard-app-project-expanded.png)
+
+
+## Tearing it All Down
+
+When you're ready to tear it all down, simply run `docker-compose down` or hit the trash can on the Docker Dashboard
+for the entire app. The containers will stop and the network will be removed.
+
+!!! warning "Removing Volumes"
+ By default, named volumes in your compose file are NOT removed when running `docker-compose down`. If you want to
+ remove the volumes, you will need to add the `--volumes` flag.
+
+ The Docker Dashboard does _not_ remove volumes when you delete the app stack.
+
+Once torn down, you can switch to another project, run `docker-compose up` and be ready to contribute to that project! It really
+doesn't get much simpler than that!
+
+
+## Recap
+
+In this section, we learned about Docker Compose and how it helps us dramatically simplify the defining and
+sharing of multi-service applications. We created a Compose file by translating the commands we were
+using into the appropriate compose format.
+
+At this point, we're starting to wrap up the tutorial. However, there are a few best practices about
+image building we want to cover, as there is a big issue with the Dockerfile we've been using. So,
+let's take a look!
diff --git a/docs/tutorial/what-next/index.md b/docs/tutorial/what-next/index.md
new file mode 100644
index 0000000..8eca969
--- /dev/null
+++ b/docs/tutorial/what-next/index.md
@@ -0,0 +1,26 @@
+
+Although we're done with our workshop, there's still a LOT more to learn about containers!
+We're not going to go deep-dive here, but here are a few other areas to look at next!
+
+## Container Orchestration
+
+Running containers in production is tough. You don't want to log into a machine and simply run a
+`docker run` or `docker-compose up`. Why not? Well, what happens if the containers die? How do you
+scale across several machines? Container orchestration solves this problem. Tools like Kubernetes,
+Swarm, Nomad, and ECS all help solve this problem, all in slightly different ways.
+
+The general idea is that you have "managers" who receive **expected state**. This state might be
+"I want to run two instances of my web app and expose port 80." The managers then look at all of the
+machines in the cluster and delegate work to "worker" nodes. The managers watch for changes (such as
+a container quitting) and then work to make **actual state** reflect the expected state.
+
+
+## Cloud Native Computing Foundation Projects
+
+The CNCF is a vendor-neutral home for various open-source projects, including Kubernetes, Prometheus,
+Envoy, Linkerd, NATS, and more! You can view the [graduated and incubated projects here](https://www.cncf.io/projects/)
+and the entire [CNCF Landscape here](https://landscape.cncf.io/). There are a LOT of projects to help
+solve problems around monitoring, logging, security, image registries, messaging, and more!
+
+So, if you're new to the container landscape and cloud-native application development, welcome! Please
+connect to the community, ask questions, and keep learning! We're excited to have you!