Course Overview and Prerequisites
This course provides a complete journey from Docker basics to production deployment, designed to be the most efficient way to build container skills. For a broader foundational understanding, you might find our Docker for Beginners: A Comprehensive Guide to Containerization helpful as a starting point.
What You'll Learn
- Docker fundamentals: Motivation, history, and underlying technologies
- Container management: Running, building, and optimizing containers. Our Docker Tutorial: Comprehensive Guide from Basics to Advanced Concepts goes deeper into these management tasks.
- Multi-service applications: Containerizing a three-tier web application
- Development workflows: Hot reloading, debugging, and testing in containers
- Production deployment: Docker Swarm, secrets management, and zero-downtime deployments
Prerequisites
- Basic familiarity with web application development
- Understanding of basic Linux shell commands
Course Structure
The course is organized into progressive learning modules:
Part 1: Foundations (Sections 1-4)
- Why containers exist and problems they solve
- Linux kernel features enabling containers
- Docker Desktop installation and configuration
- Running first containers
Part 2: Building Containerized Applications (Sections 5-11)
- Three-tier demo application (React frontend, Node.js/Go API, PostgreSQL database)
- Dockerfile creation and optimization
- Container image optimization techniques:
- Pin specific base image versions
- Use multi-stage builds for smaller images
- Install production dependencies only
- Run as non-root user
- Leverage layer caching for faster builds
Part 3: Deployment (Section 12)
- Docker Swarm cluster setup
- Zero-downtime deployments
- Health checks and secrets management
- DNS configuration. For a more in-depth look at orchestration, see our Docker Containers and Kubernetes Fundamentals Course Summary.
Key Docker Commands and Concepts
Container Runtime Options
-d(detached): Run container in background-it(interactive TTY): Interactive shell session--rm: Remove container when it exits-vor--volume: Mount volumes for persistent data--network: Connect to specific Docker network-por--publish: Map host ports to container ports
Docker Compose vs Docker Run
Docker Compose provides a declarative YAML file format to define multi-container applications, making it easier than managing multiple docker run commands. Our The Ultimate Docker Course: From Zero to Hero - Complete Guide & Tutorial covers this in more detail.
Container Security Best Practices
- Image security:
- Use minimal base images (Alpine, slim variants)
- Scan images for vulnerabilities (Docker Scan, Trivy)
- Pin exact versions
- Use multi-stage builds
- Runtime security:
- Run as non-root user
- Drop unnecessary capabilities
- Limit CPU and memory
- Use read-only filesystem where possible
- Implement secrets management
Demo Application Architecture
The course uses a three-tier web application:
- Frontend: React with Vite (development) / Nginx (production)
- API Layer: Node.js and Go implementations
- Database: PostgreSQL
Deployment Options
Docker Swarm (Covered in Course)
- Built-in orchestrator
- Health checks for zero-downtime deployments
- Built-in secrets management
- Simple single-node to multi-node scaling
Alternative Deployments (Bonus Content)
- Railway (platform as a service)
- Kubernetes (container orchestrator). Learn more about cloud-native deployments in our Comprehensive AWS and Azure Cloud Computing Course Summary and Key Projects.
Tools and Platforms
- Container registries: Docker Hub, GitHub Container Registry
- CI/CD: GitHub Actions for automated builds and scans
- Ephemeral environments: Shipyard for PR-based testing environments with exclusive coupon code available
hey team welcome to this complete course on Docker and containers my goal with this course is for it to be the most
efficient way for you to ramp up on your Docker and container skills starting from the basics and progressing through
to intermediate and advanced concepts by the end of the course you'll have the Knowledge and Skills to be productive
within a team using containers for both development and deployment of their applications you'll understand why you
would want to use containers as well as how to integrate them seamlessly into your own workflow this course is split
between the and practice for each section I'll cover the concepts that you need to know using helpful visuals and
diagrams before diving into our code editor and solidifying that knowledge with real world
examples I've provided a companion GitHub repo Linked In the description containing all of the code and
configuration that will be shown throughout the course feel free to clone or Fork the repo to follow along as we
go before diving in I do want to highlight a couple of prerequisites that will help you get the most out of this
course as the image shows it can be tempting to jump straight into learning about
containers and skipping some of the foundational Concepts that they build upon I'll do my best to explain
everything in such a way that even an absolute beginner would understand but if you have some familiarity with web
application development and how all the different components fit together that will be helpful and make it easier to
understand what aspects are container specific and what aspects are just General application architecture
concerns also having some understanding of basics L shell commands will be useful as we'll use such commands
throughout the course if you don't have these I think you can still get value from the course
but some things that I do or say might not make sense so it could be worthwhile to go and brush up on those topics
first I've broken down the course into the following learning modules each building progressively upon the last I
provided timestamps in the description to make it easy to jump between sections if you're already familiar with the
topic or if you plan to revisit this video and watch it across multiple sessions in the first portion of the
course sections 1 through 4 we'll explore why containers exist in the first place the problems that they solve
and the underlying Linux kernel features that make Docker and containers possible we'll also install and configure the
docker desktop application and get our first hands-on experience running some containers for the remainder of the
course we'll be focused on building out and containerizing our own web application from development all the way
through to deploying it to the cloud this is where the rubber meets the road and you'll learn exactly how software
companies use Docker throughout the application development life cycle so what is this demo application we're
working with I'll go into it in much more detail during section five and throughout the rest of the course but in
summary it's a minimal three- tier web application meaning it has a front end in this case built out using react an
API layer we actually have two implementation of this one in nodejs and one in goang to highlight how different
languages are handled with containers and finally a postgres database each service is designed to be as simple as
possible while still showcasing how they all fit together within a real world application when we get to the
deployment section of the course we'll learn how to deploy our containerized application to the cloud within the
course I'll be using Docker swarm which is a container orchestrator built into Docker and I've also filmed two bonus
videos to highlight additional methods of deploying containers one using Railway and another using kubernetes
these are separate from the course and information about where you can find them can be found in the description
this should give you a taste of the variety of options that we have for deploying containerized
applications before we jump into the course content I want to pause for a moment to thank the sponsor of this
video Shipyard because of their generous support I can provide this course to you completely free of charge I'm excited
about them as a sponsor because Shipyard is highly relevant to everyone working in the container World their platform
offers the easiest way to generate on demand ephemeral environments and other words automatically generating fully
functional isolated environments for every pull request you connect your G Hub repo add a couple of lines of code
to your configuration files and you can have test environments running in just a few minutes later in the course I'll
demonstrate how to use the shipyard platform with the course sample application as a bonus for all the
students of this course the team at Shipyard has created an exclusive coupon code for you the first 300 people to
create a shipyard account and use the promo code devops directive will get 30 extra free days on either the startup or
business tier plans links and instructions are in the description below you should go check out the
shipyard platform and consider how it can help your teams deliver better software faster without further Ado
let's get into the course now in this first section of the course I really want to uh ground our
understanding of why containers exist and why a product like Docker has been so successful uh we're going to start
with kind of the motivation and then we'll walk through the history of different virtualization Technologies
from bare metal to Virtual machines to containers uh and try to understand how that Evolution has occurred and why
containers have become the dominant uh kind of development and deployment mechanism for software applications
today and this meme on the right is one that I've seen shared a number of times uh it's this idea that I'm building a
software application I get it running locally but then something goes wrong in the process of Translating that local
system to the production one and so one thing that Docker does really well is help to make it such that our local
environment is as close as possible to the production environment that we're going to deploy to and so that's what
this is this is hinting at is we're taking that local environment and just shipping it directly to
production now there's two main aspects uh of the software development life cycle that Docker and containers are
trying to address uh the first one is on the development side so up front as we're building this application maybe
historically there was this long process proc you you join a company and they tell you here's your new laptop uh go
ahead and and install all these things run these scripts and get everything configured and now you can run the
application locally and do your development uh it was very complex lots of opportunity for human error uh and so
then Docker came along and Now setting up your development environment is as easy as running a single command Docker
compose up we're going to go more into what that is doing later in the course but it took this whole complex sequence
of of events that you needed to uh run and turned that into a singular system and command that
everyone can run and be up and running uh in the first hour of their their day and so making all of the different
combinations of Windows and Mac and Linux development environments all compatible uh is one thing that Docker
does really well and containers in general do really well so that's kind of the the de vment side of things on the
deployment side of things uh similarly let's say you would build your application and then you need to to put
it onto some server somewhere to run it and there was this uh idea that there's a number of steps that need to happen
let's say okay we need to create a server running auntu then we're going to run some configuration software like
anible to get all the dependencies configured and then finally we're going to copy our application code onto the
server and run it with these configuration options again lots of steps lots of complexity uh and much
harder to ensure that it's going to be right every single time what containers do is they bring a
single standardized package such that as long as my server has a container runtime and there's a number of those
available Docker provides one but there's a number out there all you need to do is take this standard container
image and run it with these specific options and that's it so now instead of worrying about the underl operating
system and the underlying dependencies as long as we have our system and a container runtime and our container
image we can run it and get repeatably reliable results uh over and over and so these two aspects are really why
containers exist and you can see how much simpler it gets when we use containers going from this historical
system with lots of steps and and potential for error into a much more simplified uh unified approach using a
system like container and like docker and so that hints at what a container is I've talked about kind of the
standardized package uh but from the docker website a Docker container image is a lightweight Standalone executable
package of software that includes everything needed to run an application so you can think of it as this box that
contains the underlying sort of os dependencies let's say runtime dependency so if I was writing an app in
Python it would have the python runtime with the version that is needed uh it would have any of the Python libraries
that I'm using so maybe I'm using SQL Alchemy to talk to a database or fast API to build out an API and then it's
also going to have my application code so the specific application code that I'm writing all of that gets bundled
into this container image and then the beauty is that we have this standard interface and can run multiple copies of
it so let's say I need two copies of this application I can take that image with a container runtime and run a
container run two copies run three copies Etc and so I've used the term container and container image it's
important to know the difference there a container image is this artifact that has all of the all of these dependencies
within it a container is what you run from that image so you take an image and if you think of object oriented
programming a container image is like the class and then the container itself that you're actually running is like an
instance of that class an instantiation of that class and so a container allows us to
have this standardized Packaging that we can then create one or more copies of uh and those copies are going to be the
same every single time so far I've been using the term Docker container uh but I want at this point to call out what is
known as the oci or the open container initiative and effectively there were a few early players including Docker
building out the early implementations of containers and as they became more popular companies realized that rather
than having a whole bunch of slightly different but similar things that we're calling containers it would make much
more sense to come together and create one standard that can be used across different
implementations that's exactly what the cont the open container initiative is a number of companies including Docker
Google VMware Microsoft Dell IBM Oracle the list goes on came together uh and created open industry standards around
what a container is and how it should be run and so there's three primary specifications that the oci defines the
runtime specification the image specification and the distribution specification so the image specification
defines what should be included in the image in terms of metadata and the format that it should contain so a
serializable file system the runtime specification defines how you would take an image that adheres to that image
specification and run it in a container and then the distribution specification talks about how those images should be
distributed so think about Registries and pushing and pulling images and so these three things together make up the
open container initiative and Docker has a specific implementation of this standard so when I talk about Docker
images or Docker container images what I'm referring to specifically is the docker implementation of this oci
specification if Docker is the only container tool that you're using it's not all that important to know this I
wanted to provide this context in case you start using other tools and know that there is this standard format that
can be shared across different Tooling in the ecosystem now now let's walk back in time a little while and understand
how we got to this idea of containers hopefully from my description of the motivation uh you understand why
containers are useful but it's I think it's also important to understand the history and the context of how we got to
where we are today and so before there were containers before there were even virtual machines uh we just had
computers uh we now call this bare metal Computing so if you're running your applications directly on the Hardware
system itself uh another term for that is bare metal but back long enough ago this was just called Computing right so
in this case you have your your physical host your server um the physical Hardware layer at the bottom you've
installed whatever operating system you want on top of it then you've installed whatever binaries and libraries are
necessary to run your application and then you would have your application one or more applications installed directly
uh onto this host machine now what are some challenges with this setup uh first of all you can
see that shared layer of binaries and libraries uh it's shared across both of those applications and so if your
applications for example need different versions of the same dependency or have some dependencies that are somehow
incompatible with each other now we've got we've gotten ourselves into what is known as dependency hell so uh we
somehow need to either modify our application such that those dependencies are not compatible or figure out a way
to to carve up the system such that we can have both versions of the the libraries available doing things like
setting uh your your path environment variables differently for the different processes Etc so that can be quite a
challenge um you also because of this dependency management challenge you're more likely to have le fewer
applications on each host machine uh and so it makes it harder and harder to achieve High utilization so let's say
application one takes one CPU core application 2 also takes one CPU core but we have this server-based uh system
that has 64 cores um it could be very hard to sort of utilize that compute capacity effectively because of the
challenges uh associated with managing dependencies for for applications you also now if you are
installing multiple applications onto the system if you ever have an issue with one of those applications or do
something that will mess up uh some underlying dependency now you have this large blast radius meaning uh changes or
or issues with one application can directly impact uh the performance of another application running on that same
host machine so uh it just makes it much more dangerous to to modify things also the startup and shutdown
speed of a physical system is on the order of minutes so uh to spin down that server and reboot it for for some reason
uh that can take a number of minutes and so so it's just relatively slow if you want to try to achieve high uptime and
and eliminate downtime for your applications in addition to that creating new physical machines uh sort
of provisioning a new system is very very slow so depending on my setup if I have my own Data Center and I need to
actually order the hardware and get it installed and hookup networking that could be on the order of days or weeks
if I'm working with a co- location facility or a provider that offers bare metal systems that provisioning might be
on the order of hours um but still quite slow relative to what we'll see as we evolve and and sort of add in this
concept of virtualization and so that's bare metal I think most people are familiar with
working with bare metal um even if they don't realize it because when you run things on your laptop or your desktop
you're installing them directly on the host and so that is this concept of of bare metal however that's not the only
way to do things and so these challenges really prompted Engineers to come up with this concept of virtualization and
adding virtual machines such that we can eliminate some of those challenges and so in the case of a virtual machine now
this outer boundary is still the host the physical system the server uh we've got our Hardware um we may or may not
have an operating system running on that Hardware I'll talk about that more in a second but then we have what's known as
a hypervisor and so a hypervisor is some combination of software and Hardware that allows us to carve up the physical
resources so think CPU cores memory access to storage or gpus other devices it allows us to carve up that pool of
resources into isolated smaller pools that we can then install our our systems onto and so this hypervisor kind of
gives us this virtual Hardware layer upon which we can then install an operating system install binaries and
libraries and install our applications and so now rather than having both applications on the single physical host
we now have application one and application 2 each with their own virtual isolated virtual machine uh
running on top of that hypervisor and so there's two types of hypervisor type one means there's no underlying OS these are
running directly on the physical Hardware um this is beneficial because you don't sacrifice any performance in
that sort of os to software virtualization layer uh there there's a small performance hit but much less than
if you have an operating system on the hardware and then a hypervisor on top of that uh so things like AWS Nitro so
that's the hypervisor that Amazon web services is using to create virtual machines on their servers uh VMware is a
very popular virtualization uh company and they have vsphere which is a type 1 hypervisor if you want to just create a
virtual machine let's say on your laptop you're probably going to use a type two uh hypervisor uh one such example is
virtual box so you can install this program virtual box and then within that you can create virtual machines for
various uh operating systems Etc and so how does this compare to those challenges that we saw with the
bare metal systems uh one we've eliminated this dependency hell so now each virtual machine has an isolated set
of dependencies binaries and libraries such that our application can specify whatever dependencies it needs and not
have to worry about the needs of other applications uh because of this we're going to have better utilization so we
can carve up a big server CPU into many different smaller uh chunks and each of those chunks can have its own virtual
machine and then we can install our applications accordingly so this is what enables a cloud provider you can go on
and see oh I can I can choose between all these different machine types they don't actually have all those different
machine types installed in their data center they have much larger machine types and you're getting some sliver of
that installed as a virtual machine you also we've also reduced the blast radius significantly such that now
if if something goes wrong with application one on virtual machine 1 that should be isolated from application
2 and Virtual Machine 2 uh and it should not cause any issues and so because of that we've we've increased the safety of
of making changes we've also sped up the speed at which we can start and stop these things so now it's still on the
order of minutes probably um but faster than shutting down and starting a physical server and then finally uh uh
one area where virtual machines have a big uh bonus over bare metal is that we can provision and decommission them much
much faster and so we're all used to living In This Cloud world where we can go onto our favorite cloud provider
click the button or issue a command and have a new virtual server ready uh within minutes or if we're done with it
we can decommission it in a can also be done and and we can stop paying for it within minutes as
well and so that's the sort of the second tier or the second rung along this path of virtualization the next one
being containers and so containers obviously are the focus of this course um they change things a bit further
still so now in this outer box we've got our host or our which could be a physical system uh it also could be a
virtual machine so we can take either of those two uh Concepts from before and we can run containers within that
context uh you can see at the bottom we've got either our virtual or physical Hardware with an operating system and
then the key piece here that's kind of analogous to that hypervisor uh is the container runtime so this is a piece of
software that knows how to take a container image and run it within the host system uh the key difference
between virtual machines and uh containers is that virtual machines are running their own copy of the Linux
kernel uh and I'm talking only about Linux containers within this course there are windows containers but that is
a different topic for a different day and so with Linux containers uh they are actually running they're sharing the
kernel with the host operating system uh versus in a virtual machine you have your own oper you have your own kernel
and you have a copy of your own operating system that's running and So within a container you're sharing that
with the host however we're using certain features that'll go into later of the Linux kernel to help provide uh
isolation between the different container processes and so you don't get quite the same level of isolation that
you would with a virtual machine you do get quite good isolation if you configure things
properly as we can see now we have our two containers both running uh on the same host however they are still
isolated and have their own copies of binaries and libraries even though they're sharing that underlying kernel
now there's a couple of different sort of types of software related to containers the first category of which
I'll call the desktop container platforms so these are sort of container Management Systems things like Docker
fall into this category things like podman these are things that you would install on your development system to
manage containers manage container images uh run containers uh set up the the necessary networking Etc um so these
are kind of the holistic lifecycle systems and then there's much more targeted or specific pieces of software
called container run times which include things like container D or cryo and these are very specifically designed to
take a container image that aderes to the spec and run it uh with the necessary configuration options to
provide that type of isolated environment that that is shown in the image now how does this compare to
Virtual machines and bare metal in terms of those challenges we saw before again we don't have the issue of dependency
conflicts because they each have their own underlying binaries and libraries uh We've improved our utilization
efficiency even further because we now for each virtual machine we had to have an entire copy of that operating system
so each of those virtual machine images is going to be on the order of gigabytes whereas container images can be much
much smaller because they're able to leverage that underlying OS and so they're generally going to be on the
order of megabytes uh I have seen very large multi-gigabyte container images but that's not ideal
uh but because of this we're able to utilize the resources of the system much more effectively again we have a small
blast radius where we have some level of isolation even if it's not quite as strong of an isolation as a virtual
machine we're able to start and stop containers even more quickly than virtual machines so on the order of
seconds uh we're able to in this case starting and stopping a container uh is kind of equivalent to provisioning and
decommissioning so also on the order of seconds we can bring up a new container from scratch and clear it out as well
and then the final sort of key piece I think with containers is that they're small and light enough that we now start
to use them within our development environment sure you could use something like virtual box you could create a
virtual machine that tries to replicate your uh your production environment but it's kind of slow and clunky and not
that many people do some people do but but many people don't containers are light enough uh that now more and more
will start to actually run containers and we're going to do this later in the course run containers within our
development environment such that we can have high confidence that our our production environment is as similar as
possible to that development environment and that just decreases the likelihood that we're going to hit some small
nuanced bug uh between the differences there uh and so we're able to develop more efficiently uh using a technology
like this and so I I pointed out here that this host can be a virtual or a physical machine and so now we can get a
little bit uh Inception with it where we have a host machine with virtual machines running on it and then
containers running within uh those virtual machines and so this is a very common configuration that you'll see uh
most people who are working with containers in the cloud are going to have something like this where you have
multiple virtual machines each running multiple containers uh and so this is just combining all these Concepts
together and the real challenge now becomes how do I how do I manage all of these containers
across those virtual machine hosts and that's where this concept of orchestrators comes in and so things
like kubernetes Nomad from hashicorp or Docker swarm are these systems that are designed specifically to take containers
and run and manage and schedule them across multiple nodes and so in this case the node is each of those virtual
machines and so when we get to the deployment section of this course we'll talk a little bit more about this but
it's just important to know kind of the concepts of hey I'm starting with this bare metal server it my my code has to
run somewhere on some physical CPU I'm carving it up into these smaller virtual machines that I'm able to manage a
little bit more effectively but then I also want to gain the benefits and the speed uh and the efficiency that a
container provides and so combining these different types of virtualization in this way that we see more and more
companies using in this day and age this just summarizes most of those challenges and tradeoffs uh that I
talked about before uh in terms of dependency management uh virtual machines and containers really shine uh
in terms of utilization containers definitely win out uh just because it's much easier to get uh to to pack
applications more efficiently uh when running them in containers uh from an isolation perspective uh you can't get
any more isolated than than multiple physical hosts if you're running application a on on one server and
application B on one other physical server that's the strongest isolation you can get virtual machines still
provide very strong isolation and those hypervisors are these very optimized systems with a small attack surface area
that have really been battle tested and so I I think isolation provided by virtual machines is almost as good as as
those from bare metal obviously there could still be some unknown bug that that causes uh a security issue there
but I think most people have quite high confidence in the isolation that hypervisors provides and then containers
um this is one area where they are less good than these other options is they do provide some level of isolation we'll
talk about the the ways in which they do that but they don't give the same they're not as strong and if you make a
configuration mistake the the level of isolation can be dropped significantly so that's why I gave containers a yellow
on isolation um startup speed containers win out the ability to run containers within a development
environment because they're lightweight enough means that we're much more likely to do so and therefore have stronger
parity or have have our development environments and our production environments more similar to one another
this control row is one that I haven't talked about as much this is just the fact that you can have full
configuration control over the system um in a bare metal context that you might not get in a virtual machine so only you
only have control over the features or the specific configuration options that the hypervisor passes through or the the
container runtime passes through and so maybe there's some specific type of GPU or Hardware accelerator that is not yet
supported by your particular container runtime or your hypervisor in those cases the bare metal system is going to
win out because if you need that specific Hardware config that might be the only way in which you can achieve it
and that leads into kind of this this next row of performance obviously ly a summary of red yellow green um here
can't fully capture the nuances you don't take that much of a performance hit when you move to a virtual machine
or container however I think one place that performance can win out on bare metal is because of that control you're
able to do things like if you're if you're running multiple servers for example ensure that they are directly
next to each other on the same server rack such the networking latency is as small as possible or ensuring that the
storage devices that you're using are directly connected to that physical Machine versus accessing
them over the network and so there is Performance Tuning that bare metal provides the opportunity to do um but
overall the performance is fairly comparable for most use cases across these different
Technologies and then the final column there just sort of how challenging it is to work with and so so if I'm if I'm
buying and racking my own servers in a facility that's intensely difficult to do and and manage there are now
providers that can give me access to Bare Metal systems and handle some of this operational overhead but I think
it's still more challenging than virtual machines and more challenging still than containers where I can spin something up
and and down very easily and there's all of these services that have container run times such that as long as I provide
a compatible container image I'm able to run it within that system and we'll see that optionality uh in in a later
portion of the course when we get into deploying our own containerized applications and so now that we have a
better understanding of the motivation and history of containers and virtualization Technologies I think it's
useful to do a bit of a technology Deep dive and start to understand the the foundational Linux kernel features that
software like Docker is using to make the magic of containers happen and provide those isolated environments that
we described uh then towards the end of this section we'll actually take a look at the docker system architecture itself
there's three technologies that make up the core of uh a container and that is namespaces control groups
and Union file system and now namespaces are a a mechanism to wrap a global system resource such that from within
that namespace any processes running within the namespace it appears as though they have their own isolated
instance of that resource uh and they cannot see things happening outside of their own instance uh so for example on
the right here we have a listing of the various types of name spaces at one very good example example is the PID
namespace so the process ID namespace within a container it is isolated with with a p namespace such that the process
I'm running appears to be process ID number one even though there may be tons of other processes running on the host
from inside the container we don't know anything about those host processes and we essentially think that we are the
only thing running uh in this system another good one to call out is the username space uh so by using a username
space containers are able to map a one user within the container to a different user on the host system so we
could be running as a root user with elevated privileges inside the container but by run as long as that container is
run within a user name space that's configured correctly that root user inside the container may actually map to
a non-root user outside the container uh and that's very important from a security perspective in case there was
an issue with with one of our containers and an attacker was able to gain access to it and break out of that container if
we had properly set up username spaces then on the host they would not have the same level of privileges that they had
inside the container cgroups are a Linux kernel feature that allow you to organize your
processes into groups which you can then limit and monitor access to certain resources and so on the bottom left you
can see within the file system under proc cgroups we can see all the different cgroups that are available
these correspond to different system resources whether they're CPU memory storage devices Etc and using cgroups we
can specify a configuration for a particular process or group of processes on how they should be able to
access those system resources and so for example we might have one application that is enabled to use up to 30% of the
CPU Cycles we would use the cpu. shares cgroup maybe it can use up to 50 megabytes of memory we would specify
that with the memory. liit in bytes cgroup and then we could also limit the amount of disk read that it has access
to uh using the block IO throttle read bits per second device C group uh now the reason that this is important is
that it allows us to avoid what's called a Noisy Neighbor problem where one application uh is very resource hungry
and sort of starves the other applications of resources by using cgroups we can isolate from a
performance perspective each of our different processes and each of our different
applications now the third key underlying technology is what's called a union Mount file system a specific
example of which that Docker uses is called overlay FS it essentially allows you to take separate file systems and
combine them into a unified view that you can then operate on and so in the image you can see we have a lower file
system and an upper file system with various files we got file one 2 a and four in the lower layer in the upper
layer we have a different file 2B a new file three and then we've actually deleted file 4 when we combine these
layers into the overlay we end up with a a combination of those two things where the upper layer takes precedence over
the lower and so we have file one file 2B and file three and so while Nam spaces and cgroups are sort of what make
containers possible it's this concept of a union Mount file system that makes containers practical because of this
layered approach we're able to share lower layers and cache those layers such that the amount of data that needs to be
transferred and stored for container images is much much smaller because many images can share a common lower layer
and then only some subset of the upper layers can be modified which enables us to move less data when we need to get a
container image onto a system and allocate less space if we're running multiple copies of the same container
and so with those Technologies while you can interact with them directly it's software like Docker that takes it and
packages it into a really nice user experience so rather than needing to know all the different commands to
create control groups and move processes into them or create Nam spaces and and block off the different portions of our
system in such a way Docker has bundled that uh into an easy to use desktop application that we can install in our
system and get all of that uh with a series of simple commands that we're going to learn throughout this course
and so this is a diagram of the docker desktop application uh there's there's a number of pieces here here so let's call
them out in turn on the left hand side is the client application and so this is the thing that we will be interacting
with there's a a number of elements there's the command line interface so when I go to my command line and type
Docker run or Docker pull I'm interfacing with that Docker CLI there's also a a graphical user interface which
gets installed uh this is nice for browsing which images you might have on your system uh you can do things like
configure how much CPU and and memory and disc space the docker application has access to so there's a number of
nice user experience features that the that the guy provides there's also a credential Helper and so this is when we
learn more about Registries later on in the course essentially a registry is a place where we can store we can push
images to and pull images from and so in order to have a private registry you need some way to log in and so the
docker credential helper is a mechanism to help store the necessary credentials to do the authentication for that
and then finally uh extensions are a relatively new feature of Docker that are thirdparty pieces of software that
plug into the client and provide additional functionality in some way and so that's the client application when
you install Docker desktop on your system it also creates a Linux virtual machine uh so if I'm on Mac OS there's
no such thing as a Mac OS uh container uh instead Docker uses a hypervisor installs a Linux virtual machine and
then within that virtual machine it setss up uh two things it sets up the server host application and so that's
known as the docker demon or Docker D that exposes an application programming interface the docker API and so when I
execute a command from the the command line uh let's say I do Docker run that command gets passed to the docker demon
which is listening via that Docker API and then it executes whatever command that is specifying uh within that server
host application so let's say I did Docker run and then specified I wanted to run my container image one I might
create it would then go off and create that container 1A let's say I then issued that same command Docker run with
my container image number one again I get another container container 1B and so this Docker demon is managing those
container objects as well as things like uh networking and and volumes all within this server host
application finally there's an optional kubernetes cluster that gets configured and installed uh within that virtual
machine this can be a nice to have if you are developing four kubernetes you can leverage that cluster directly
without having to uh install and configure a separate cluster I mentioned for Mac OS it's a Linux virtual machine
on Windows there's actually two options you can either configure it to use uh WSL Windows system subsystem for Linux
as the back end or you can use hyperv as that virtualization layer uh to create a virtual machine I think either of those
are viable when we go through the installation and setup in the next portion uh we can talk through that in a
a little more detail and then the final portion to call out on the right I hinted at this but a registry is just a
place to store your container images and share those container images uh with your team or with the world or with
wherever you're deploying uh your containers to and so while it is not a part of Docker desktop it's useful to
call out here uh because that Docker demon when it needs to get an image that it's not building locally it's going to
go off to some registry dockerhub is a great example of one such registry and pull that image into the the host
environment so that it can then run the container from it there's also a little bit of confusion around Docker Docker
desktop and Docker engine I've highlighted a few of the subcomponents here that make up what's known as Docker
engine and so that is the command line interface so that Docker CLI uh as well as the docker demon and the docker API
those make up what is known as Docker engine and that is the open-source core of Docker because the the Docker demon
only runs on Linux you can only install Docker engine on Linux you don't get sort of this niceness of the Linux
virtual machine and sort of the developer experience that the GUI provides but if all you need to do is
build run and interact with containers then Docker engine might be sufficient for your
needs in this course though because we're using Docker for development and likely many of you are on Windows and
Mac OS systems we're going to install Docker desktop so that we can leverage all of the developer experience niceness
that Docker desktop provides at this point I think we are ready to go ahead and install Docker on our systems and
start using it one thing that is important to call out is that for personal use Docker is completely free
you can use Docker desktop without paying anything uh however if you're using it for commercial purposes and
your team has either more than 250 employees or more than $10 million in annual revenue uh it does require one of
the paid tier subscriptions so depending on how using it within your company uh it may or may not be free so that's just
something to call out before we uh before we install it but for personal and learning purposes it is completely
free now to install it we can just go to the docker docs under docs. do.com Docker and so in preparation for this
video I actually uninstalled Docker so I'm going to go ahead and pull open Chrome and go to that uh site and so
here I am at docs. do.com getd doer it gives us options for Windows Mac and Linux uh so
choose whichever operating system you're on and then it will have some additional system requirements uh and information
that you're going to need I'll just click Docker desktop for Mac with Intel and it'll start downloading uh it is
quite a large file so we'll see hopefully my uh home internet is fast enough down below it has information
about system requirements uh for depending on the type of system that that you're running so you just want to
go through uh and read these the installation process is pretty much just like any other desktop application here
it's downloading that dot DMG file I'll then open it drag it into my applications folder on Windows similarly
a pretty straightforward installation process the one thing to call out is the two different options for the back end
so uh I described in the architecture diagram how you have your client running directly on your system but then you're
going to have this virtualized environment where the docker demon is running and so this is choosing whether
you want that Docker Dage to be running within WSL or using uh hyperv as the the backend
system if you are on Windows I would probably just go with this WSL backend as that's kind of the default option
here so it looks like my installation file has finished I'll drag it into
applications it expands from the 600 megab download into a 2 GB install and now I should be able to just
go to my applications and click on Docker it's
verifying it asks whether I want to trust it or not click open and so now here is that graphical user interface uh
one thing that I will call out is you may want to go under settings and then resources and here you can
specify how many of your system resources that Docker as a whole should have access to so here it's defaulted to
four CPUs 10 gigs of memory I'm going to give it access to a little more memory uh the defaults are likely fine for just
getting started but if you find yourself running up against resource constraints modifying these uh resource
configurations can be very helpful in in alleviating those constraints in order to have those
changes take effect you have to click apply and restart and so I'll do that now now at this point we can run Docker
commands and installing that application gave us both the CLI as well as the the docker demon and so let's run our first
container um one fun example that we can use is uh the whalsay container and so I can do Docker
run Docker SL whales say uh and then the command is going to be C say and I'll just
say hey team and so if I run that it's going to go off and find that public whale site image download it from
dockerhub so we can see it downloading now and each of those lines corresponds to one of the layers in that overlay
file system and so it's downloading each of those images onto the host system such that we can then run a container
from that image and we see it took the phrase that I put in here uh and gave us this nice asky
art output uh with that phrase built in and so with that we have uh pulled an image from dockerhub run a container
from it and provided a custom command so that's kind of the the most basic thing that we can do uh with a Docker image
and so that's a fun example but let's do something a little more useful let's say we needed a copy of postgres 15.1
running uh for our application uh we can just issue a Docker run command and then I'm I'm doing two options here one I'm
setting the postgress password environment variable uh that's just something that's needed the container
won't start if you don't set a password and then I'm also publishing Port 5432 and so because these Docker containers
are running on an isolated Network the only way that I'm going to be able to connect to it from my host is to publish
that port and so that publish command is saying take Port 5432 on my my my Local Host my system and connect that to Port
5432 inside the container uh I'm then specifying the postgres image and specifically uh version 15.1 uh running
on the Alpine operating system again it didn't have that image locally so it's going to pull that from
dockerhub once it finishes pulling it then starts up the image and now we have the
database system running inside that container ready to accept connections uh just to show that it is actually running
and and accept connections we can open up PG admin uh and we can see I've added this
Docker database uh and it was able to successfully connect to it uh and we can for
example run a query I could do select star from information schema
tables uh and run that and we get all the tables from that information schema within the database
running inside the container hopefully this gets you excited about the possibilities of what running inside of
these isolated container environments can provide in terms of your development life cycle now that we've run a couple
of simple containers on our system I want to really dive deep on using thirdparty container images and how we
can leverage these within our development process to solve a number of different use cases now dockerhub is a
container registry hosted by the company Docker there's over 100,000 publicly available images ranging from things
like Alpine and auntu which are different flavors of Linux to specific tools such as engine X which is a a web
server and reverse proxy or busy box which is a utility image that has tons of different uh useful commands that you
can leverage on your system from within Docker if you go to hub. do.com you can then search the images or browse based
on the different filters there on the left and so if there is some utility that you want to use likely someone has
created an image for that now before we go off and create a bunch of containers on our system I do
want to pause and call out the fact that by default any data that we create or modify within a container at runtime is
going to be ephemeral meaning if that container is stopped and removed from our host all those data will be gone now
there are two mechanisms by which we can help to solve this uh for dependencies that we want to be in the container when
it's started up we'll want to build those into the image itself and we'll look at how to do that um as you can see
in the diagram on the right if we remember that overlay that Union file system approach we have all of our
different build layers there and those at runtime are treated as readon so we've we're starting with an auntu image
we're installing uh some dependencies we're copying in our source code we're running npm install to pull in
those node uh those node dependencies all of those layers when we create a container from that image or treat it as
readon and then there's just this thin read WR layer on top that's where any data that's modified at runtime will go
if there's some additional dependency or configuration change that is needed rather than starting the container and
then making that change where that change would live in the the the read WR layer at the top we'll want to pull that
into our build process such that every time we create a container from that image it will have
the necessary change now the other case with data that we may want to persist are data
generated by the application at runtime so this could be things like data that we're storing in a database uh when a
user creates or or modifies some uh some aspect of their profile let's say on a website those data we can't incorporate
into the image because they're not known at build time but instead we need to store them in such a way that they'll be
that they will persist beyond the life cycle of the container itself now there's two mechanisms to do that the
first of which is called a volume and so we can create what's known as a Docker volume that will designate a particular
uh location within uh the docker host system so it will be within that virtual machine that Docker is running uh
located at/ barli doer volumes uh we can then when we run a container tell Docker to use that volume and mount it at a
specific path within our container file system such that any data we store there gets stored in the volume and that can
be managed separately from the life cycle of the container itself now the second option is a bind Mount it's a
where the volume Mount the data is still stored within that virtual machine on the Linux file system with a bind Mount
we're actually we're actually connecting our host file system into the Container at runtime so I'm going to have some
path on my system uh and then I'm going to mount that into the Container at runtime and now if I modify any files
within that path they'll show up on the host file system both of these are viable options Docker suggest using
volume mounts by default one reason for this is that because that bind Mount is crossing the boundary between our
virtual machine and our host system performance can be much lower if we are doing things that have a lot of reads
and wrs and so by default use a volume Mount if you need to have easy observability into the data that's being
modified or you want to be able to see the files that are being created more easily then a bind Mount can be a good
option also later in the course we'll use a bind Mount to actually Mount our source code into the Container such that
when we make changes and are developing on the Fly those changes can be represented within the container file
system right away I just covered a lot within this one slide but I created a walkth through in the GitHub repo that
we'll do now that I think will really solidify these Concepts so the read me for the fourth
module of the course uh so the running third party containers module has all of this within it and so now I'm going to
walk through the process here that is outlined in this read me and I think it will make what I was just describing uh
much more clear about data persistence so remember our container file system looks like this we have a number of
readon layers that that exist in the image and then we have a read right layer at the very top let's do some
experimentation with uh running a container installing something in it and seeing how that behaves when we create
additional containers from that same image so let's start by doing a Docker run command and we'll pass it the uh--
interactive flag in the D- TTY flag those two flags together mean that when we issue this command we'll get a
running shell within that container the-- RM flag tells Docker that once this container process exits so once we
exit the container uh it should not store that stopped container it should actually remove that from our system so
if we run this it's going to go off and pull that Ubuntu 224 image then it starts the container and now we're
running a shell inside that container let's try to Ping google.com from within the
container as you can see the pin command is not found it's not installed by default within that UB 2 image to
install it we can use the APT package manager we can do apt update that'll go off and fetch all the metadata
associated with uh the APT package manager and then we can do AP install IP utils
ping I'll say yes and so now with ping installed we should be able to call Ping
google.com uh one time that's the- C1 it's successfully pinged Google uh and that's great now I'm going to exit this
container and if we want to take a look at the container ID here d61 we'll get a different container ID the next time we
create create one so now I'm going to rerun that same command we're going to create a new container from that same
image and you see we have a different container ID this container doesn't know anything about that previous one and so
because we installed that pin command within the readr container image of the other container it was blown away
completely when we exited and Docker removed that container so now that we're in this container again if we try to do
ping ping is not found however let's change this experim ment just a little bit so I'll exit this
container and now I'm going to run the same command but I'm going to add I'm going to remove this dsrm flag so Docker
is not going to throw away the container after it stops and then I'm going to add a name option and so I'll name it my
Ubuntu container and now we have a shell inside that container again we're going to do
apt update and apt install and that will get our ping utility installed within the
container okay so now with ping installed we should be able to use it great and so now when I exit because I
didn't have that D- remove flag the container still exists on my system if I do a Docker PS that will list containers
but because it's stopped it doesn't show by default so I'll do a dash a and we can see my Ubuntu container was running
it was created 41 seconds ago it exited 13 seconds ago but it should this container even though it's in its stop
State still has that read write layer associated with it uh so I can uh now restart my container with the docker
start command so I'll do Docker start and pass it my container
name and then I can use the docker attach command to attach to the Shell running inside that container so now I'm
back inside the container and I can ping Google and it works successfully so that just shows we created a
container we installed the dependency in that container layer we stopped the container but did not delete it and then
we were able to jump back in that same container and that dependency existed again now normally we would never want
to jump into a container and modify things like this on the Fly the better approach and the approach that we'll
want to use for a dependency like this is we want this to always be there when we start a container from that
particular image so instead of starting the container and resisting it uh in the container itself instead we want to
build our own image containing that dependency and so we're going to do a much deeper dive on building containers
and optimizing those builds in module 6 but we can write a very simple Docker file here and pass it at the command
line so this is just saying start from that Ubuntu container image run the APT update apt install and that passes it to
the docker build command and then we're going to tag it with this tag my Ubuntu image so if I run this command
Docker is going to build a a brand new image based on the original but now this new image is going to have IP utils
installed out of the box if I want to run a container based on the new image I'll use the tag
specified in the build command and now I don't have to install ping because it's already installed so
now I can run ping Google and it succeeds so hopefully that gives you a better idea of how this container file
system works and the fact that if we want some data likely in the form of a dependency or configuration to be
available at runtime every time we want to build that into the image we don't want to start an image and then make
modifications to get it to the state we want and so that's the that's the case of data that we know about at build time
and can build into the image directly the Second Use case that I described was Data produced by the application at
runtime uh and so like I mentioned this could be data that's going into a database let's do some experimentation
to build up some intuition about this type of data again I'm going to create a container from that auntu image now this
time I've shortened the-- interactive D- TTY uh you get the same thing in a shorthand version with just - it and so
here we have a shell running in that container to simulate uh data created at runtime I'm going to make a directory
called my data and then I'm going to Echo hello from the container into a file within that directory and so
now if I C my data hello text we can see hello from the container stored in a file on the container file
system if I exit the container run a new container from the same image what do we expect if I try to
do cat my data hello text of course that file doesn't exist just like when we installed something into the
file system it's in that top readr layer that gets Blown Away away with the container but now we can no longer build
this into the image because we don't know what this file should say ahead of time so instead we're going to use a
volume I can run Docker volume create my volume and that creates a volume and now I can modify my run command such that I
mount the volume into the file system of that container so I've added this-- Mount
flag it's using the source of my volume which I just created and it's using a
destination of my data root SL myy dat now I'm going to do exactly what I did before I'll do make my
data oh it already exists because I mounted that volume that directory was already created so I'll Echo just like
before hello from container into a file within that directory I'll C it
our data exists within that file system but if I exit and then recreate a new container
also mounting that same volume we can go into my data and see that hello Tex has persisted across that
container destruction uh so the data was stored in the volume even though we stopped the container and Docker removed
it from the system the volume is treated separately and so we were able to persist the data across that container
boundary now you might be wondering where where does this file actually live um it has to live somewhere on disk and
in this case because I'm on Mac OS it lives within that virtual machine that Docker is managing um so it's if I was
on Linux it would just be directly at VAR lib Docker volumes because I'm in Mac OS I don't have VAR Li Docker
volumes that lives within the virtual machine one way that we can take a look at that is to run this privileged
container so this is a Nifty trick that I saw online where I can run this command and it will essentially give me
a shell into that virtual machine that Docker is running and now I can navigate to VAR
lib Docker volumes I see a whole bunch of volumes the one I care about is the one that I
named my volume now if I go into the data directory if I go into the data
directory and I see my hello. text and so this is showing in that virtual machine file system where that
volume actually lives now this was just a trivial example where I was creating that file
and wanted it to persist let's say we were running a database like we were running postgres before we would want to
use the same approach with a volume and mount it into the file system of the container where postgres was going to
store its data and so by default postgres stores atar lib postgres data and so we would want to modify our
Command to use that mounting syntax uh there's an alternative syntax with the- v that does the same thing and this
would allow us to store the databased data in a volume and manage that separately from the
container now I mentioned that volume mounts were one option and bind mounts were the other let's explore what a bind
mount looks like for the same use case now I'm going to take my command that I used before where I had a
volume Mount and change the type to bind so now the type is bind my source is my present working directory and in
particular the my data directory within my present working directory and so let's just list out to show I have a my
data directory here within there there's already hello. text let's remove
that so now there's nothing in my data and if I run this command I'm going to mount that directory from my host
file system at the present work directory SL myy data and the destination is going to
be rooty data on the container so let's Echo something in here blah
blah that's going to go into hello. text so inside the container we have blah blah within hello. text if I exit
that container I'm now on my host system and if I cat my data hello. text we can see that file was created and stored at
the path that I specified I can also modify the host data and that will be reflected inside of a container mounted
in the same fashion so I can do I'll add another blah okay so it's three BLS outside the
container I'll now recreate my container it'll actually be a new container but still mounted at that same path and if I
do cat my data hello. text we see the updated version reflected inside that container and so as you can see bind
mounts just like volumes are a mechanism to persist data beyond the container life cycle uh the volume Mount is kind
of tucked away inside the the file system that Docker is managing the bind Mount is located on my local
system by default Docker suggest using volumes they're slightly easier to manage however if you want easier
visibility into the data itself it can be convenient to use a bind Mount just be aware that you can suffer some
performance impacts if you're doing heavy read wres into the bind Mount location so my hope is that this has
helped to build your intuition around how data is handled within containers uh both sort of the default case then the
case where we want dependencies available in the container when it's started and then the third case where
data is generated at runtime and sort of how we should manage each of those either by building those data into the
image or by mounting a volume or a bind Mount uh to capture those data beyond the life cycle of the
container the other thing that I've called out within this read me that I think you'll find useful are specific
use cases for leveraging thirdparty containers directly and what by that I mean not needing to build our own
container image the first one is databases so databases are always a pain to deal with sometimes you need one
version sometimes you need another sometimes you'll have multiple databases that you'll want to interact with for
different projects Docker provides a super useful mechanism for spinning up a new container with a different database
that you can use and have that completely separate from one application to another from one version to another
uh very very easily I think the key considerations are to always use a volume to persist the data um if you
have additional configuration that you need so let's say there's a config file that you need to have in the container
when it's starting up to to in order to set some specific configuration you can use a bind mount to mount that from your
host file system into the container and then finally you'll want to set the necessary environment variables so a
number of the the official database images use environment variables to configure things like the root password
within the container so earlier when we started that postgres container we had to set the the postgis password
environment variable uh there's a number of things like that with these database container
images I've laid out a number of a number of different databases and I've tried to capture the specific volume
mount and environment variables that you might need to use so we've seen postres before um here I've added that volume
amount for the location where the data gets stored uh if you had a custom postr SQL comp file and didn't want to use the
default you would modify this command to use this bind Mount so you would have this on your local file system and then
you would pass the- C flag uh to The Container when you when you run it and pass it a path to that flag flag so this
is as you can see where we mounted inside the container this is that same path and that tells the container upon
startup to look there for the configuration file so mongodb the official image it looks quite similar as
you can see it's expecting to save the data to SL dat DB so we're we're putting that into a volume uh we're setting the
initial root username and password with environment variables and we're publishing the default Port of
277 again if we have a specific configuration file we need to pass in we're going to bind Mount that into the
container and then we're going to pass a configuration flag to tell where to look for
that I'll let you look at the rest of these on your own time but I've I've laid out the commands for redus MySQL
elastic search and neo4j now the Second Use case that I think using third party containers is
super useful for is when you need an interactive test environment for something other than what you're running
uh on your daily on your laptop so I'm running Mac OS but sometimes I need to test out commands in auntu or Debian or
or Alpine and so being able to run this Quick Command and have a interactive shell within that specific Linux flavor
is really Priceless like I can test out commands and iterate on them so much more quickly if I'm writing a script
that's eventually going to be run in that environment I'll just create a container using one of these commands
and then I have an interactive environment where I can test things figure out the exact syntax use the help
commands Etc inter inter active test environments for operating systems or for programming runtimes uh we can do
something very similar but instead of having a shell in the OS we'll have a a repple in our command line with a
particular version of that programming runtime and so if we're if we're testing something that needs python 3111 versus
3110 uh or some minor tweak it's so easy using Docker to get an environment that has the exact versions that we care
about for doing that type of test now the final sort of use case that I find super helpful for using thirdparty
containers are command line utilities sometimes you just won't have it installed or you don't have the right
version and so people have bundled up these utilities and because so many Unix tools are designed to pass data as text
from one to another so we can pipe data from one command to the next um we could pass data into these containers and get
the output as a stream uh we can use these without actually having them installed on our system so
JQ is a utility for working with Json data and so for example uh if I run this command so if I jump back
here and we look at the sample data I have here so let's say I have this test. Json it has key1 value one key2 value
to if I run this command it's going to take that test Json data file pass it into this JQ image and
perform this command so it's going to find the value located at key1 and combine it with the value located at
key2 and so in this case I get value one value two as a string and that's the output and so without having JQ
installed on my system I'm still able to use that utility directly with Docker yq is similar to JQ but is for yaml there's
also command line interfaces from cloud providers such as the AWS command line or the Google Cloud platform command
line uh and you can run those in container images uh you will need to mount in your credentials so by
default we know that the container is not going to have access to the host file system and so if your credentials
are stored at let's say the AWS directory within your home directory or the config directory within your home
directory you'll need to craft the the correct volume the correct bind Mount commands to get those credentials uh
into the CLI but once you do that it allows you to use different versions of the C very easily and so if there was a
breaking change from one version to the next you can leverage that previous version without having to reinstall and
modify things if you've already upgraded now this sounds cool but you might be thinking I don't want to have to type
out Docker run blah blah blah with all these bind mounts Etc and so so one way that we can improve the ergonomics of
this quite a bit is to use two concepts one is a shell function so we can write a function like this that will take this
command and essentially invoke it when we run this function we also can use an alias to do the same thing to to close
out this section I wanted to pull up a talk that was given by Jesse frel um I think this was in 2015 so this is quite
a few years ago uh Docker was much younger in its maturity however she was an early adopter and employee at Docker
and used to run all sorts of things uh within Docker and so I think this is a talk that is worth watching for sure and
just sort of shows that you can pretty much run anything in containers giving it a watch is really interesting and
inspiring and helps to understand sort of this mindset of I don't necessarily need to install everything on my host
system she goes a little further than most with this concept uh but I think it it's still eye opening to see the number
and types of things that she's able to run in containers so with that we've reached the end of the first portion of
the course in which we learned the history and motivation of Docker about the underlying Technologies we installed
and configured Docker such that we're now able to run containers and interact with them and then we saw some use cases
for leveraging thirdparty container images for different things throughout our development life cycle at this point
we're going to shift gears and start building building our own containerized applications I think it's important to
realize that learning Docker in a vacuum doesn't actually provide much value and so instead we want to have some
realistic web application with which we can apply this technology in this module I'm going to walk through a sample
application that I've built for exactly that purpose it's a minimal three- tier web application and by three tiers we
have the front the front end or the client that's built in react we have an an API layer application programming
interface layer we have one API built in node.js so that's an interpreted language one built
in goang a compiled language we have both of those they're almost identical but I wanted to show two different
languages with two different developer tool chains so that we can showcase how Docker handles those slightly
differently and then we're going to have a postgres database I've tried to keep each of these components as simple as
possible while still being representative of the type of application that you might see in a
modern web technology company today the implementation of each of them will be quite minimal but how we configure
things like networking and passing credentials will be realistic to an application at a web technology compy at
this point I'll jump over to my code editor and walk through this code at a very high level I'll point out though
because this course is focused on Docker and containers and not on web application programming I'll keep this
relatively high level just to give you an idea of the things that the code is actually doing and highlighting the
configuration surface area where Docker is going to interface with them at this point if if you haven't already you'll
want to go to the companion repo for this course I'll have a link for it down in the description uh and you can find
all the source code that we'll be going through and building out uh and what you'll want to do is here under code uh
you can either click here and clone it to your system but even better is if you Fork it because that way you can make
modifications commit those modifications and if you have any improvements you'd like to make uh and propose back to the
course for others to use you could create a poll request back to my copy of this repo on get Hub and that would be
awesome while you're here if you don't mind giving it a star that would be super helpful to help the project grow
and while I'm asking for your support you may as well hit the thumbs up button on the video as well and so with that
let's jump over to the text editor and see this demo application so I've got the code editor up here uh and we're in
module 5 the example web application I'm going to start by running the database just like we did in module 4 I'm going
to run the database with a Docker run command I'm going to pass it a postgress password environment variable I'm going
to use a volume Mount like we talked about to persist the data outside of the container in this volume so it can live
beyond the life cycle of this particular container and then I'm going to publish Port 5432 which is the default Port that
postgres runs on and I'm using postgres 15.1 uh and in particular the Alpine variant so this is a make file each of
these is called a make Target we can do make run post Gres and now we have that database
running in a container container I can do the docker PS command to list out the containers running and
we can see here's the container this is the image it was based on uh because I didn't specify a name it generates a
random one and we're publishing that Port so that I can connect from my local host at this point let's move and take a
look at the node API and get that running as well so it's located within this API
node uh subdirectory uh if we look in the package.json file we can see there are just a couple
of dependencies we have Express which is a package for building rest based apis we
have Morgan which helps us configure logging uh for that API a little more easily and then we have the PG package
or the postgres package uh which is going to allow us to create a client and connect to the
database so what I'm going to do here is make sure I'm running the right version of node so I have NVM installed
so I can do NVM LS it'll list out the different options that I have installed on my system I want 19.4 so I'll use nbm
use node 194 and then I'll do npm install it's going to grab those dependencies from
the package.json and install them in my system in this node modules folder if I wanted to run the
application I would do npn run Dev but I also need to pass it one environment variable and that environment variable
is the database URL so this is going to give the application the information it needs to connect to that post Chris
database running in the container and so again I have the same I have this command laid out here and so I'm just
going to do make run API node and now it's started up and listenting on Port 3000 let's just go
see how that application behaves if I load it up we see uh at the root path so I haven't specified any
path it Returns the current time stamp and I'll show it I'll show you how this is working behind the scenes but it's
just querying the database asking for the current time and then returning that and then I have the API node key and
value just to show this is the node based API we'll have an an almost identical response from the goang API
except this will say goang instead of node for each time we refresh the page uh
we're seeing there's a a 200 response that's a successful HTTP response uh for the request and then we have this 404
for the favicon so because I'm loading it from uh from a browser it's also trying to load that little icon that you
would see in the top left corner but it doesn't have one so that's the the 404 not found error that we're seeing
there now let me just look very briefly at the source code for this application to show you what it's doing we've got
two primary files index.js is the main one we're importing some dependencies here at the top we're setting the port
number that we're listening on defaulting to Port 3000 uh we're setting up some logging and then here's where we
Define that API endpoint where we call our get date time function which I'll show you here in a minute uh and then we
populate the uh node we populate the API key within that Json object as well uh in our database do our db. JS file uh is
where we have that function uh it's where we're going to load in our connection our credentials so that we
can connect to the database instantiate a new uh pool which will be that database client uh and then make the
necessary call to the database to get the time that then we we can return on that
endpoint and so that's pretty much all there is to this node application like I said I want to keep this as simple as
possible while still highlighting the types of networking and configurations that they would we would need in a real
world application uh at this point with the node application running we can jump uh
we can move on to the the goang application and so for this one it's located at the subdirectory API
goang uh here I am within that directory uh to install the dependencies I would do go Mod download uh first I'm going to
set the go path just to tell it where to download those modules to to set a workspace um I've got this go workspace
subdirectory uh I can export go path and that will point to that subdirectory now if I do go Mod
download it's going to look for the dependencies that are in my go mod file and so the two dependencies that I've
added here are Jin which is a an API framework work uh and PGX which is a postgress client and so that go Mod
download looked in go mod brought those into my workpace okay so now we should have all of our necessary uh
dependencies uh and just for reference I'm using uh go19 and so to execute this program I
would do go run and then point it to the the main.go file uh that's going to both build the application and run that built
binary um again just like the uh node application I either need to pass it an environment variable with the database
URL or pass it a file containing those credentials um I have a command in my make file that does just that uh so I
can do make run API goang we see it passes that database URL in as an environment VAR variable
and then executes our go L and then does gun main.go uh that's running on port 8080 so I can go here and reload this
and very similar response to the no l to the node.js one uh we have API goang and then it's grabbing that time stamp from
the database each time I refresh it gets an updated time and so just very briefly uh I can
take a look and show you the implementation here I'll pull open the the main file uh as you can see we
import our dependencies here at the top uh we then have our init function uh within there we're loading in the
credentials that we need to to connect to the database once we do that uh we pass those credentials uh to this init
DB function which is defined in the database module that I'll pull up here in a minute and then within our main
function we have our endpoint defined where we uh call our database. getet time function and take the results of
that and we've returned that Json object using Jin I'll just pop open the database module here just to show you
the two functions there um we've got our init DB function where we're taking our connection string uh with those
credentials uh and initiating a client uh and then I have this get time function which just like in the node
implementation is calling select now is now getting those data back and and returning that uh to the
caller and so that's really all there is to this application like I said almost identical to node we just want to have
both of these different languages so we can show how to manage the different tool chains how we're going to build the
docker container images and how we're going to create containers from those images now with the goang API running
the final piece of the puzzle is going to be the react client uh so that implementation is here in the client
react subdirectory uh once again we can look at the package.json uh we have a handful of dependen we have react and
react Dom those are installed by vit when I created this project I used V to do so and then I added axios as well as
this react query package which just enabled me to very easily call my apis get those data and feed that data into
my react uh components uh so I'll want to once again just make sure I'm using the right
version of node I want 19.4 great I'll do an npm uh make sure I'm in the right directory
then I'll do npm install this goes off and downloads uh all those dependencies brings them into
my my local um node modules folder and then to run it uh I'll just do npm run
Dev and by default uh V is going to run that development server on Port 5173 so if I load the page here
uh we see the result it's a react app it calls both of those apis gets the data and returns them each time I refresh you
can see that the the time stamps are updating and so that's doing exactly what we wanted to do once again I can
give you a quick look at the source code just so I can show you what it's doing all the logic pretty much is in
this app.jsx file we're pulling in our dependencies at the top I have this function based component called current
time that uses the use qu method and axios to make a get call to the uh API that's specified and so this enables me
to use the same component for both those apis by passing the API in as a prop I then get the data back and can use that
within my component then down here this is the actual content that's going to be
displayed I just have which API it's coming from from data. API and then the time stamp that was returned in data.
now while it's still fetching it will just it will have this placeholder up dating text here at the bottom is the
content of our app itself uh we wrap it in this query client provider uh that just enables us to use uh the
functionality within uh that react query package we've got our static header and then two instances of the current time
component uh one where we're passing it the goang path one where we're passing it the node path it is important to call
out here that within my V.C config I have these proxies set up such that API goang is sent to Local Host 880 where
the goang application is running and then API node is passed to uh Local Host 3000 where the node API is running
that's pretty much it for this configuration hopefully that didn't seem too overwhelming uh it's quite complex
trying to run all of these Services independently and get them all talking to each other that's kind of the whole
point of this course right I want to avoid having to set up each of these Services separately and avoid the
complexity of needing to match the right version on your system versus my system and Mac versus Linux versus Windows
docker's going to help us simplify this dramatically and so as we progress through the rest of the course we're
going to take these components containerize them and then learn how to run those containers how to configure
them properly and how to set up a development environment such that we can very easily go from zero to rapidly
developing and iterating on our applications all within a containerized setup and what I really tried to do with
this application is have each component be the absolute minimum complexity while still giving us that application the
networking the configuration be representative of what you would see in a modern-day web technology company and
so ideally don't get bogged down with any of the particular applications themselves but just try to understand
here is the core of what it's doing we have our client on the front end it's calling those two apis so we need the
networking to be set up properly there those two apis instantiating clients and connecting to that database and making a
database call so we need to handle those credentials properly as well as the networking between them and just
generally we need to have our environment set up for each of these applications to run as
desired now that we have our example application we can go through the process of containerizing it now what
that means is that we're going to create a specification and then we're going to build from that specification our
container image the specification is what is known as a Docker file so it's a text document
that's going to have all the commands that we need to create and assemble this image you can think of it almost like a
recipe for your application you're going to start with some base layer like an operating system you install your
language runtime and any dependencies and tooling associated with it then you would install your application
dependencies you might set up your execution environment with certain environment variables and then finally
at the very end you're going to run some command that starts your application now this Docker file is paired with what is
called a build context we write all of our instructions in the docker file and then the build context is generally a
folder or a directory on your local system containing all of your source code it can also be a URL so you can
pass uh like a GitHub a public GitHub repo as a build context and we take these two things together and run the
docker build command and with those information Docker is able to go off follow the instructions in the docker
file and produce a container image I'll call out one additional file that's important to to know about and that's a
Docker ignore file within your build context you can include this dot Docker ignore file as the name suggests you can
tell Docker to ignore certain files so let's say we've installed the node modules locally we definitely don't want
to copy those into the Container image because we're going to install them within the docker file and we could have
incompatibilities from the installation on our host system to that within the container and so with this as sort of
the highle framework let's go ahead and start writing some Docker files for our example application as we go about
writing our Docker files this page within the docker documentation under docs. do.com engine reference Builder
lays out all of the valid commands that you can use within a Docker file the format is actually relatively simple if
we use a hash that's a comment uh we'll there's a Convention of using all caps for the instruction and then we'll pass
arguments after that instruction so for example we can have a comment saying what this step is doing then we can use
the Run command which is going to run this during the build phase and then Echo something out if we scroll down
here on the right you can see all the different options that you can pass with the Run
command as well as all the other commands that are valid we're going to use a number of these and I'll try to
call out all the ones that you're going to need to know along the way so let me just jump into the code
editor and I will create a Docker file in my API node subdirectory and you might notice that
uh I have the 05 module as well as the 06 module I have a whole bunch of Docker files here that show kind of the
sequence of events that we're going to go through uh but just for purposes of of demonstrating building it out I'm
going to put it in the 05 directory and generally you would want to do this You' want to put your Docker
file in the place where your code is living let's think about what that recipe might be for my node.js based
application I need to start with some uh Linux distribution and so so I'm let's just use Ubuntu so I'll use the from
instruction and then I'll specify an image name so Ubuntu uh we also then are going to need
to install the nodejs runtime uh and I can use the APT package manager to do that so I use the Run instruction which
is going to run this command uh during the build phase I'll do apt update uh that'll grab all the latest
metadata from the package manager so that I can find the correct package and then I'll do run apt install
nodejs um and let's just execute the build now just to make sure these steps are working properly I'll navigate to
API node and then to build this I can just do Docker build with a period I didn't
specify a name of a Docker file because it's the default is to use Docker file with no extension I pass my current
directory of period as my context uh so it looks like uh it failed because that apt install
command uh is interactive and so I need to pass it a Das Das a-y and that'll override that question and automatically
install so let's try building it again now it looks like it's installing nodejs and you can see each of our steps
we started one of three we we pulled that Ubuntu Public Image then step two of three we ran that run apt update and
then in three of three we installed node.js and so if we think back to that union-based file system each command in
this Docker file is adding a new layer to that system and so often times we want to minimize the number of layers
and think about because Docker is caching Those Behind the Scenes how we can avoid invalidating the cache one
such method that we can do to improve this already uh would be to run these on the same line so rather than
rather than doing the APT update and then the APT install we can combine them into a single command
here and so now those two actions will get combined on that single layer okay great uh so now how are we
going to get our application code into this container image and for that we're going to use the copy command so this
allows us to copy uh either from our host system uh or later we'll see how to copy from another container image
during the build and so I'll copy and I'm just going to copy my entire current directory into the image we're going to
improve upon this later but uh but this is just a simple way to do it for now and so with that we'll have our source
code within the container then I can run the npm install and that will go off and grab
all of my dependencies so let's just make sure this still builds okay so npm was not installed so
we also need to install npm okay and now that we had npm installed in the system we're able to
execute our npm install command and then the final step that we need to do for the stalker file is to
add a command that will be executed at runtime when we create a container the format for this is we'll have an array
of strings and so in this case I'll do npm run Dev and that would run my Dev script
from the package.json file uh when we started up so I'll build it once more and one thing to note here is that
I changed something in my Docker in my Docker context and now when I'm rebuilding it it's executing run npm
install and that's because this copy step came before the npm install and so making that modification actually
invalidated the cache and so it was much slower to build because we had to run npm install and
download all those dependencies we'll address that in a future Improvement now in order to be able to
execute this this Docker build C this Docker build command builds it but doesn't
actually tag it with any name and so I could run this I could reference it with this shot directly or I can add a-t and
I'll call this API node and call it version zero now I can do Docker image list and
we can see API node version zero was built a minute ago and look it's almost a gigabyte in
size now this Docker file while functional is terrible uh there are so many ways upon which we can improve it
and now let's go through and think about what those are first of all starting from a to and then installing uh node
and npm while we can do that there are a bunch of official images on dockerhub for language run times so if I go to
dockerhub and SE search node it'll pull up the official image and so this is a Docker official image it has that tag
it's been downloaded over a billion times and has over 10,000 Stars uh and they maintain all of these different
tags for different variants and so if I do this I can remove this install command and instead go straight from
node and so let me build that again as version one and so for any runtime any language
runtime there's probably an official image that will maintain all the different versions you need and have
things preconfigured out of the box so usually you can start from node or from python or from goang and we'll see that
when we go to the goang API as well now you'll also notice that I don't have a tag specified here I have the
image name but no tag and what that'll do behind the scenes is it just assumes that I mean uh node latest um that's a
problem because that node latest tag can change based on the images that that the maintainers of that image have pushed
most recently and so rather than tag it with latest it's much better
practice to specify an exact version um and so what I'm going to do is look here here at the versions uh Alpine is a
Linux distribution is quite small and therefore good for building Docker images because as we're thinking about
final image size That Base image that we're building from that's as small as we can get and so it's really important
to choose a small base image it's also good from a security perspective to have fewer utilities and dependencies
installed from the Geto and so instead of using node with no tag or node latest I'm going to use node 19.6 Alpine
and so I'll build that once again and I'll call that one API node 2 and just to show you what a huge
difference that makes in terms of image size we can do Docker image list and scroll up here and we see node zero was
the iunu based one uh this tag number one was the just node latest version so that's over a gigabyte and then and by
using that Alpine base image uh We've cut our image size by a factor of five and we're down to 200 megabytes however
you will noticed that each time I'm building it's taking quite a while and the reason for that is I'm copying in my
entire Source directory before I do the npm install so I keep invalidating the cache and so the way that we can get
around that is that rather than copi the entire Source directory right away uh we can first copy in only
the package.json and package lock. Json files and then do npm install and after that copy the remainder of our source
code by doing it this way this layer will only be invalidated if we change our dependencies and so I'll call that
uh tag number three it's also best practice to specify what's known as a working directory within the image
that's the default directory in the file system where commands in the build process are going to be run or the
command at the end that we're running is going to be executed when we instantiate the container uh we do that using the
work dur instruction and this is going to depend on the convention of your language or
framework that you're using uh in this case for node apps I like to use user Source app that's not really going to
change anything about our system except that now within the image instead of copying all of our code to the root
directory and installing the node modules there it's going to copy things to this path which just keeps things a
bit more organized also I have things within my API node directory that I don't want
to be copied into this image um there's two things that I can do to help avoid that one I can add this Docker ignore
file and in this case I'm specifying the node modules directory so I don't Mo I don't want the node modules that I've
installed on my Mac OS system to be copied in because that would cause conflicts and so by putting it in this
doer ignore file those will not get copied in I also can specify let's say I only want to copy in my source code
directory I don't need to copy in the remainder of this stuff into the image and so I can do that here I'm only
copying the source code directory into the Container image let's build it once more we'll
call this four now because this is eventually going to be a production container image
I don't want to be running npm run Dev here instead I want to run node and then point it to my index.js file and so
rather than execute this npm script I will just say no node and then index.js and so that index.js file on my
host system is in the S Source subdirectory but because I'm copying from source to my working directory it
will be directly it will be in that working directory and I can just call it that way so I'll call that one five and
now most of the improvements that we've been making so far either have to do with image size build speed uh Etc
another key aspect that we can influence by changing and improving our Docker file is the security of our container
and so one very useful technique is to execute as a non-root user and so this nodejs official image happens to have a
user defined in it called node and so by by using the user argument and passing it the name node it
will use that user now that I've done that though I want to make sure that the files I'm copying in here are owned by
that node user so I can use the CH own command node node and now all those source files being copied in will be
owned by that user and be accessible by that user so I will copy this tag this as
number six now many nodejs applications look for an environment variable called node EnV and so we can use the EnV
argument or instruction uh and set node n and we'll set that equal to production if it's set to production the
application or the package will behave one way if it is unset or set to anything other than production it will
run in a development mode and we don't want that for a production image um so by specifying this environment variable
here that can be very useful I'm also going to change from an npm install command uh to an npm CI command and I'm
going to add the uh only equals production option and so these two things together
the the npm clean install is what CI stands for uh will look for the specific versions from my package. loock file and
install exactly those versions so it gives a much more repeatable build and the D- only production option says don't
in don't install my Dev dependencies so I have things like just and nodemon in my Dev dependencies within the
package.json since I don't want those or need those in my final production image uh I can use this-- only flag uh to
achieve that so let me call this number seven and now I know as the person who wrote this app that it is expecting to
run either on an environment variable named Port that it loads or it will default to uh Port 3000 I'll add a
command called expose uh and just say 3,000 and so this doesn't actually change anything about how the container
image behaves or the container this is just documentation for users of this image that it is expecting to run on
Port 3000 that way they know to publish that port to The Host or to whatever system they need to so that they can get
traffic to the app and now there's one additional place where I can make improvements and that's in this step and
this again is going to be to improve our build speeds there's the option to mount a cache location to store information
from this step specifically so I'm adding this Mount type cache with a
target of this location uh and then I'm setting I'm telling npm to use that location to store its cash dependencies
um before calling my npm C call and what that does is if previously if I invalidated the layer 7 it would
download all of my dependencies from the internet from npm and then install them now if I've run that once before on my
system it will only have to download the new dependencies from the internet it can use that local cache to install the
rest of them from that cache avoiding an extra download and just speeding up the process and so I'll call that API node
8 and so with that we have a Docker file that I'm pretty happy with we took that initial naive implementation and
improved it along so many dimensions both from repeatability size of the image avoiding
invalidating the cache so that F future builds are faster documentation around ports and this the fact that this is a
production image using the production command that makes the most sense and using a non-root user within the
container now let's take a look at sort of the image sizes as we progress there I'll grip for API
node and we can see we started off with those huge images uh when we move to the Alpine based node image as our as our
base it cut it by a factor of five uh I believe this small jump was when we when we added the Das Das only production
option to our npm install command so there were 30 megab of Dev dependencies that were previously getting installed
and then the the change we made there at the end where we set the cach for the npm system uh saved us a couple of
megabytes so huge improvements along size security Clarity and this is a Docker file that that I would be happy
to use now if you look in the repo on GitHub you won't actually see a Docker file in module 5 that's because I've
included them here in module 6 and I've got each of the sequential steps that we took ining it with comments describing
the commands that we built up along the way now that we have a container a Docker file and a container image uh for
the nodejs API that we're happy with let's go ahead and shift gears into the goang API and build one out for that now
similarly to with node I could start from auntu but instead I'm going to use the official goang
image and because we're doing it the naive way we're not going to tag it we're going to use that latest uh we're
going to use that latest one and then we'll come back and improve just to see how much to see how much that saves in
our final image size but yeah we can start from that golang image and so I can build it with just
that from command this image would be identical to the the Upstream base image at that point like with node we want to
set a working directory uh in this case I'm just going to set it as app we need to get our source code into
the image so again we use the copy command let's just copy it all we're going for the the simple naive
implementation first and then we're going to improve upon it to get the dependencies installed uh we'll use the
Run command with go Mod download so that will look at our go mod file and download those dependencies and finally
we can set our Command and for this I'll use go run and then point it at my main.go
file uh so this should be a a functional Docker file and there we go let's do a a
Docker image LS and we'll GP for API goang okay again over 900 megabytes over the set of improvements that we're going
to make we'll improve that quite a bit I will add uh tag zero to that one just so we
can remember it we never want to use an untagged or latest image and so we'll add the 1.19 Aline tag and then we can
rebuild with that and so already just that has cut our image size in half which is great but we're not going to
stop there just like how with node we didn't want to run our npm run Dev script because that's a development
specific thing uh here go run actually builds and runs our application and rather than doing that we would much
prefer to build our application in the docker file and then at runtime we can just call that built binding
and so instead add a run command I will do go build- o API go langang that's going to create a binary called API
goang from our main.go file and then rather than use this as our Command we can just run API go
langang we'll call this number two this change means that every time we start up our container application we no
longer need to build it because it's already built so we're just shaving off time that it would take in that
container bootup process at this point I'll also point out the docker ignore file that I'm using for this uh project
I've added the make file the readme and uh any Docker file that I create to the docker ignore those are files that are
that do not need to be in the image and so this prevents uh when I make that copy command from invalidating the cache
uh each time I change the docker file each time I add something to the readme ETC now similar to uh node we don't want
to invalidate the cache here before we download the dependencies because then we'll end up downloading those
dependencies way more often than we need to and so I'm going to move this copy command
after and instead we can just copy the files that contain the references to our dependencies and so that's the go mod
file uh and the go. suum file and we'll copy those into the uh current directory I'll call that number
three with that change we can make changes to our source code without invalidating the cache before
downloading dependencies and so each build would be much quicker and now at this point we're
going to add a new technique that I haven't shown yet and that's something called multi-stage builds and these can
be super powerful because it allows us to have within a single Docker file multiple individual stages that are
building separate container images now for a language like goang that is compiled this is super helpful because
we need a lot of dependencies in the build stage where we're using them to build our code however the end result is
this binary that can have all of its dependencies statically linked we don't need any of that golang tool chain in
the final image I'm going to label this stage as build and then I can add a new stage down here and in this case I can
use a special image called scratch and this is a official image that we can look at here let me just go
to Docker Hub uh it's a Docker official image it really has nothing in it the only thing
that it has in it is a valid root file system it's effectively a noop that gives us a place to start from that we
can copy our binary into and so now since we're starting from that scratch image we can use a copy command but in
this time instead of copying from the host we're going to copy with a D- from equals build and so now this is saying
copy from this container image that was built in that first stage and now we want to copy the slapp Slash
API goang and we need to use SL apppp because that was our working directory
that where that binary ended up uh we want to copy that to I'll just copy it to API
goang and so it will be at the uh root path and then we can call it from our uh we can call that when we
instantiate a container from this image uh however that wouldn't work as is because this build command didn't
statically compile all those dependencies in so I need to modify that build
command and I can do so like this so I've added these uh LD Flags to change the link mode to external uh and
set the compilation to static I've also added this netgo tag I found that I needed to add that to make my networking
within the image work properly however by doing that I now get this compiled binary that has all of those
dependencies built in and I can run it from the scratch image so if I do this and build it I'll call this one
four o but when I built it uh I ran into an issue where my build stage didn't have the compiler required to do that
static linking uh and so one way that I can fix this is to use Buster which is a dban based distribution my Alpine image
while it was smaller that doesn't matter as much anymore because this build stage is separate from my Deployable stage I'm
going to end up using this image to deploy and so even if this is slightly larger in my build stage uh my
Deployable stage should still be quite a bit smaller so let me try building that again and so that can be a trick when
you're using multi-stage Docker files is you care a little bit less about the size of your intermediate stages and so
you can use an image that has more build tools available uh or slightly easier to work with and so in this case we're
using that deian based image in the build stage and then we're still getting the benefits of that tiny scratch uh
stage for the output so if I look at the image sizes here we've now jumped from 500 megabytes to that statically
compiled one is only 16.8 megabytes so huge benefit by using that multi-stage build compiling to a static binary and
then copying that into a separate stage a couple things I want to add to set up my production environment uh and that is
environment variable for Jin which is the API framework that I'm using and so I'll do gin mode and set that to release
so similar to node end production Jin behaves differently whether or not this environment variable is set and then I
know that this app is going to run on port 8080 and so I'll set expose 880 there as
documentation uh for the end user I'll build this as tag number five and then the final Improvement that
I'm going to make is similar to the cache Mount that I use for npm but we can do the same thing for our go Mod
download command uh and so I can add a cache Mount here um this slgo package mod is and
the root cach go build are the two directories where uh those dependencies get cached as they're being downloaded
and installed and so adding this can just speed up subsequent builds accordingly and so as my final image
I'll call this six I guess one final thing that we can do from a security perspective like our node based API we
would prefer to use a non-root user within the side of the container and so to do that we're going to need to set up
that user uh in our build stage and then copy a couple of files into our release stage that will allow us to use
it I can do that with the uh run user ad command uh and I'll do user ID 1000 and I'm calling it nonroot then in my
release stage I will need to copy the password file from my build stage so I can do copy from build SL Etsy password
that will copy that in and then I can do user non root and that will use that
user we'll call that number seven now in this case whether that was necessary or not is a little bit
questionable uh because we only have our goang binary that's the only thing pretty much in that scratch image the
tax surface area is quite small and so maybe it's over the top to add this nonroot user but I just wanted to
highlight the fact that we can do it and it provides one additional layer of protection uh to our sort of Defense in
depth approach uh when it comes to security and so with that we have a Docker file for our goang based API that
I'm relatively happy with again we've pinned our our base image uh we've used a non-root user we're avoiding
invalidating the cach when possible we're leveraging this Cash Mount to speed up future builds uh we're
compiling to a static binary and leveraging a multi-stage Docker file to really take advantage of a much smaller
image in the end uh and we're setting up our final environment with things like this environment variable um and the
expose command for the port that we plan to run on and so with this we can create a container for our API that's both
small and secure just like before if you take a look at the repo you won't find a Docker file in the module 5 instead I
have created a number of Docker files here in module 6 that walk through all the steps that we just went through and
have commentary around why we're doing the different things that we're doing so we've got two of our three applications
containerized we have the docker files created the final one that remains is our uh react client so let me navigate
to that and I will create a Docker [Applause] file uh and with this one the
development mode for a v based react app uh is going to be a node JS server serving those files and so you'll see
that our Docker file looks pretty much identical to the one that we had uh for the node based API so this would be a
valid Docker file for that again let me build it so I'll do Docker build- T we'll call it client
react zero we'll build that as always before that even finishes building I'm going to
add a pinned version of that base image for repeatability and image size uh in this case we're not going to we're going
to end up using a multi-stage build as well so I'm going to use the Adian based base image just cuz it's a little easier
to work with than Alpine sometimes and I'm not as worried about it since it's not going to end up in the final image
so I'll tag version one as the one with the the 194 Bullseye tag similarly our Docker ignore file is going to have the
node modules directory within it because we don't want to copy those into our our image two improvements that we saw with
the node API that we're going to make here as well is we don't want to invalidate the cache by copying in our
source every time instead we're going to copy in the package.json and package lock.
Json and then also let's set a working directory as sluser SL Source
slapp I'll tag that as version two uh changes like that don't actually modify the size that much of the final image
but it's just useful to see kind of the progression uh I'm going to use the same cache Mount
approach that we used before to speed up future installs I will use the npmc command
here I'm not going to use the Das only production flag because in this case it's our build stage where we're going
to generate some static files rather than the final image that we're going to deploy a container with and so you'll
see here our Command is still currently npm runev that's going to run a development server that's not what we
want what we actually want to do is run npm run build which is going to generate a set of static files HTML JavaScript
and CSS that then we're going to be able to serve so this will generate those files I need to name my first stage
because we're using that multi-stage build approach so this will be as build we're going to add a new stage
here at the bottom uh and we can use from engine
X and let's see what version of enginex we want to use uh let's use 1.22 D
[Applause] Alpine we're not going to need a command we're going to use the default engine X
command um but in order to make this work we need to get the files that we built in line 13 and copy them over into
this final release stage and so to do that uh we'll use the copy command we'll specify from build that's saying from
that top build stage look at the path user Source app so we have our work directory user Source app and then the
npm Run build command is going to place those files in a subdirectory called dist for
distribution uh and then this path is the location this is the convention for where you would store uh your website
files if you're going to serve them with enginex and so that's what we're saying there we need one more copy though and
that is to copy in our enginex config and so our enginex config is how we're going to set up
uh engine X to serve our site the way we want it to and so this is being copied from the host I have it here let me just
walk through this real quick we've got a few endpoints set up we've got a ping endpoint which just returns 200 um pong
directly I also have these two additional endpoints set up as proxies so if you go to/ apiol Lang that gets
proxied to this address um and if you go to SL API node it gets proxy to this address and we're going to set up Docker
when we run it uh to host those two apis at those paths uh within the network that we set up and then finally the root
path if you don't hit any of those you'll end up here it's going to serve our react app files from that directory
where I copied them in the final thing that I'll do is just to once again have an expose command this is just going to
be an HTTP server so we're exposing Port 80 uh that's where it's expected to listen all right uh let me build this as
three um and actually this engine X image the official one runs as root and so one way to improve that as always is
to run as a non-root user and so there is a uh another image that I found which should be pretty much identical to that
one um let me just search for it here this engine X Inc engine x unprivileged uh is a very popular one it has over 100
million poles uh it's very similar to the official enginex image with a few minor tweaks around uh the default
settings including the user that it is run as and so in this case I'm going to swap out uh from that image to this
unprivileged image and rebuild it once more as tag number
four and one of the key differences when you're running unprivileged is that you can no longer bind to Port 80 uh you can
no longer bind to any port below 1024 I believe and so rather than expose 80 the default here is going to be to run on
port 8080 uh and so I'll change that and with that we can build it once more and let's just
look at the image sizes as we progress so we can do instead of that we're looking for
client react uh we started with a massive 1.16 GB image uh then when we switched to the engine X image it
dropped to 23 megabytes uh the unprivileged engine X image isn't quite as optimized I guess so it's 76
megabytes but I think the Improvement of running as a non-root user is worth it for for that extra image size
as with the other projects uh within the module 6 I have a series of Docker files listed out here that show the
improvements along the way along with commentary and so feel free to look at them and see the differences between
each step why we're making the changes that we did now going through that process hopefully it was educational it
might have felt a little bit repetitive but that's kind of the point we want to find a set of general principles and
practices that we can apply to any Docker file for any container image that we're building and the general approach
that I think about is first we want to make it work that's kind of the fundamental thing that needs to happen
that's that naive base Docker file that just has a few lines gets it running then we want to think about two things
we want to how do we make this more secure how do we make it faster to build so it's easy to work with I've laid out
a legend here that talks about the three types of improvements that we're making whether it's impacting the security of
the image uh the speed with which we can build build it or the clarity of the image how clear it is for future
developers who are coming along and reading it and so for each of these techniques I've listed out with the
Emojis so that you can see what they're doing we pinned specific versions within our build this can impact security speed
and Clarity the base image is a huge one right if we pin a specific small secure base image that's a great starting point
we can either use a tag like I was using 1.19 Alpine you can also use the hash of the image itself to use it or a
combination of the two so you can have the tag and then an at signed and then shot 256 and then the hash that's
actually kind of The Best of Both Worlds where you have a human readable version as well as that immutable hash that will
correspond to a specific version that can't change you also want to pin specific system dependency so if we were
installing something from apt or a package manager we would want to install a specific version there so that it
doesn't change on us uh that can help with security as well as Clarity so you know exactly what is going into the
image uh and then application dependencies so within our package lock Json file uh within our go mod or go Su
files we want to install the exact depend every time so that we're not accidentally changing things from one
build to the next I mentioned this in the pinning of specific versions but it's worth calling out again you want to
think about what's the smallest and most secure base image that I can use that will still be nice to work with and I
can install my app into it that's going to help both from a security perspective as well as a speed perspective because
those smaller images are just easier to move around whenever we're adding a command to a Docker file we're adding
layers and we want to think about how do we protect that layer cache such that future builds can leverage as much cash
content as possible the general principle here is to order your commands by how often they're going to change so
that base image is very infrequently going to change we want to install our dependencies before we copy in our
source code so that we don't bust the cache there we want to use that cash Mount technique so that it can store
downloaded dependencies locally and reuse them in the future this next one is a a technique that I didn't actually
highlight in that process it's a relatively new feature it's an option to the copy in instruction uh and by using
copy-- link it creates a new separate layer that's not tied to the previous layers and this is super useful in the
multi-stage build environment if I'm copying from the previous stage I can use this such that if I change my base
image on my second stage that won't invalidate this the cache because this copy. link when I'm copying from that
previous stage it ties it to that layer stack instead and then the final step is to combine steps that are always going
to be executed together so that we have fewer total layers in our image we also wherever possible want to be
explicit within our Docker files just for clarity sake uh we want to do things like set that working directory so we
know where within the file system we're operating we want to indicate the standard Port that we're going to be
listening on with that expose command for clarity and we want to set the environment variables how we want them
with the end instruction so it'll be automatically set when we run that container we also want to avoid
unnecessary files in the container image we can do that in two ways we can add things to the docker ignore file so that
they don't get copied in we can also when we issue our copy command copy specific files or subdirectories rather
than just copying in the whole build context for security purposes it's always better to use a non-root user so
we saw how to do that in a few different cases uh we also want to only install production dependencies so if there's
certain things you only need in your development environment there's no need to build those into the Container image
uh we always want to avoid leaking sensitive information uh and so good example of this that I'll show here in a
minute is if you have a private repository that you need to log into you could have private npm packages maybe
you're building a python app and you have and you have a private python registry we'll need to pass those
credentials in a specific way so they don't get built into a layer within our container image and then the final one
here is just to leverage multi-stage builds where it makes sense we saw how with the goang API we were able to cut
our image from 500 megabytes to just 16 with the react client we were able to go from that node based build environment
to that optimized engine X image at the end it saved quite a bit of size and just makes a lot more sense for serving
those static files that are generated as I built out those images I often chose an Alpine based base image I was doing
that because of the size however size is not the only consideration when choosing a base image for your container it is an
important consideration because the size of that base image is kind of the lower bound on the size of your final built
image however you also want to think about how well that particular base image
supports the language that you're running in in this case it may have not been the best choice for a node.js
application if you look at the official nodejs Docker file page you'll see that Alpine is still considered experimental
uh and that's because it uses a different variant of the standard C library uh and so there can be subtle
bugs associated with that if your code or the libraries that you're depending on are making assumptions about the
implementation details of that core Library you'll also want to think about the ergonomics of the image how easy is
it to work with if you need to install additional packages does it have utilities built in for debugging Etc and
then finally the security of the image is another huge consideration things like the number of cve built in at what
level uh and the attack surface area if someone is trying to compromise the container on the right hand side here
I've listed out some sample images that you could use for a node.js application the first one is the default latest tag
which is currently pointing to the 19.8 Bullseye image and Bullseye is a variant of
dban that one is nearly a gigabyte it has five critical vul vulnerabilities in it generally you wouldn't want to use
that however it is quite easy to work with you can use the app package manager to easily install lots of different
Packages Etc the next one down there is the slim variant of that so it's still using that Debian based image it is cut
the size by a factor of four eliminated all all of those critical high and medium vulnerabilities it still does
have some lowlevel vulnerabilities cves however it's a much better choice than the fullsize image for that reason
however because it's slim it won't have a lot of utilities built in out of the box and so you may need to install those
if you need them at either build time or at runtime uh the node Alpine image is the one that I was using in some of the
examples it doesn't have any cves built in it's quite small however as I pointed out and I actually learned this after
filming that section it's still considered experimental by node and so if you go to the repo you'll notice that
I'm using that slim based Bullseye image for the the base image there there's two additional images here at the bottom
that I've listed out the first one is what is called Dro list that was created by Google as a security focused image
that only contains the language runtime that you need it doesn't have any additional things like a package manager
uh shells built in ETC and so it is quite small and reasonably good from a security perspective however one
potential downside is that it only supports LTS long-term support variants of node and so if you want to run on a
different version of node that's not possible it also if you need to install anything outside of node there's no
package manager so that could be more difficult to do the final image that I've listed there at the bottom uh is
from chainu guard and chainu guard is a security focused Cloud native company they have started producing and
maintaining uh these BAS images for various languages and run times it is quite small as you can see it's the
smallest one listed here it has no known cves in it and it's designed specifically with security uh and
software Providence where the source of any dependencies are coming from because it is more new uh and it is really
locked down it may be slightly more difficult to work with in terms of the types of packages that are supported and
included if you have particular needs for your application and so I think as a general purpose image that's easy to
work with the dbn based variants the slim variant in particular uh is quite a good choice and if you're really looking
to lock down your eventual containers something like the the chain guard based image uh could be a good choice through
the process of building out those Docker files for the demo application I highlighted a number of different
instructions however I didn't cover them all there's a couple of additional features that I want to call out here
included this Docker file. sample in the repo as well um the first one is paral directives so at the very top of your
Docker file there's two things you can do one you can specify a specific version of the docker file syntax that's
necessary if you're using some of the newer features that are not enabled by default so that copy-- link that I that
I talked about a minute ago um that I I think is in Docker file version 1.5 so if you wanted to use that you would need
to have this directive at the top of your Docker file uh you can also set an escape character the default Escape
character is going to be that backslash but if you wanted it to be something else you could set it accordingly the
next feature that I'll call out is is label and this is super useful for including important metadata within the
image manifest for example there on line 14 I've included myself as the author of this Docker file you can add any
arbitrary key and value pair there are a number of standard specific labels under org open containers image that people
use including things like authors version Etc so I would suggest taking a look at those and deciding which ones
make sense for your images I also mentioned that sometimes you want to combine multiple commands into a single
step so they're combined within a single container layer one nice feature for doing that is what's called the her do
syntax and if we see they're on line 20 we have run and then the less than less than eof apt update apt install eof that
allows us to write multiple commands across multiple lines however they get interpreted as being a single line
within the docker file and so we'll execute those two things in turn but they'll be stored within a single layer
within the container container image in order to avoid sensitive data from leaking into our our image at build time
we will want to use the secret Mount type so on line 30 there I have an example of that where we're mounting in
a secret so this in this case it is a on our local system it's a file called secret. text and we can mount that into
the file system of the container at/ container secret. text uh and that will only exist at build time it will not get
included in the final artifact that is an important technique to know if you need to use credentials within your
build context now so far we've been using the CMD or command option and that specifies the
command that gets run when you create a container from your image there's an additional instruction called entry
point which is slightly different and a bit nuanced as to how these two interact with each other it can be a little
confusing as to which of these get gets run in which case if you're building your own application and it's always
going to be run with the same set of arguments you want to just place all of those in the command like we've been
doing however if you're building a utility where you can pass additional arguments to The Container at runtime
you would want to put the executable in the entry point and then allow people to append arguments via the command at
runtime when we wanted to get files from our host system or from a an earlier build stage into our container image we
were always using the copy command there's a very similar command called add which is a little more ambiguous and
so Docker generally prefers using the copy command you may see Docker files though that use ad and just know that
they perform almost the same way but there are a little bit of nuance in the Syntax for very specific edge cases so
just know that that exists and then the final thing to call out is there's a technology called build X which is built
into the docker CLI which allows us to do a number of things but one of the very important things is to build and
run multi-architecture images so if I'm on an x86 based system but you're on An Arm based system or I want to deploy to
An Arm based system I can use build X to build versions of of my image that are compatible with either of those now I'm
not going to go into detail in this course however I do have a video that does a deep dive on buildex and building
and using multi-architecture images that I'll link to in that card up there I will just pull up this Docker file.
sample and just interact with it a little bit though so within module 6 I've got this
Docker file. sample uh and then in my make file I have a couple of commands that I want to show you
here uh the first thing that I'm to do is make build sample because it's using that secret
Mount that's a build kit feature that you have to enable and so by specifying the environment variable Docker build
kit equals 1 I'm able to use that I'm also setting this base image tag as a build argument so one important thing to
call out is that within a Docker file I can use this ARG instruction that will be an environment variable available at
build time but not available at runtime and so in this case I'm setting my base image tag it was going to default to 194
but I was able to override that with this base image tag here of 193 in my build
command however even though they're not available at runtime they still exist as artifacts in the image metadata that's
why we can't pass credentials using this buildar uh approach instead we want to use the secret Mount approach now you'll
notice that I have both an entry point and a command here uh and so I'm going to run this image and let you with a few
different options and let you see how that behaves and so to do that I have this run sample
entry point command so I'll do make which goes off and builds our image and then it's going to run it in a
number of different configurations if we run the image with no arguments so if we just do Docker run and then the name of
our image uh what do we get out we get out hey Team wave entry point plus command so with no arguments we get we
Echo both this string and then we get this string as well so both of those get passed to the echo command if we run
with an argument so that would be like Docker run sample plus argument now we we still get hey Team Wave entry point
but instead of plus command we get plus argument so adding that argument onto my Docker run command overrode the command
that was specified in my Docker file we also can override the entry point at runtime and so here if we do Docker
run-- entrypoint Echo sample that overrides the entry point and it ignores our Command and so that comes back with
an empty string because we ran the echo command with no arguments and then the final case is if
we override the entry point and we pass additional arguments uh it's going to do it's going to override the entry point
ignore the command and use the arguments that are passed at runtime and so with this command we end up with hey team
overwritten entry point plus arguments hopefully that description shows you how those two things interact like I said if
you're building your own application that's not going to be called with different arguments each time put
everything within that Command Block it's just simpler and we don't have to worry about the entry point you're
generally only going to use the entry point if you're building out a utility that's expecting additional arguments at
runtime to be passed into that container now I do have a make Target here to also demonstrate Building multi-architecture
images so I'll to make build multiarch uh in this case I have a a
repo on Docker Hub called Sid multi Sid Palace multi-arch test so I can show you that and this is going to with this D-
platform tag it's going to build one image for amd64 one for arm 64 and one for arm V7 which is a 32-bit uh arm
architecture it's going to build that based on the docker file. sample and then it's going to push it to dockerhub
so if we go here it is still building once that finishes building and pushing uh we would see an updated tag I
last pushed it 20 20 days ago but if I update this here in a minute it will update to now we can see it's working on
those different architectures in par parallel and so behind the scenes build X and buildkit are
using uh a emulation technology called Kimu qmu to enable us to build the an architecture
other than the one that we're actually running on this this topic is becoming more and more important as the number of
developers on arm systems with apple silicon is increasing and the fact that arm-based servers in the cloud are
generally quite a bit cheaper than their x86 counterpart so being able to build for whatever architecture you want can
increase the likelihood that your team is all compatible and allow you to take advantage of some cost savings with your
eventual deployment so it looks like it is finished so if I refresh this we should
see that we just pushed a few seconds ago for all three of these architectures uh with that test image and that
concludes our Deep dive on writing and optimizing Docker files hopefully you'll be able to take
that knowledge and now apply it to any future application that you'll need to containerize in this portion we focused
on building out those production container images in later modules we're going to
think a little bit more about how do we modify these to make them easier to work with from a development perspective so
that we can have both our optimized production image as well as a development and debug or test image that
we can work with on a day-to-day basis so far in this course we've been building all of our container image
locally but if you're working on a team or you need to deploy those containers somewhere you're going to need to start
using a registry a container registry is a repository or a collection of repositories used to store and access
container images so these are generally stored somewhere in the cloud and they enable us to take those images that
we've built either locally or within a continuous integration server uh push them to this registry and then other
team members will be able to pull them we can pull them into whatever deployment environment we're using repos
can be either public or private so when you go on dockerhub and search for those public images you'll find
those but you can also set them up such that they require authentication there's many different container Registries that
are available dockerhub is a popular one that is provided by Docker the company GitHub has its own container registry
Google Amazon Azure they all have container Registries as well as I mentioned you can have public
or private container Registries for a public registry you don't need to authenticate in order to pull however in
order to push to that registry you would need to authenticate and for a private registry you'll need to be authenticated
for both pushing and pulling Docker desktop has what's known as a credential Helper and using this when you log into
one of these container Registries it's able to access system resources such as on Mac OS it stores the necessary
credentials within your system keychain so that they can remain secure on your local system additionally some
Registries May leverage their own tooling to authenticate for examp example the Google container registry
uses the g-cloud command line utility to authenticate your gcp user to that registry and works with the docker
client to provide access now I want to demo uh building and pushing an image to dockerhub and the GitHub registry just
to Showcase how we would do that I'm using the most simple Docker file you could imagine here it's just going to
have a base image from scratch I'm putting that into a Docker file building my scratch image from that and then I'm
deleting that doer file just because I don't want to store an extra file in the repo so if I do make
build we've now built that container image and it exists locally on my system in order to push to a remote repo you
have to do two things one you need to be authenticated to that repo and two you need to tag an image with a tag
corresponding to that repo and so if I go to dockerhub I've already created this repo called my scratch image if we
needed to create a new repo you would click the create repository button uh fill out the name add a description
choose if it's public or private and then click the create button in this case I've already created this and so
the tag that we need to use in order to push to this repo is going to be Sid Palace which is my dockerhub username
the name of that image my scratch image and then we can append additional tags to the end in order to authenticate to
dockerhub we would use the docker login command I've already authenticated so it used
those existing credentials but it would ask you for your dockerhub username and password in order to log in if you
hadn't done so before so in order to take this image that I just built and push it to
dockerhub I need to retag it with a tag associated with that repo so in this case it was sidp Palace my scratch image
uniquely identifies that repo for Docker Hub you don't need to include a prefix you can just use the the username and
repo directly and it will assume that it's dockerhub if you don't have a registry prefix in front of it we'll see
that's a little different for the GitHub one but in this case I'm using the docker tag command to take my original
tag of mys scratch image that I applied up here uh add an additional tag of Sid Palace my scratch image and because I
don't have a colon with a tag uh version it's just going to apply the latest tag to
it I can then push it to dockerhub and that will show up however generally we never want to use the latest tag and I'm
going to talk about tagging best practices here in a bit but it's much better to apply a specific tag to this
so I'm going to once again take my original tag of my scratch image apply a new tag which corresponds to my
dockerhub repo and in this case I'm adding the tag abc123 as just an example tag I can call a Docker push on that tag
and it'll get pushed to to dockerhub so let me just call that make push dockerhub uh it tags my images with the
ne necessary things and then pushes both of those to dockerhub I can now go to dockerhub and if I refresh the
page we can see both that latest tag when I didn't specify anything as well as my abc123 tag were both pushed uh
just a few seconds ago so that's dockerhub let's now go through the process of authenticating and pushing to
GitHub container registry the process looks quite similar but we do need to follow the instructions um at this link
and so here's a walk through of how to work with the container registry in particular I want to look at
authenticating to The Container registry they give some instructions on how to do this within a GitHub actions workflow um
I'm going to use this authenticating with a personal token option and so the way that we do this the set of minimal
permissions that we need can be created using uh this URL that they provide zoom in just a bit uh I'll say
Docker course creds it has the Right Packages permission
now I'll click generate token don't worry I will delete this token once I finish recording so I'm going to export
this as my uh container registry personal access token great and then there was a login command
here yeah so I'm going to Echo that token out and pass it to the docker login uh command specifying that I want
to log into the GitHub container registry. registry so Echo that looks like our login succeeded and now
just like with dockerhub I'm going to retag my image using uh this repo name and so in this
case I have the same repo name as I had on dockerhub but now I'm prefixing it with
gc. which tells it to use GitHub container registry again if you don't specify tag
it will default the latest and then down below I'm tagging it with abc123 and pushing both of those to GitHub
so I'll do make push GitHub packages it tagged both of them pushed them to GitHub container registry and
now I can navigate to GitHub container registry Sid Palace my scratch image uh and we can see we have a latest
tag and an abc123 tag now let me just go delete those personal access tokens so I don't forget
and that's under settings developer settings personal access tokens
tokens revoke all so each registry is going to have its own instructions for logging in so
find the instructions from the from the registry itself and then follow those and that'll work with the docker client
to store those authentication credentials on your system now I talked about when I was
tagging and pushing those images how if you don't specify a tag it defaults the latest and that's generally a bad
practice you want to have descriptive tags that tell you what the image actually is doing it's also important to
note the same image can have many tags and so for example if we go into dockerhub when I last checked on January
30th all five of these tags auntu latest auntu 2204 Jammy -22 1130 Jammy and this specific shot 256 hash all point to the
exact same image and so when we're tagging our images we can put multiple tags that we think will be useful to the
end user now ideally you want to treat any tag except for maybe a temporary one
that you're using for development as immutable and by that I mean you shouldn't create a tag push it to
dockerhub or or whatever you're using for your your registry modify the image rebuild and tag and then push and
overwrite that can be a nightmare if someone else is using that image and they could accidentally get the updated
version and cause issues within their process so you always want to treat tags as immutable where possible the one
exception there is if you're doing something locally and building it over and over and running it locally you
might not need to change the tag every time because you're you know the fact that you're changing it every time now
usually your images are going to be built within some continuous integration system hopefully that's automated and
often times I'll see people use some combination of a time stamp of when it was built so maybe like we see up above
that 22 1130 uh it was like likely built on November 30th uh you might put in a build ID from your continuous
integration system so maybe the GitHub action the ID of that GitHub action so if you need to go look and see when it
was built you can see the process that happened uh the commit hash of the source code so let's say I'm building an
image based on the latest commit to my main branch I might take that that hash from that Branch uh of the latest commit
so that I can very easily find where in the code base uh that build is coming from uh and then also often times I'll
see people use a sver release version so like 1.1.0 or whatever the the latest release version is these can be combined
in various forms uh when I set up an automated workflow with GitHub actions in the developer experience portion I'll
tag the images with a bunch of with combinations of a bunch of these also as we saw when we pushed to a registry
other than dockerhub we had to prefix the name of that registry at the front end of our tag now that we've built our
container images and learned how to push them to various container Registries we can move on to running them and
configuring them to all talk together within Docker up until this point we've used the docker run command a number of
times to run containers from third party images we've seen this example with postgres a number of times however
there's another way to run Docker containers that is built into Docker and that's called Docker compose behind the
scenes they do exactly the same thing but Docker compose allows you to specify all of your application configuration
within a gaml file and it makes dealing with the container life cycle much much easier these two images here on the left
hand side shows the docker run variant the right hand side does exactly the same thing but using that compos file
instead if you're just running a one-off container I think using Docker run is fine but for our situation where we have
multiple services that need to talk to each other Docker compose is much more intuitive and easier to work with I'm
going to show how to do both for our setup but in most situations you'll be using Docker compose if you have an
application with multiple containerized Services I'll also point out this GitHub repo called composer eyesee which you
can paste in a Docker run command and it will generate the equivalent Docker compose file and so if you have a
command that you've been using and you want to convert that to Docker compose that can be a very easy way to do that
now there's a ton of different options and flags that you can use at runtime with Docker I've tried to pull out here
the most common ones that I use on a day-to-day basis and let's go through and test these out and showcase what
they do the first one is that Das D which stands for detached which allows us to
run a container in the background so as an example I can run Docker run auntu sleep five and it's going to take over
my shell until that sleep command exits and then it will return so after five seconds it's done however if I add A- D
to that now it gives me the ID of the container and runs in the background I
can see it by doing Docker PS and here we see that abun 2 container uh running my command sleep 99 in the
background the next one is the D- entry point option where we can override the entry point that was defined in the
docker file an example of this by default the auntu image doesn't have an entry point and uses bash as its command
we can actually use the entry point like this to say roer run entry point override that with Echo and then
we'll pass the argument hello so when I run this it should Echo hello back to the terminal there we go the third one
there --v or- e or --v file is to set environment variables at runtime so we could do things like pass in database
credentials or configure anything else in the environment that we need to Showcase that one I can do
Docker run-- EnV my n equals hello auntu and then the command I'm going to run is print end which will print out all the
environment variables in the environment you can see it has these three environment variables including
the one that I set at runtime the init flag uh is an interesting one some programs are not
designed to be run as process ID number one which is what happens by default if you run them in a container and so if
you pass the init flag with your Docker run command instead Docker will run its own initialization script and that will
spawn your process as a subprocess so for example I can run Docker Ubuntu PS and that PS command gets executed as
process ID number one however if I pass the init flag and run the same thing we see that this Docker
init is process id1 and my PS command is process ID 7 this is helpful if your application is managing subprocesses
itself passing the init flag can make it easier to handle forwarding along termination signals from Docker to The
Container the interactive flag or - I and the TTY flag DT allow us to have an interactive TTY session inside the
container so if I just run Docker run Ubuntu it will call bash which will exit immediately however if I add the dash it
doer run-it Ubuntu uh the - I will make sure that standard in remains open and the- T will
give me a TTY teletype or shell session with inside the container and now I have the shell
running in the container we've seen the D- Mount and D- volume options used quite a bit when we
need to persist data outside of the container layer in a volume so I'm not going to demo that one now the name
option gives us the opport to provide a specific name for a container otherwise Docker will assign a
random name when the container started so I can do uh Docker run I'm going to use the DD flag so this runs in
the background and doesn't take over my terminal I'll name the container my container and then pass it the Sleep 99
command ah and this actually shows one downside of using named containers is that you can't have two containers with
the same name on your system so before I could run this I would need to remove that
container now I should be able to run this and now we have that container in the background if I do a doctor PS uh we
can see under names my container this can be useful if you want to be able to quickly grab the logs from
the container you can now address it with the name rather than the ID that that
container doesn't have any logs because it's just the Sleep command but usually I just let Docker manage my naming for
me and do it randomly the network or net option allows us to connect to a specific Docker Network so if I do
Docker Network LS we can see the different networks on my system I'm going to add a new one called my
network with Docker Network create my network now we can see that new my network was
just created and now I can create a container that attaches to it and if I inspect that
container with the ID and grap for Network we can see it's running in my
network specifying a network like this allows us to create isolated networks for any for our
applications rather than just letting all of the docker containers end up on docker's default Network in such a way
that they would be able to communicate with each other the platform option allows us to
specify which architecture we want to run our container image even though I'm on a amd64 system
I can run it with this Linux arm64 V8 option it went off and pulled the arm 64 version and then I'm running this D
package print architecture command and we can see that it output arm 64 if I do the same thing but specify
Linux amd64 we get the amd64 result we've already seen the publish
or- P option used quite a bit to connect a port from our host system to that of the
container the Das Das restart option there's a few different options you can have always unless stopped or Never So
if I specify to restart this container unless stopped Docker is going to see if it exits and unless I told it to stop it
will continue to try to restart it so in this case that bash command the default for auntu is going to exit docker's
going to see that and restart it over and over so now I can do watch Docker PS and we'll
see this image is exiting and restarting exiting and restarting and then lastly the-- RM flag
tells Docker that if the container process exit it should remove that container rather than leave it on the
system as a sto container this can help a leave at that naming conflict that we ran into before where the two containers
had the same name if we had used the D- RM flag when the previous container had exited Docker would have clean that up
to demonstrate this I can run one Docker container uh named this one will be there that's going to exit however it
should still remain in the system as a stop container I'll now run another one with the-- RM flag called this one will
be gone and now if I do a Docker container list- a the d a is necessary because
otherwise it would only show running containers and I grep for this one that should find both of them if they existed
however we only see this one will be there in the listing there are way more options than
this including this set which I think you should be familiar with you may not use these on a day-to-day basis the
first of which is capabilities add and capabilities drop this allows you to specify which Linux capabilities should
be accessible from the container so this is a security feature where you can what is allowable within the context of that
container cgroup parent allows you to specify which cgroup ID your container should be associated with if we think
back to how we can use cgroups to Monitor and limit the resources that the container has access to this is one way
to specify that directly CPU shares is another cgroup related option where you can specify what percentage of the CPU
Cycles this container should be able to access the CPU set option allows you to specify which core specifically so if
you're fine-tuning performance and want to pin a process to only execute on a specific CPU core you can do so these
next two are related to the different devices that the container should have access to and at what throughput and
bandwidth if we wanted to be able to access gpus with our container we would want to use the GPU flag we can use
these health related options to specify a health check that Docker will use to periodically ping our container and make
sure it is is healthy like our CPU option we can use this memory option to specify how much memory the container
process should have access to if it goes over that limit Docker will kill it we can use these PID related options to
specify how many subprocesses our container should be allowed to manage the privileged option pretty much
overrides all of these other Security Options and effectively gives the container access to all of the
Privileges that it could want the readon flag specifies that even the container layer of the file system should be
readon so if you don't need to have right access you might want to set this as another security Forin the security
opt option allows you to specify either app armor or set comp profiles that you want to use for the container when it's
run and then this last one is not one that you execute at container runtime but is something you would specify when
you're running Docker D for example if you installing Docker engine and that is to enable namespace remapping for the
user namespace allowing you to map from a non-root user on the host system to a root user inside of your containers as
just one more layer of defense against a potential attacker so like I said these are very useful to know that they exist
however if you're working on your development system it may not be as necessary to leverage more of these
Advanced configuration options let's jump over to our editor and and configure Docker run commands and Docker
compos configs to run our application and so I I have all of the necessary commands to do this stored within the
make file here and I'm going to walk through how they're all configured and what they're what each of them is doing
the very first thing that we're going to want to do is to build a version of our container image and tag it such that we
can use it in our Docker run commands so this Docker build all make Target is calling a Docker build command and then
because I'm located in a different subdirectory so I'm in the 08 I'm in the module 8 directory whereas we know our
Docker files are contained in the in the module 6 directory and our source code is in the the module 5 directory that's
why I'm having to use this file option and pass it a context outside of my current directory uh if you had your
Docker compose and Docker files living alongside the code these commands would be somewhat
simpler so this is going to go off and this first one is going to build my client react Docker file number three so
if you remember as we were building up our Docker files at one point for the react client we were using using the npm
Run Dev script and so that's actually using V to serve a development copy of our front end uh so we're going to build
one image with that Docker file we're also going to use the final the final react client Docker file where we're
copying a a built version of our frontend app into an engine X based container image so we'll have one tagged
client react enginex based on that image we'll also build our node-based API using the final Docker file that we came
up with and then we'll build our goang API as well using that final Docker file that we came up with for that one uh so
let me just do make Docker build all and that will go off and build those four
Images I did clear my image cache so this is going to take some time to build from scratch all of these
images however in the meantime I can walk through the docker run commands that we're going to have to use to spin
up these containers so I'm going to start by creating a Docker Network and so by default there's a number of Docker
networks on the system uh let me just show those now so if I do Docker Network LS we can see these are all the default
networks that are created we have a bridge Network we have a host Network and we have an empty null
network if I don't specify a network it's going to get attached into this default Bridge Network however I want to
create an isolated Network for my application and so for that I'm creating a network called my
network so I'll do just that I can do Docker Network create my network and now if I list them again we can see my
network and by default it uses that bridge driver which is what we want in this case the first container that we're
going to run is the postgres container we've seen seen this command a number of times the only difference here is that
I've added the-- Network option and passed it the name of my network and added the restart unless stopped command
to tell Docker to restart it if it if it were to crash this- D flag will run this in the
background we can see that postgres image is running in the background I could look at the logs
if I wanted with doer log logs and then the ID we can see it's started up and it's ready to accept connections it
looks like all of my uh four custom container images have now built so I can walk through and execute the various
commands such that those containers will start up as well uh I'm going to start with uh API node so I'm going to work
from the database forward so we'll have Docker run again we're going to run in the background with the DD flag I'm
giving it a name I'm giving it a name here so that I can have another make Target that will clean up those
resources afterwards I'm putting it on that same network so that it will be able to talk to my database I'm passing
it this database URL and so I've specified that environment variable here and one important thing to note is that
I gave the postgres container the name DB and then I've also added this D-Link DB option referencing the name of my
postgres container and that gives me an alias with which I can address my database container and that is what is
used here in my connection URL I should now be able to run Docker run for my node container we can see API node and
postes uh at this point it looks like my node container is crashing for some reason so let's look at the logs
here Docker logs so I wonder if this string is somehow getting escaped improperly so
let me try again um Docker stop API
node Docker remove API node ah it's because I copied and pasted this in without setting that environment
variable export database URL equals that now if I run my container uh it should be able to pick
up that environment variable and pass it in so I can do Docker PS uh it is no longer restarting and so
I should be able to access on Port 3000 my API so let's make sure that is working and so it looks like my node API
is working properly for the goang docker run command looks pretty much identical to the API node
one uh except the name is updated and the image is updated we're still passing that link option so that we can
reference the database in the same way and so let's start up our goang image it is now up let's switch over I
know that one is running on 8080 okay goang is working properly and now we need to get our two front end
containers so we're going to have the one running engine X and the one running uh V we'll start with the V one one
thing to note is that my V config inside of Docker needs to be slightly different than my V config outside of Docker let
me just compare the two so I'll select for compare and then here I will compare with selected
and you can see when I was running these directly on my host without Docker I was able to my proxy was set up to just
forward these to Local Host 880 now that I'm inside of that Docker Network I'm and I'm passing the the link option
instead of referencing on Local Host I need to reference them by the Alias for that container so in this case it'll be
API goang and API node and the way that I get this updated V config into my container at runtime is
via a bind Mount so I'm using the- V option to specify that uh with my present working directory and then a
relative path to client react V config.js and I'm mounting that to the same location that the other one would
have already been located and this will overwrite the one that was in the container image with the one on my host
system at that bind Mount location I'm linking this both to API node and API goang because I'm talking
to both of them and then I've referenced my name as well as my image name down here in the
command so I can run [Applause] this okay it's been up for one second uh
so let's go to 5173 which is where I'm publishing that Port we can see it loading properly
that's awesome and then the final container that I want to run uh is going to be my engine X based front end where
I took my react app I built those files and then copy them into an engine X based image almost identical to the V
one with the one difference being I don't need a bind Mount because the engine X config that I wrote was already
designed to work within this containerized setup I'm also going to port forward from Local Host 80 to 8080
with ins inside the container which is where that unprivileged user from my engine X base image is is going to bind
to by default I'll run that and now if I navigate to just Local
Host 80 or by default if I don't specify a port it will default to 80 it is working
properly and so with these with all these commands I can now spin up my application and spin down my application
very easily I've added this docker stop make Target that will stop all of those named containers I've added this Docker
remove one that will clean up those stopped containers off of my system and delete those as well as delete that
Network and so I'll do that now and so now none of those containers are running anymore in my opinion having
all these doer run commands with all these different Flags uh stored in a make file is pretty clunky and hard to
deal with hard to modify hard to reason about luckily for us Docker compose exists and gives us a much nicer
interface with which to do this exact same thing so I'll pull up this Docker compose file I'll also show you uh that
composer eyes tool so let me just go to composer eyes zoom in a
bit paste in my command click output and so we see my postgres command gets translated into this
compose file where we have our services in this case it's named postgres we've named the container DB we're specifying
the network we're passing an environment variables we're configuring the volume we're setting ports we're setting which
ports to publish the restart option and the image I've gone ahead and done this for all of those commands that we had
and that gener ated this Docker compose yaml file the latest version for Docker
compose is 3.9 so if you're creating a new Docker compos file and want to take advantage of the latest and greatest
features you may as well specify 3.9 and then within here I've got one block for each of my different services so I'll
start with client react V and we can specify both an image as well as a build context and so Docker compos can handle
not only running our our our containers but can also handle building them based on a context that's specified
so in this case it's pointing to module 5 where the source code lives as well as a Docker file and here it's pointing to
module 6 where the docker files live where the docker file lives this init true is the equivalent
of a D- init in the docker run command again we've got our bind Mount here specified for the V config I did
something slightly differently with the networking here I have two custom networks I have a front-end Network and
a backend Network and so because my react app only needs to talk to my apis and should never need to talk directly
to the database I've specified that those are attached to the front- end Network my react app and the two apis
and then I have a backend Network which my two apis and the database are connected to and so that just gives me
even more fine grain control over which Services can talk to which my enginex based service uh looks very similar
the main difference being I'm pointing to a different Docker file and port forwarding to a different
port my node API again quite similar I will call out I've added this dependson DB option and that tells Docker compose
that wait until you've started up the DB container before you spin up the node container as I mentioned it's the API
layer is connected to both the front end and the backend Network so we can have a listing of multiple networks here that
this container should be attached to pretty much the same thing for the goang service and then finally the DB service
which we're specifying that password it's only attached to the backend Network we've got our volume Mount at
the known location where postgres stores its data and then we're using that public 15.1 Alpine image as our
container image down here at the bottom we specify our different volumes in this case I have my PG data volume that's
getting used for postgress and I specify my two networks because I haven't specified any additional data about
these Networks they will use that default type of bridge which is the desired network type for this situation
now with this all defined in this yaml file interacting with it is super easy let's say I want to build all those
containers I can do Docker compose build and it will look at the different build sections for each service with the
context and the docker file and go off and build those for me now if I want to run them I can just do Docker compose up
and it's going to run all five of those containers and log out to the console from them all with color coding for each
so I can see now I'm attached all my containers are running if I then go and load the page so in this case I opened
up the engine X based front end off screen and we can see the logs coming from the engine X container followed by
the log from the goang container where the front end made a request there followed by the log to the node
container where the front end made a request and so we can see this be very valuable and help us to see what's going
on within our containers by attaching to the logs in this way when we did our Docker run command we could have not
used the detached option and instead had terminals connected to everything but with doer and compos it's just much
easier to manage within a single terminal if I wanted to I could uh run these in the background with the- d flag
I could do Docker compose Dash up DD uh and we can see it's created all those in the background if I then wanted
to stop I would do a Docker compose stop and because my Docker compos file is named as such that's the default if I
named it something else I would need to pass in a reference to that file uh with my Docker compose command just like when
we run a Docker build command if we have a Docker file in the non-default location we need to pass that- F
flag and so hopefully you can see the power of a Docker composed setup like this you'll be able to go to the GitHub
repo clone it navigate to module 8 run a Docker compos up build and it will literally build all the necessary images
and run it on your system and you'll have this application running in less than 5 minutes uh and you can see
exactly how everything is configured within this one file and so this can be a super powerful tool for you and your
team in setting up a common shared environment so that's it's easy to build and iterate on your your applications
when we go to deploy the application and work on improving the developer experience we'll make some modifications
to this and change the config a bit but I think this is a great starting point that hopefully highlights uh many of the
different options that you'll use when you're running containers for your multi-service applications since many of
those more advanced runtime options are security related it seemed like a good time to do a little bit of a deep dive
on container security and this I think is my favorite of the memes that I created for the sections of this course
just because something is running in a container doesn't inherently make it secure depending how you configure
things at runtime it may be secure or it may not be so here if you specify user equals root the privileged option and
connect it to the host Network you're removing almost all of the security boundary that a container would provide
and it's about a secure is using that Cheeto as a lock there's two primary components to
container security the first one is security of the image itself so as you're building the image what features
are you using and how are you configuring things to make sure that your image is secure a big part of that
are the software dependencies that are installed within your image and what attack surface area exists that a
potential hacker could exploit the other key area to think about container security is at runtime so if someone
were to successfully compromise your container because of some vulnerability that you built in what would they be
able to do would they be able to escape the container and move laterally to compromise the host or other containers
or would they mostly be stuck and confined within the container because you configure the runtime security
appropriately some of my main things to think about when you're considering image security are how do you keep the
attack surface area as small as possible one way to do that is to use minimal base images so the smaller your image
the fewer things installed in it the less likelihood there is to be a potential bug that can be compromised
one great source for secure base images is a company called chain guard which maintains a set of Base images
specifically with a security Focus as you're writing your Docker file don't include things in the final image that
you're not going to need if it's not needed at production time don't install it or use a multi-stage build if you
need it at build time but not at production time also after you've built your image there's a number of tools
available that you can use to scan that image for potential vulnerabilities there's one built into Docker called
sneak there's another one from Aqua security called trivy take advantage of these tools to scan your images and see
if there's potential vulnerabilities and what level those vulnerabilities are at this gives you visibility and allows you
to choose if you need to patch them or if you're willing to accept having some known vulnerability in your image we saw
this as we were building out our Docker files ideally you don't want to use a root user inside the container you want
to use a Linux user with with the minimal set of permissions to accomplish the task at hand you should avoid
building things like credal into the image itself you should treat your images as if they're public and inject
any sensitive information at runtime you also can cryptographically sign your images to prove who was the person who
built this and that can be useful to then verify on the other end that an image is what you expect and finally as
you're choosing which base images to use make sure to pin at least to the minor version number if you Pinn to major.
minor but not the patch version it would allow updates to the patch version let's say for bug fixes to be automatically
rolled in but hopefully not incorporate braking changes into your system alternatively if you really want
to lock down the base image you can use that image hash which will always refer to the same
image on the runtime side of thing we can both think about the the container run time itself and the configuration
options used for each container if we're setting up the docker demon Docker D within Docker engine one
good tip is to use the user namespace remap option as described in the docs at that link to ensure that containers
being run are in a separate username space than the host system and then for individual containers we can set the
file system as readon if our application doesn't require WR access we can use the cap drop all option to remove all
capabilities and then add back anything that we might need after that we can limit the CPU in memory to prevent a
denial of service situation where one process is using too much of the resources and then finally we can use
Security Options to set either set comp profiles or app armor profiles both of which you can find more information at
in the docs throughout the course I've used a number of Docker CLI commands as we've needed them in this module I'm
going to showcase the breadth of commands that exist and how you can use the help flag to get additional
information the four main types of Docker objects that we'll be interacting with are images containers volume and
networks all of which we've seen so far in the course for any Docker command we can do Docker
d-el and it will give us information about all of the sub commands as well as a brief description of what those
commands do in this case let's look at the commands associated with Docker images Docker images Docker image uh
help and when you see all of the sub commands associated with Docker image we've seen the build command quite
a bit it's how you take a Docker file and build an image from it the history command shows the steps that were used
to build that image uh so I have a Docker file here that is just from Ubuntu 2204 run
Echo hello command Echo goodbye and I built that into a image tagged with the tag New Image so let's do Docker image
history on that new image and we can see my command is here at the top that was
the last step in my Docker file I echoed hello here as the previous step and then these steps are the steps that were used
to build that Ubuntu base image so for example I can do Docker image history on it and we'll see these steps within that
history and so Docker history can be a good way to explore the steps that we used to build a container
image the inspect command will show a whole bunch of detailed information about images it can actually be quite
overwhelming uh so we can do inspect on that same image and we get this big Json dump with
a whole bunch of metadata associated with that container image the import command allows us to take a tarball and
create an image from it honestly I've never used that before the load command similarly can take a tar archive that
was generated using Docker save on a container and create an image from it I don't find that very useful all my
images I'm generally building from a Docker file image LS will list all the images
on our system and so as you can see I have a whole bunch here some with tags some
without tags uh prune is a great way to clean up those old images if I wanted to clean up
my images I would do doer image prune it would remove any image not associated with a
container that's what it means by dangling images I'll skip that for now the pull command we've seen is how we
can pull an image from a registry we haven't used it directly that much because when we do a Docker run if the
image doesn't exist it will first execute a pull but if I did want to execute that I could do Docker image
pull and then an image name and tag and it would go off and pull that from the registry push likewise pushes our our
built images to a registry we've seen that one quite a bit so far if I want to delete a single image
rather than using prune to delete them all I can delete just one with Docker image remove new image for example now
it's untagged and deleted from my system the save command can take an image and dump it out to a tar archive
this could be useful if you wanted to have if you want to be able to explore the file system uh outside of a
container I haven't found this to be that useful in my workflows and then finally we can use
the docker image tag command to tag an image with a new tag so I could do Docker image tag then to
22.4 mytu tag Docker image list GP mytu and we can see I now have an image
which is based on that Ubuntu 20204 but it has my custom tag associated with we saw this when we built the image with
one tag and then tag them with new tags to push them to those to Registries in the container registry module that's it
for the sub commands of Docker images however there's one more command that's associated with images that's not a sub
command and that's Docker scan so I can do Docker scan uh let's just scan the aun2 image for fun
22.4 and that will use a tool called Snick to go off query of vulnerabilities database and assess whether there's any
known vulnerabilities within that image it then outputs a report of all the different vulnerabilities that we've
found things like system D op SSL uh these are at the medium severity level we've got one here at the high
severity level it also tells you when in the Upstream project that vulnerability was fixed so then we can decide do we
want to use this base image or do we want to pick a different one that potentially has this mitigated already
that covers most of the things that you'll be doing when interacting with images let's look at the options for the
docker container command now as you can see there are many more sub commands for Docker
containers than there were for images I'm just going to highlight a few that I think are useful to know uh the attach
command if you have a running container already and you want to attach your local shell to the input output and
error streams of a container you can do that with attach a similar one to that is the exec command where rather than
attaching to an existing stream you're going to run a new command within the container just like with
images we can run the inspect which will dump a Big Blob of Json metadata that can have useful information the Stop and
kill commands are how we can either gracefully or non-g gracefully uh stop a container so if you have a container
that uh you've tried to stop but it won't die you can always use kill and that will generally do the trick you
would want to issue that with either the name of the container or the ID of the container
so if we do Docker run uh Ubuntu sleep 999 now that's going to sleep for a th
seconds and even if I do control C it nothing's going to happen uh so if I want to get out of that I can do Docker
PS uh we see that container here is running and then I would just generally do Docker kill paste that in and
now our containers exited and we've got our shell back if you want to view the logs of a
container you would do Docker container logs and then either the ID or the name of the container if you want to Trail
the logs so if you want to have a live feed of them you can add the- F command this container here that you you've seen
pop up when I do Docker PS it's a system container that you'll likely see on on your host but let's just check the logs
of it since we're here we can do Docker logs of that container we also can do Docker container logs those are the same
command and then finally if we wanted to tail those logs we can add the- F and now any additional logs would be printed
to the console automatically the ls command lists all running containers and I believe is pretty much
identical to Docker PS if we add A- a flag it will show stopped containers as well so without the d a it's only
running containers with d a it shows stopped as well if we wanted to clean up all those stopped containers we can
issue the prune command or if we wanted to remove a single container we can use the RM command the Run command is one
we've seen over and over where we're creating a container from a container image top can also be a useful one if we
want to see what's running inside the container so let's do a Docker run- DTU sleep
99 and now we have this container idid docker Docker top Docker container top then ID uh and you can see the processes
running inside of that container and then finally the weight option would could be useful if you're
writing a script where you needed the one container to finish before you progressed you could issue a Docker
container weight command which would stop your script until that container exited then print out the exit code and
you can proceed on with your script from there the volumes options are much simp we only have these five options we've
seen the create option where you just pass it a name Docker volume create my
volume we can use the inspect volume to look at that we can see where it's mounted
within that virtual machine file system the name the driver Etc the ls command will list all available
volumes marker volume LS we see a few named volumes here at the bottom we also see a bunch of anonymous
volumes above it that are created and managed by Docker if you have data that you want to persist it's much easier to
manage if you go with a named volume create that volume and then mount it into your container at runtime just like
with images and containers we can use the prune command to remove all of our unused volumes or the RM command to
remove one or more volumes now finally let's look at the options for networks they'll look very familiar given that
they're almost identical to the volume options Docker Network just like with volumes we can create inspect list prune
and remove the two new ones here are connect and disconnect and so if we had previously created a container but we
forgot to connect it to the network that we wanted with the Das Das Network flag we could then run a Docker Network
connect with the ID of the container to tie it into that Network so we can do Docker
Network LS um let's say for example we wanted to do Docker run vuntu sleep 99 I'll run that in the
background but we didn't want it to be on the bridge default network instead we wanted to connect it to my network so
that it'll be able to talk to other services that were spawning there we can do Docker Network
connect my network pass it the ID and now if we inspect my
network we can see the listing of containers that are active there and we have this one named awesome Brown just
confirm awesome Brown is the automatically generated name for that sleep container that I created if I
wanted to remove the container from that Network for whatever reason I could use the do Docker Network disconnect command
uh generally with these networks because I'm bringing containers up and down so frequently rather than starting a
container and then attaching it I would just want to add that to my Docker compos config or as a option in my
Docker run command so that it would be connected to the proper Network at runtime now that we're fully versed in
the docker CLI how we can make the developer experience working with containers extremely nice what are the
main considerations that we should think about in terms of the developer experience when building out
applications like this how do we go from this confused frustrated developer at the top this excited pair of programmers
here at the bottom we want an easy and simple setup we want to be able to iterate without needing to rebuild the
container image so far even if we made the tiniest change to our code we would need to rebuild the container image and
then rerun the container to have that reflected and that's just a terrible experience that being said we can can
use the bind mounts that we learned about to mount our source code from our host system into the Container at
runtime and then we can use hot reloading utilities like nodon or there's one for goang called a such that
with each change of our source our applications will get automatically rebuilt and restarted and those changes
will be reflected immediately we need to be able to debug our code as if it was running locally and for that we can add
debugging utilities into our development image and set up a special Docker compose file to use those utilities we
also want to be able to execute our tests within the container again I can make a custom Docker compose file that
we'll use to do that in most scenarios the only containers that you'll build be building on your local system are the
ones you're currently developing on for production images you'll want those built within a continuous integration
Pipeline and finally it can be super powerful to be able to spin up an ephemeral environment with each pull
request that you make that will have all of your modifications built into it automatically that you can share with a
team member to get feedback and iterate on things more quickly when we get to that that's where our sponsor of today's
video Shipyard dobu comes into play they make it very easy to take your Docker compos setup and get a fresh ephemeral
environment built and deployed automatically with a simple GitHub event like a poll request so let's jump over
to the code editor and take our initial Docker compose file that we built a few modules ago and modify it to improve the
developer experience in these ways so we first built out our do ER compos file in module 8 so I'm going to copy that into
module 11 let's just make sure it still runs I already have a make Target set up
that we'll call uh compose up and I'm going to name this Docker compose Dev make compose up build which
will look at that Dev compose file build my images and then run them uh it looks like uh
line nine here where I'm using my present work directory it's no longer a valid reference to that V config file in
instead I'm going to have to set the relative path to that like so okay so things appear to be working
as expected just had to make that one tweak to the volume Mount i' would say if we're if we're thinking of that wish
list that I just had up easy and simple setup is a check we're able to get up and running with just that single
command now we want to improve upon iteration without having to rebuild the container also while this volume Mount
syntax works I slightly prefer the more verbose version that we can specify here with a
type A source and a Target so this should do exactly the same thing and now in addition to bind mounting in that
configuration let's also bind Mount the entire source s code directory so that's going to be uh in
this module 5 client react and so this will bind Mount the client react directory into our
container at user Source app one additional point though is that within client react we have things like the
node modules directory and the uh distribution directory which we don't want to get bind mounted in and so the
way that we can overcome that is to specify another volume Mount that has no source and only a Target and so by doing
this it will effectively overwrite this bind mount for this particular path and avoid bringing those node modules from
my host system into the container and just to remind myself what the command is for Docker file 3 let's
go look at that one client react Docker file 3 this is running npm runev which in turn calls V
so we should have hot reloading built in and so now with these bind mounts in place let me start up my application
again now if I navigate into my source code and make a change let's see what happens client react source app.jsx
save it because we're bind mounting that Source in we automatically reload our application with feet and that gets
built in and we see it immediately on the front end now let's take the same technique and apply it to our two API
servers here we're going to use the same bind Mount approach for the uh node API including the empty volume Mount over
node modules to avoid bringing those into the container and I'll put it right after in
it true uh instead of client react we want API node and let's just remind ourselves
what this Docker file 8 is doing okay so we're going to want to make a few tweaks here uh this Docker
file is optimized for our production deployment but in this case we want to have a separate build stage that we can
Target for this development environment I'll start by naming my first stage uh as
space and then where we diverge from our production versus development is going
to be at this step where for production we only want to install the production dependencies so I'm going to say from
base here and then I'm also going to have uh as
production and then I'll do from base as Dev and so now within this stage we can have our development commands so our
development commands are going to look similar uh but instead of npm C only production uh we'll do npm install and
that will install both Dev and production dependencies let's also move this
environment variable down such that we set that only in the production
image after we've installed our dependencies we want to copy all of our source code
in and then for the development stage we want our Command to be uh npm runev uh and I'm actually going to call
this Docker file number n just so we can keep them keep track of them uh so we have our Dev stage where
we're installing development and production dependencies we're copying our source we're running npm runev and
then we have our production stage where we are doing what we were doing before with the non-root user installing only
production dependencies uh Etc now in our Docker compos Dev let's use that new Docker
file 9 and see if we succeeded oh one thing I forgot to do is in
addition to a context and a Docker file you can pass a Target and that's allows you to specify which of your stages in
your multi-stage build you want to Target so in this case it would have defaulted to production as the final
stage I want it to Target that Dev stage because that's going to be where we're running nodemon for the auto
restart so now let's make a change to our node API Source here Source
index uh in our response let's just add
Tada so we save it now refresh and we see our node reserver was detected noemon saw the change restarted uh and
now when we hit that API from our front end we get the updated version back and so we've got hot reloading working for
our front end in vit as well as our uh node based API let's do the same for goang our bind Mount setup should be
quite similar a volume going to API
goang uh we don't need the node modules Mount depending how you have your go path set up uh that will determine where
you're going to install your dependencies on your host system uh in this case it's outside of my project
directory so I don't have to worry about shadowing that with an empty volume however my target inside the
container instead of user Source app if we think about the work directory that I used in the docker file it's just
slapp uh and now I need to do something similar to my production Docker file add some development stages so that I can
have both my production ready and my development images make a new one called Docker
file number8 make sure we're using the new one and we're going to add a target of
Dev and we will get around to adding that here uh so my first stage already has a
name but now because I'm going to have I'm going to split this into two build stages so I'll have build
base as this it should be the same up until here where we download our
dependencies however this is where we want to split and so I'll do from build base
as build Dev and so for our Dev build in addition to building in addition to pulling in
our dependencies that we've specified there's two more dependencies that that I want to add from a development
perspective and that is a package called air which enables a hot reloading so it'll detect a change like noon rebuild
that application and then restart it as well as a package called delve which allows for remote debugging that we'll
set up so we can uh do set break points and connect to our container remotely to investigate the state of all of our
variables and that sort of thing and so the command to install those two dependencies is go
install pass it the name of the package and go install pass it the name of the package we still need to copy in our
source code and then we'll set a command using that air package so we'll call Air the
and pass it this default air. toml which I created ahead of time and it just tells it sort of thing
things like where the main file is what command it should run when it needs to rebuild where to look for changes Etc
and so that can be our build Dev stage uh that will be actually I'll just call it our Dev
stage like we did in the docker file and then for the remainder uh we build our production images and then copy them
into that final scratch image so I'll do from uh build Bas as build
production then down here when we copy from build instead we're actually copying from build production
uh we'll add this non-root user within the build production stage and so that should be
good we're now targeting our Dev stage we're bind mounting in our source code we're using that air package to do
automatic reloading uh let's try it out okay it looks like our app has started up so let's uh load here uh we
can see it's running let's make a change to our goang API just to see that rebuild
happening if we go up to module 5 main.go and add something
here we see it detected main.go is changing uh it just rebuilt and so if I reload we get goang woo
awesome I'll save it again you can see it's detected it it builds it I refresh that update is reflected so now we've
got hot reloading within our containers by bind mounting our source code in and then using utilities like nodon vit or
air to automatically detect file system changes and update our application in real
time now the next item on our wish list is remote debug so we want to be able to run a debugger within our containers and
then connect to it from our development environment as much as we love doing print F debugging where we put in a
bunch of print statements sometimes you really need to use a debugger uh and investigate a little more thoroughly so
let's get that set up now what I'm going to do is create a new Docker compose file that I'll call Docker compose
debug and one interesting feature of Docker compose is that you can take two files and pass them both into the docker
compose up command and it will take the first file and then take the second file and
overlay it you only have to define the fields that have changed between the two you can have your base configuration and
then slot in slight differences and so in this case we're going to be able to leverage that Docker composed Dev file
almost entirely but we'll add a few new Fields mostly adding additional ports to publish because we're going to have to
connect on New Ports that the debugger is listening on and modifying the command that's executed when the
container started uh we'll start with our version of
3.9 Services uh the services that we're modifying here are the API layer so
we'll have API node and API goang we want to override the command
field so we'll have a command here and so instead of npm run Dev like we were running before uh we actually
want npm run
debug Docker and I added that as a new npm script so we can go take a look at what that is npm debug Docker runs
nodemon so we still get that hot reloading uh but the Das Dash inspect command tells node to run its debugger
here we're listening on all hosts on Port 9229 so this will enable us to connect from our Local Host system on
Port 9229 as long as we open as long as we publish 9229 in our composed file and then we're passing it that index.js file
as the entry point for the program uh so here we'll do ports and I'm going to have both my normal 3000
Port that I had before as well as my new debug port and so when I run this command if I
do a Docker compose debug build I do Docker compose and then I first pass it my development compose file so that's my
Docker compos ddev and then I pass it my debug compose file Docker compos debug and Docker compos takes those two and
interleaves them together such that these will be the new values but we can still take advantage of all this
additional config here our goang API will be somewhat
similar the debugger that we're running for goang uh runs on Port 4000 it's a tool called delve which was that other
Dev dependency that I installed and our Command I'll just copy and paste it here because it's a little
bit long uh uses that utility in debug mode we pass it the main.go file that should
run we tell it which Port we want to listen on as well as some additional configuration options and so that should
be enough to run our application in debug mode so I can do make compose up
debug build as I showed it passes the dev file first then the debug uh composed file so while that's building
and setting up I can show you the launch. Json configuration that I use and that is the way that VSS code sets
up its debugging utilities so under the configurations block I have two of them I have one set
up for node with Port 229 one set up for goang on that Port 4000 uh the key things to call out here are that I'm
using the request attach mode because my container will already have that debugger running inside I don't need to
spawn a new one I just want to attach to it and then this local root command tells VSS code where the root of this
project is within my workspace and that's so that when I create a breakpoint it's able to map that to a
path within the container which I'm passing here of user Source app oh I see I hit an error here it's
because I forgot to change uh this port for the goang API to uh 8080 so let's try that
again similarly for goang uh we're using a particular adapter that is associated
with that delve again we're using the attach request option specifying the port giving it a a remote
path and then this one's this one was a little more tricky to get set up again we need to figure out we need need to be
able to map from our host system into the Container such that it's able to know when we set a break point
what that corresponds to and properly apply it inside the container and so the substitute path
command takes the path on the local system and modifies it such that it gets fed in properly inside the container
my app is now running in debug mode I don't have any breakpoint set or anything so it just loads as normal
however I can go into the run and debug portion of my uh code editor and pick one of these so let's pick attached to
node click the play button aha uh it was not connecting because it was
forwarding my debug request to Port 3000 where the app is listening and so instead I should be forwarding to
the port where the debugger is listening now that the API have started
up I'll click play and now it did successfully attach we have our breakpoint in in
our source code just before it sends that response back if I reload here we see it caught at that break point and we
can see all of the local variables including the response itself Wednesday February
23rd API node 1257 so if I then click skip over the breakpoint or play it will resume and
allow that to be returned from the API so let's click play and we see we got back that
response I'll click disconnect here and let's try the same thing with goang click the play
button looks like we were able to attach let's load our goang API and set a breakpoint somewhere within
our goang code under main.go uh let's do right before we send back our data
breakpoint we can see it being injected here in the logs and now if we load the page once again we successfully caught
at the breakpoint uh and we can see the locals here uh we have our time data as well as our gin
context and now we have debugging working for both uh the goang and the node-based
API uh I also added a react query debugging tools as a de as a Dev dependency that get built in
here allows us to get all sorts of information about the uh queries that react query is making to the back end
uh so for example if we load here we can look at this query when it was last updated uh
the data that we got back we can tell it to refresh refresh the data and so without having to reload the whole page
we can interact with the react query specific components directly that gives us debug
capabilities for all of the services that we're building so that's awesome and now the next thing our wish list was
to be able to execute tests with inside the containerized environment similar to how we had a
overlay compos file for uh debugging I'm going to do I'm going to make us another one called Docker compos
test uh we don't need the port access we will be overwriting the command
uh we'll just execute our npm run test script which will execute just which in turn looks for our test so I can just
show you the test Suite that I have set up it's it's quite minimal so API node test we have one example test that
expects true to be true so that's always going to pass and the package.json specifies uh test calls just which will
discover the test and execute it and then for goang similarly
minimal uh I have a test module with exampl test. go uh it tests that 1 equals 1 which indeed
it does and my command that I'm going to use there is go test so here instead of
using delve I'm going to run go test and then I'll pass it this V option which will enable it to properly
discover those tests so now in order to run this I have two make targets set up or I have uh
Docker composed calls for goang and node separately so again I'm going to use the dev compos file first overlay that test
compos file onto it and then I can specify run build and a specific service within that compose file and so this one
should execute the goang test and this one should execute the node tests so if I do make run
tests it's going to build those images and execute those tests so we see the just test passed and
let's see let's go find the yeah here's the goang test here so the goang test pass the just test passed and we were
able to execute those tests within the containerized environment the next item on our wish list was
continuous integration uh and so that essentially refers to this idea you whenever you're pushing code to your
virsion control system you should be running pipelines to do things like execute your test Suite uh build and
push your images Etc GitHub actions is a continuous integration system that's built into GitHub and you can just add a
couple of files to your repo and it makes it very easy to set up this type of thing I've added a workflow to the
repo that I can show here that is going to do a few things it's going to build our image it's going
to scan it tag it and push it to dockerhub um I'm only doing this for one of the services just as an example but
you can use the same Concepts to add it to as many services as you want within the repo you place GitHub
action files at the GitHub workflows directory and then any yaml file in here will be interpreted as a workflow with
about 50 lines of code here we're able to configure a GitHub action that will build our images scan them test
them Etc So within this file we have a number of different sections I'll just walk through them now this first section
starting with on tells GitHub when it should run this workflow in this case I've specified you want to run it on
push events to specific branches and tags so if I push to the GitHub action branch which I'm on now uh it will
trigger this workflow or if I push a get tag with starting with the letter V so maybe it's vx. y.z so some sver version
when I do a release you can also do other types of events like a cron string so you would run it periodically based
on a schedule or upon creation of pull requests below this you define your job section where you have the different
jobs that you're going to run and then within each job you can have a number of steps and so I've only defined a single
job I'm just going to build tag and push my image you specify which uh runner you want to run on I'm running on auntu
latest and then the steps that I'm going to run are executing first checkout which is a public action provided by
GitHub which checks out the code I then am using a public action from Docker to generate a bunch of tags that I want to
use for my image using information such as the name of the branch the name of the pr the version if I'm pushing a tag
I can use that to generate a Docker image tag and then finally I'm using the date and the commit hash as another
example of a potential tag that I might want to use like I talked about when we were looking at container Registries
here I'm giving it the name of the repo that I'm going to push to and these actions these public actions that I'm
referring to are available in the GitHub action Marketplace and so this was an action
that was produced by Docker um you can see the source code here for it but if we go to the action
Marketplace uh we can see here it is it tells us all the different ways we can use it the types of inputs the types of
outputs Etc so I'm able to specify that this step should use
that action give it these types of tags and will automatically generate those for me next again I'm going to use a
public action from Docker to log in dockerhub and within my repo I can specify my
username and my access token such as it's table to do that so within the
repo within the repo under settings under secrets and variables actions you can
specify environment Secrets or repository secrets and so here I specified my dockerhub username and a
access token such that that action is able to log in next up again another public action from
Docker so really I'm I'm leveraging their work to make my life super easy this one is going to point to the file
that I want to build as well as the context with which I want to build it I'm saying I do want to push it to
dockerhub and then I'm using the output of my metadata step here and applying those as tags so this is going to be a
listing of all those tags that are generated this will apply them all and push them all and then finally I'm
leveraging a tool from Aqua security using their vulnerability scanner to look in the image for any critical
vulnerabilities you can specify what level you want to block on here and so I'm saying if there's any critical
vulnerabilities in the image fail the run and that way I can go investigate uh if there are no critical vulnerabilities
it will pass the run so I haven't made any changes on this Branch uh but I do want to trigger a run so I'm going to do
an commit I do get commit DM empty allow empty get push and now if I go to
my repo under actions we can see that empty commit just triggered a
build here's my job if I click into it I see it starting up that Runner and and then each of my steps we
can see it's checked out my code it's generated the tags so let's look at the tags that it generated we've got GitHub
action is the branch I'm on uh latest is just the default and then this is the this is today's date as
well as the short hash from the commit we successfully logged into Docker Hub we then successfully built
and pushed our image we ran the scanner and found no critical V vulnerabilities and then there's just some cleanup at
the end so now let me go to dockerhub we should be able to see for our API node
repo this version was just posted a few seconds ago and so that kind of highlights The Continuous integration
workflow that you would want to set up for your container images that's just a taste of what you
can do with GitHub actions and with continuous integration pipelines uh Brett fer has a repo called Docker cicd
automation that has a whole bunch of really cool examples around uh doing things not only building and testing and
pushing your images but actually interacting with PRS adding comments to those PRS with the results of the tests
and that sort of thing you can see his talk that he gave on this on YouTube I would definitely suggest checking that
out now the final topic that I want to cover here in the developer experience section of the course is ephemeral
environments ephemeral environments are this idea of building buing and deploying short-lived isolated
environments that we can use for any sort of purpose including testing validation QA Etc and in particular I'm
going to use this platform from Shipyard to very easily set up this for our sample application I'll start by just
walking through their onboarding process and I'll show you with just a few lines of configuration to our Docker compos
file we can have ephemeral environment set up with every new PR to the repo I'm here on the shipyard dobu website I
click log in It'll ask me to log in with my gift Hub I'll do that you'll authorize Shipyard to access your
account this now redirects me back to the shipyard site where I will connect my GitHub
organization so that initial connection just gave them access to my user information now this is the
authorization to actually access my repos you can choose either all repos or only select repos so here I'm going to
pick the specific repo that we care about which is the docker course repo this will grant them the necessary
permissions to the contents of the repo they also have features where they can provide information back on the GitHub
interface that's why it has read and write access to checks commit statuses and pull request at this point I'll just
click install now as we can see we're on the free trial period as I mentioned during
the introduction students of this course have a exclusive promotion code where the first 300 people to sign up can get
an extended trial period so go check out the description for more information on that and then at this point we can click
the we can click this scen button to add our first application we'll select the repository
that we care about it's our Docker course repo and for our base environment I'm going to use the main
branch we can use the default name for application and now if we click the select Services button it's going to
scan the repo for a Docker compos file in this case because of the structure of the repo based on the course it wasn't
able to find specifically which one we want I'm going to choose the one that we set up initially in module 8 because
that's the simplest Docker compos file that contains all the services we care about I'll just click
select and now we need to pick which of the services in that Docker compose file we want to include here I'm not going to
include our V based deployment instead I'm only going to have the engine X based front end um we'll leave on our
two apis in the database here and then we can see that it automatically Det Ed our named volumes
and an interesting feature here is that because we have that named volume whatever's stored in that volume on our
main environment will get snapshotted and copied over to uh additional environments as we make PRS against
that at the top here it's saying please include at least one service with a route and so we need to provide Shipyard
a bit more context and configuration so that it knows how to route traffic to our various services and the way that we
do that is by adding labels to our Docker compos file so let me jump over to the code code editor and add in just
a few lines to our Docker compost file so that Shipyard will be able to properly detect and utilize those
routes and so each of our services we can add a labels field and so here under the engine X client react enginex field
I'll add a labels Target and add these two labels the first one is specifying that this is the primary route this is
the route that we want traffic on the root path to come into uh and then we're specifying the route as just the forward
slash so this is saying this is going to be the default service that we want you to Route traffic to and so with those
two things that's good um we'll adapt for our API node Service uh we'll add this
this is not the primary route so we'll get rid of that and we will add this going
to slash API slash uh node just like we had before and then similarly for our API
goang we'll go here uh and just replace node with goang and so with just those I think it
was additional seven or eight lines of uh code into our Docker compose file we can now commit that to the main
branch push it now if we go back to Shipyard and reload our latest
commit now we can see that it automatically picked up those labels and it's including those in the information
about our configuration here with that I'll add environment variables all the default values here
should be fine we don't need any additional environment variables and click create
application this is now automatically going off and building all the containers based on the latest commit to
that main branch it will then bundle them up and deploy them now you'll notice that the first
time you log into Shipyard you'll get this message at the top that's because it's provisioning a standalone
kubernetes environment for each customer uh and that allows for isolation between customer data however it does take a few
minutes to set up and so the very first time that you you're here you'll have to wait about 10 minutes after
that the build and deploy should be much quicker now while we were waiting realized that I should have added one
additional configuration here uh for our API layer and so that is that is the shipyard route rewrite
label and this is saying if we hit the URL API node our node service is expecting that to come in at the root
path and so by adding this label it will strip out that prefix uh and we'll do the same thing for goang because the
routes that I've defined inside of my services are not expecting that prefix so I'll add those and push
them now another neat feature that we can turn on is if we go here to this uh three dot menu and click under
configure under notifications and general settings we can add GitHub commit checks and GitHub PR comments so
the when Shipyard has information for us it will populate that back into GitHub so that we can see it from there we also
can tell it which pull request we want to deploy on so we could have specific labels let's say we only want to deploy
on bugs or we only want to deploy on enhancements or we can specify a r a regular expression to decide which
branch names we want to deploy with in this case I'm going to leave these as the default so that should deploy any PR
back to the main branch I'll click save there's also a slack integration you can use uh to get updates and notifications
from chipyard I'm not going to go into that now but I just wanted to call it out as an option a few minutes later
back on our home application page we can see that our base environment has built and is ready to be visited now you'll
also see this little timer here that says stopping in 2 hours and 49 minutes so that's a feature that is called SLV
or since last visit and you can configure that within your organization to tell Shipyard how long an Emeral
environment should live and by putting environments to sleep if they're not being used allows you to have more
ephemeral environments on a given tier within their platform if I click the visit button it's going to take us to
this page where we get a login prompt so we can log in either using GitHub or Google for ooth and this allows you to
have these ephemeral environments not public on the internet but actually protected behind this login layer where
you can very finely control who has access to it so I'll click to log in with
GitHub and we can see we've got our application running within their environment and if you look up here at
the URL we see our custom URL that's associated with this particular ephemeral environment now if we wanted
to if we wanted to Grant access to this we could go to the visitors Tab and under here I can add GitHub usernames or
Google usernames and that way you can control who has access via single sign on so that's our base environment now
let's make a slight change to our application and then redeploy base on a pull request event I'll go back to my
code editor uh and let's have it deploy with a different message on that home screen so I'll go into my example web
application here on the front end in my app.jsx file I'll scroll down and instead of hey team we'll say hey team
from Shipyard and we'll change the wave into a ship awesome uh now I'll do get checkout
DB Shipyard demo and I'll commit to this new Shipyard demo
Branch change message now I'll push that now if I go over to GitHub I can
see that recent Branch had a push I'm going to create a pull request from that Branch back to the main
branch if I click create pull request and now jump back over to Shipyard we can see it immediately detected that
pull request and is now building a new ephemeral environment specifically for that new Shipyard demo
Branch once it finishes building we'll be able to see that is different than the original Emeral environment with
that updated message clearly a change that small is quite trivial but it could represent any number of complex changes
to our different Services we could be changing the API layer we could be changing our things like our database
schema let me go here to the build details dashboard and show you how that's progressing it shows us the
various services that we have within our application that we selected initially we can see the build logs as it ran our
Docker build commands and looked within the docker compos file found the corresponding
Docker files and built those out we can see all the logs here across our different
Services now under the deploy tab we can see the pods as they come up and then under the Run tab we can see
the logs coming out of the containers themselves it's now handling that final DNS setup so that we'll be able to
access the environment and now we have our second ephemeral environment live let me visit
that one as well as we can see it's has taken that change that we made in that PR and
deployed it in a separate environment from our base environment there's an additional interface here if we click
terminal it'll pull up a way for us to get additional information about all of our environments so we've got our main
environment we've got our Shipyard demo environment let me just click into the main one and we can see the pods running
here within that environment I can choose one of them if I select it we'll get the logs here at the
bottom we can also hit the r key if we wanted to exec in and do some debugging that
way and so now this is a shell running inside that pod within the environment I'll exit out there's also a
CLI that I can run locally to get a lot of this functionality on my host system directly from my laptop let me jump over
and show you that CLI real quick to install the CLI uh they provide either a bash script that you can use or you can
use something like Homebrew to install it I've already got it installed and then the next piece is to get your
Shipyard API token and so in order to turn on the CLI for your account you'll need to ask the team at Shipyard to do
so but once you do you can go up here to the user and then click profile and it will show up on that page and you can
grab that API key to use with the CLI we can see here all the different functionality that we have so I can do
something like Shipyard get environments that's going to list out all the environments that I have and
let's say I want to get the logs from one of my pods like I could see from the web interface I can do
Shipyard uh Logs with the N flag I can pass The UU ID of that environment and then with the service flag I can tell it
which service I want so let's do API uh node and we can see the log's coming out of my node based API application uh
similarly we could port forward directly to one of those applications so let me just port forward to that
one so instead of logs I'll do port forward uh and then my node application is listing on Port 3000 so
I'll do 3,000 uh it wants the port flag and now my Local Host 3000 should
be forwarded to that node-based API and I can go here and I'm connected directly to it
this would be super useful if I wanted to do something something like uh maybe connect to my postgres database directly
with PG admin or otherwise I could port forward to that environment uh make whatever changes I needed to let's say I
was building out my base environment and I needed particular seed data and I wanted to run my migrations I could do
that manually once on my base environment and then those changes would get snapshotted because they're in a
named volume and built into my additional environments moving forward so hopefully this gives you an idea of
the power of ephemeral environments and the ease with which we were able to use the shipyard platform to generate them I
created That Base environment from my main branch and now every PR that I create moving forward we get an
ephemeral environment to validate before we deploy to staging or production if you a call we turned on the ability for
Shipyard to populate information back on GitHub so let's go see what it did on that PR if I go under pull requests demo
Shipyard we can see that the shipyard app created this comment and tells us the environment was successfully built
and running we can access it at at this link or see the build details at that link and so this just makes it very easy
for someone reviewing this poll request to now go check out what exactly these changes are and how they impacted the
application hopefully that demo kind of give you an idea of the power of ephemeral environments and why we would
want to use them within our software life cycle and showcase the ease with which we were able to use the shipyard
platform to build those out for our project at this point we've got all the tools within our system to have a really
nice developer experience with containers and now now let's shift gears and actually learn how to deploy them
into the cloud all right congratulations we've made it to the final module of the course and that will be focused on
deploying containers so we now know how to build container images run containers from those images we've learned about
how to do development with them and now we need to build on those skills and learn how to deploy our containers to
production now one of the main benefits of containers is that they provide the standard interface upon which to run
as long as you can provide a container image that meets that open container spec there are many cloud service
providers that will happily take that container image and run it for you I saw this article back in 2021 from Corey
Quinn who runs the last week in AWS newsletter talking about the 17 different ways to run containers on AWS
now obviously AWS is kind of this sprawling massive cloud provider but even still to have 17 different ways to
run containers uh ranging from container-based serverless options like Lambda uh to elastic container service
to running kubernetes with elastic kubernetes engine uh there's just so many different ways to run containers
and this is just within one cloud provider and so that portability is both a blessing and a curse meaning that it
provides us this flexibility but also it can be a bit overwhelming in terms of deciding which of these methods to use
and now what do we actually care about when it comes to choosing a method to deploy our container based workloads I'd
say the first item that's top of mind for me is security and whether either the cloud provider is handling security
out of the box and setting all those container runtime options such that it meets the threshold we need or they give
access to those knobs and dials that we can set them ourselves the next one is the ergonomics or the developer
experience so how easy is it to go from uh having a set of container images and running those and getting traffic from
the internet to them also how easy is it to deploy new versions so if I make a change to my code how does that code
make its way into production and then the third one in terms of ergonomics is how do I work with the running
containers how do I check the logs uh can I monitor them can I collect traces easily combining those three elements
make for either a good or bad developer experience when when working with any of these platforms the next one is
scalability so does the platform meet my needs in terms of the scale of any individual container or the number of
total containers required for example can I access particular types of gpus uh what if I need to scale beyond the
capability of a single host can I launch containers across multiple hosts and how do those Services then communicate with
one another another key consideration is persistent storage so we've talked about
vol volumes within Docker we've mostly focused uh from a developer perspective with Docker desktop and seeing how those
volumes get stored on your laptop or desktop uh however if we're now running containers across multiple hosts and
they're getting scheduled and they could be scheduled on one versus the other we need to think
about how storage works and if it's possible for those storage to be accessed from different virtual machines
or different physical machines and the last one that I have here is cost how affordable is it to run your containers
in this particular environment these range from sort of bare metal where you're paying just for the compute but
handling all the operations yourself all the way up to maybe a fully managed platform as a service offering where
they're charging you a premium for some of those improved uh ergonomics or developer experience features so
thinking about how much it's going to cost you understanding the financial impact of your platform will be
important if you're running a business based on these containerized workloads now when I was planning out
the course uh I actually built out three different deployment targets so one using railway. apppp one using Dockers
swarm and one using kubernetes uh since we're already at about 4 hours into the course I've decided to shift that
slightly uh within this video I'm going to demonstrate the docker swarm deployment and then I'm going to create
separate bonus videos uh showcasing how to deploy onto Railway and how to deploy onto kubernetes for the the sample
application that we've been working with uh and once those bonus videos are released I'll have information in the
description about how to access them now at this point you might be asking why can't we just take the docker compos
file that we created in module 8 uh create a virtual machine and run that directly now while that would work and
you would have your containers running on that host and you could set up the firewall to access them it's not ideal
there's some major limitations with Docker compose uh that prevent us from wanting to use that for production
workload for first there's no way to deploy a new version of our containers without having to stop and then restart
the container and so by definition you're going to have some downtime there it might be very quick on the order of a
second or two uh but nonetheless you generally want to avoid downtime if you can and then also there's no utilities
for easily rolling back a version uh with Docker compose directly also Docker compose uh doesn't have a native way to
handle credentials properly we've been passing in the that database URL or the postgress password as an environment
variable there's no there's no mechanism in Docker compose to handle encrypted secrets and so that's a major limitation
there also you can only run Docker compose on a single host uh so that hasn't been an issue as we've been
developing on our development machines however we would like the ability to expand Beyond a single host and add
things like high availability in the future and so Docker compose is limited in that way there's two other reasons
you might have historically heard cited for not using Docker compose in production uh the first is that it used
to not support health checks and so we'll talk about what health checks are and add some in that is no longer true
they've added health checks to Docker compose relatively recently and then the other one has also been mitigated is
that Docker compose used to live as a separate binary uh when you would run it and so it was just harder to install and
config now that Docker space compos rather than Docker Dash compose is built into the docker binary itself that's no
longer an issue either you may hear people reference these as potential reasons just know that historically they
were reasons not to use Docker compose in production now I've phrased these as things that Docker compose doesn't have
however as we'll see these are features that Docker swarm does have built in we can configure a deployment strategy to
have zero downtime deployments we can roll back changes if we want to we can handle credentials via a feature called
Secrets uh and we can scale a swarm cluster across many hosts to keep things simple I'm going to deploy a swarm
cluster on a single host uh so it's going to be quite similar to the conceptually to using Docker compose up
uh we're going to create a swarm cluster consisting of a single virtual machine and then we're going to modify our
Docker compose file and you'll see that the actual changes in the configuration are quite minimal uh there's only a few
things that we need to tweak to make our composed file compatible with swarm uh we're going to deploy that and then
we'll set up DNS so that we can access it via the Internet what's the process going to look like I'm going to create a
virtual machine I'm going to make sure that the firewall set up for that virtual machine allows inbound traffic
on the necessary ports we'll install Docker engine and so I'll use the script located at get dod.com I'm going to
continue to use the docker client running on my laptop but connect to Docker engine running on the virtual
machine using this Docker host environment variable so we'll connect over SSH and have the client running
locally but Docker engine running remotely we'll initialize the Swarm we'll modify our Docker compose file to
add health checks uh this allows swarm to periodically ping our service and make sure it is healthy and if not
replace it or restart it we're going to add a deployment strategy which allows us to deploy with zero downtime then
we're going to add support for secrets so that we can handle uh the credentials for that database properly finally we're
going to build and push our container images to dockerhub previously we were using Docker comp osed to also build our
images for production based setup we want to have a specific image tag that we're running and so I'll build and push
those and then finally we'll deploy that stack onto the virtual machine and see it
running now at this point I also want to issue a slight disclaimer about the way we've configured the database here we're
going to be deploying our database alongside the applications into our Docker swarm cluster now that's
perfectly fine to do and we do have a Docker volume to persist the data such that it will uh exist Beyond container
restarts however we're not implementing backups or anything like that and so if you have an application where your user
data is very important you'll want to either Implement a more robust setup that includes periodic backups and the
ability to restore from those or move the database to something like a database as a service from one of the
cloud providers Amazon has their RDS service Google has Cloud SQL Etc okay so I'm just going to create a virtual
machine here on SEO you can use whatever cloud provider you prefer I'll call it Docker
course swarm we want one instance I'm going to choose the small machine type I'll use
auntu uh let's use the auntu initial user uh we can use the default Network um we do want a public
IP address uh we will create a new firewall and we'll use the SSH key that I have already uploaded to my CFO
account I'll click create that will take a couple of minutes
to spin up uh let's look at the firewall here so right now it's using uh default default I just want to make sure that I
have inbound traffic allowed on Port 80 uh so I have this Docker test firewall that I created uh when setting
things up let's look at the the rules uh so this is allowing inbound traffic on all ports let's actually turn
that off uh we do want to be able to ssh in on Port 22 uh we're going to have web
traffic coming in on either Port 80 or 443 uh let's also open up access for our two API ports uh so in
this case it would be Port 3000 and Port 880 for the goang API just so we can access those
directly now let's go to our VM and attach this firewall to it so instead of default default I want
to attach it to Docker test uh and now I want to install Docker engine on this machine so I will SSH
into it uh so we created that Ubuntu user it should be using my SSH key that I
already have set up the first time connecting you have to accept the fingerprint and now we are connected to
that machine if I just try to run Docker it doesn't exist however I know that
there's this install script for Docker engine here at get. do.com so I'm just going to
do curl on that pipe it to Shell and this will go off and it has conditions within it to install for
Ubuntu or Debian and so this is a Ubuntu machine and it should install properly and so Docker is installed but
if I try to do something like Docker PS I'm unable to connect to the docker socket I need to change the permit of
that socket to be accessible by my current user and so I can do that with the CH own
command and now if I do Docker PS I'm able to access Docker so I'm going to exit my SSH
session uh however I can still connect to that remote Docker engine by using by using the docker host envirment variable
so I'll export Docker host uh and in this case it will be Ubuntu at at that IP address and now even though I'm
running this command Docker PS on my laptop it's actually talking to the API of that remote Docker demon we want to
enable swarm mode on that remote VM so that's as easy as doing Docker swarm in
it and we now have swarm running on that remote host next up we want to take our Docker
compose file that we created in in module 8 and update it such that it is ready to go with swarm so I'll snag that
Docker compose file copy it over into module
12 and so what are the modifications that we want to make here uh we could just run this directly like I was saying
however there's a number of improvements that we want to make so for a production based system uh and let me actually
rename it instead of Docker compose let's call it Docker
swarm am uh there's a number of things we're going to do first of all we don't need
our V based front end because that was a development server anyways so we'll just get rid of that entirely uh next up
we're not going to be managing uh the building of our container images via swarm uh so I can get rid of this build
section and instead I created uh repositories on dockerhub for each of my images and I'm going to
build and push those images to that and so we can see here I've got my Docker course API goang API node and client
react engine X in order to build and push my images to those I have this make Target uh that is going to navigate to
module 6 and then build version 5 9 and8 of the client node and goang uh Services respectively and so that's effectively
going to go into here into each of these directories and execute the build and and push and make targets and so if I do
that uh this image that I'll be using for the front end will be Sid palala devops directive Docker course client
react enginex version 5 the API node once again will clear out the build portion uh and we'll specify to
use the image and this will be Docker course API node 9 was our final uh Docker file and then the final one uh
API goang will be goang number eight uh clear out the
build uh we're going to take inbound traffic on Port 80 and forward that to 8080 CU that was that unprivileged
engine X container running on 8080 uh we'll take traffic from 3,000 forward to 3,000 880 on the host
go to 880 in the Goen container let's run this config file using Docker swarm on that remote host
we already initialized the Swarm uh in this case I need to update this IP address so
this was the IP address of the virtual machine I used when testing now I'm going to set it to the IP address of the
one I just created uh we've already initialized I'm going to use this uh Docker stack deploy command uh and then
pass it the name of my uh swarm file so I'll do make
Docker F swarm deploy stack uh because I didn't specify a composed file version in that file I actually need to do that
and so at the top here I'll specify that this is version 3.7 let's try it again uh and it's warning me that the
docker compose option restart is not supported by Docker swarm uh and so I'm just going to remove
those it will handle restarting our services slightly differently uh and the great thing about something
like doer swarm is it's declarative so now I can just redeploy the same stack over the top and it will reconfigure my
services accordingly now if I do Docker PS we can see the four containers that
it's spinning up uh and let me try to access them uh just at that IP address so the IP address was
212 all right and so I now have my containers running remotely on that virtual machine using Docker SW however
there's still some improvements that we need to make right I talked about handling of credentials here I'm still
passing in those credentials via this environment variable so we need to fix that uh by using a feature called
secrets and so generally you would create these secrets via some other process so you wouldn't Define it in
your config directly because you don't want to store those credentials with in your get code base and so I have a make
Target that we can use to create those Secrets um it essentially uses the docker secret create command uh pass it
a name of the secret and then here I'm echoing in the string that will get stored within that
secret uh so if I do Docker secret list there's no secrets currently but
then if I run this make Target and run it again Docker see secret
LS we now have these encrypted Secrets living on that virtual machine that contain the information that we care
about so food barbaz as our password uh and then the URL string containing that information as
well now in order to consume those Secrets uh we're going to add another field here at the
bottom called Secrets uh the first one is database
URL and we're going to going to say external true and what external true is saying is that we created this secret
already and Docker swarm doesn't need to manage it as a part of this stack we'll do the same for
postgres password external
true great and so now that we've told Docker swarm about those secrets we need to tell the services that are consuming
them to do so and so what I'll do here is we can tell this database service that we want to consume the
postgres password uh we no longer need the postgres password here I will change it I will specify the postgres user to
be postgres so the way that Secrets get consumed is is as a file mounted into
the file system um using a a temporary f a temporary Mount structure where that file only exists in
memory and so I needed to adjust my application slightly whereas previously they were expecting to read those values
from an environment variable now I'm expecting to read them from a file and so I need to tell the application where
that file lives and so in this case uh that file will get mounted in to to this location basically under SL run/ secrets
and then the name of the secret and so I'm going to tell my application this is where you should go look and read in
that that password from the file uh similarly we will have for our apis something very similar
so we're telling it we're telling it the path to the file where that Secret's going to be mounted in uh in this case
this will be database URL and there we go and then let me just
show you real quick the modification that I made to the application so that it can either read those sensitive read
those Secrets as environment variables or as files so if we go to our node.js application here Source database and so
when I'm populating this database URL variable I either load it from the environment or if that doesn't exist
then I read from the file system at this path that I'm passing it very similar on the goang side
where when I'm initializing I either get the database URL directly or if it's empty then I'll read the file and use
that instead and so now I can redeploy using those secrets
now also it takes my swarm config file and turns that into a stack with many services in it so I can do make
swarm LS and that's going of list the different services and we see that we have our different replicas
here and they all appear to be running except for the goang one let's reload our
application and so it looks like our front end is still working but our apis may not be so if I navigate
to and so let's take a look at the logs for those containers to see what might be going
on let's look at the node API logs uh it looks like we are refusing to
connect So within the the node container I can print the environment I can see that we have this database URL file that
looks good um what happens if I C that file out let's see uh okay so it looks like
within so it looks like when I tried to use this dashn option for the echo command to avoid new lines it actually
included that in my secret file which is what I didn't want to happen and so I'm actually going to remove my stack make
swarm remove stack so it looks like the dashn I was using to remove the new line at the end
of the file was actually getting included in the secret so I'm just going to use print F instead do make create
Secrets uh and now let me redeploy my stack let me uh make sure that the secret looks okay within
those within those containers uh so in this case let's cat this out within the postgres container
that looks good and then within let's say the goang container or no we'll do it within the node container because the
goang wouldn't have cat within [Applause] it okay so that looks much better and
now do my services look healthy uh they do and if I go to my IP address address and refresh I can now
access my application again okay so minor dour there uh how I was creating those Secrets there was an issue with
the that dashn was making its way into the secret itself and so I fixed that by using print F and now the secrets appear
to be working properly now the next thing that I wanted to do is to add a an ability to
deploy new versions without bringing down the old version first and so for that we actually use the
deploy configuration option and so for our front end we will add a deploy section and within this section there's
a few things We'll add we'll put it on replicated mode with a single replica
so we could change this to 10 and it would spin up 10 copies of our front end but we just need one for now uh and then
the key configuration here is update config we want to start the new container before we terminate the old
container so that's what this order says you can actually do stop first if you want to do the reverse and so this is
how you this deploy configuration is how you tell Docker swarm how you want to do rolling
updates uh we'll have pretty much the same configuration for our other services as well
uh let's make the goang application have two replicas just to show how easy that is to
do and then the final piece of the puzzle that we wanted to add is health checks uh and so we want to have a way
for Docker swarm to know whether our containers ready or not so that when it's checking should I bring down the
old container is the new one ready to accept traffic the way that we do that is via health
checks now the health check config uh is relatively straightforward you use the health check key you give it a test and
in this case we're saying run this command inside of that container so it's going to try and curl uh Local Host 8080
and then I've configured my engine X configuration so that when you hit uh the slping route it will just return
pong you then tell it how frequently you want to issue this health check how long you want to wait before you consider it
to be timed out out and how many tries before you consider it to be a failure finally the start period is how
long you expect it to take before the container is ready from initial boot up uh before it needs to do this test in
this case our container image already had curl built in so there was no additional uh changes to the application
that we needed to make for the API node service we'll have a very similar health check I'll add it right
here um in this case though we're not going to be we don't have curl installed into our container image we wanted to
keep our container image as minimal as possible we didn't want to have extra utilities floating around and so in this
case I actually defined a health check in JavaScript that's going to be run from within the container and tell us if
the container is healthy or not I'll show you what that looks like so under API node under health
check uh uh we're using the HTTP package uh we say take it up to 2 seconds to Ping the slping route on Port 300 if we
get back a 200 code great if we don't then fail and so this is what is getting run
by that health check inside the container every 30 seconds for goang it will be almost
identical uh in this case I have a separate binary that gets compiled in my container
image and so if I look at the application I have this health check
package within here again I'm just pinging Local Host on the route ping I have that defined in my application to
return a 200 and then in my Docker file when I'm building my application I build both my
health check as well as my main.go and so these are two separate binaries that live in my container image one of which
is the application the other one gets run by this health check okay so with those health checks defined
uh I can now redeploy my swarm stack and now I can list out those Services we'll see something interesting
though if I do a Docker PS we now see under the status field see that these have status of starting
meaning that it's using that health check to determine whether or not the container's healthy or not and until it
is it won't Route traffic to those containers and it keeps the previous containers alive such that they can
continue to receive traffic now that those health checks have started passing we can see that
they've been up and they are now healthy and so we should be able to continue to access our
application with no downtime and so now whenever we make a change we would build and push that container
image update our Docker swarm configuration if needed redeploy and drer swarm would be
intelligent enough to use those health checks to determine when we can Route traffic to the new containers or not
you'll also see that because I have two replicas specified in my config we get two containers both running that goang
image a couple of other additions that we could do uh we could configure our engine X front end to retrieve a
certificate from let encrypt for example and store that in a volume such that it can persist across container restarts
because right now we're serving traffic on Port 80 over HTTP unencrypted uh if we go to https we're not serving on 443
so that doesn't exist uh we also can update our DNS settings for a domain of our choosing and so for
example we can go to cloudflare uh under devops directive I already have the setup but I'll update it to be the
new IP address and so we can update this and that will take a few minutes to
propagate but because I already had the old one set up we can just navigate to it directly
and as you can see we're now routing traffic from that domain to our virtual machine hitting our application running
within darker form and everything's behaving as expected you'll also notice that here I
do have https and now this is a little this is a bit of a hack because it's using Cloud flares flexible TLS and so I
have this set up such that encrypted traffic goes to cloudflare that's where it terminates TLS and then it sends that
request unencrypted to the origin server and so if you're not dealing with particularly sensitive information
depending on your threat model this may be sufficient however if you are dealing with sensitive information or there's
requirements about the data that you're handling you would want to terminate TLS on your system uh this can be done like
I was saying you could have engine X go off and get a let's encrypt certificate uh at its first startup uh you also can
you can also handle that kind of at the Ingress layer for your doer swarmm cluster uh using something like traffic
or another Ingress controller that can handle that and so with that we have our application deployed to a virtual
machine running in the cloud being managed by Docker swarm we've set it up to use secrets to handle the credentials
properly use health checks to allow us to have zero uh downtime deployments uh and so
that's great for a single node if you want to add High availability to your system or or deploy to multiple nodes it
starts to get a bit more complex especially around how it manages volume so in this case we have a single volume
for our postgres data on that virtual machine if we wanted to have a multi-node cluster we would need to look
at volume plugins and how to manage those how to allow Docker swarm to manage those across multiple hosts and
so I didn't want to go into that level of depth in this course if you do want to dive a bit deeper on Docker swarm
there's a great course from Brett fiser called Docker swarm Mastery that I would suggest you take a look at that'll be a
full Deep dive on how to use swarm there's a recent reinvigoration of excitement around swarm They just added
support for uh what's known as the CSI container storage interface which is a standard interface for managing
persistent storage that is used in kubernetes has a lot of usage and maturity from that domain that's now
going to get pulled hopefully back into swarm and make it a viable option for production deployments moving forward as
I mentioned this is just one of many many different options for deploying containers um hopefully this gives you
some ideas but do be on the lookout for the bonus content around uh how to use something like Railway or kubernetes to
deploy the same sample application and with that we've reached the end of the course congratulations
hopefully you feel ready to go off and incorporate containers into your software development and deployment
approaches and use the lessons you've learned to improve the software systems within your organization to recap
everything that we've covered we started with some fundamentals exploring why we should even care about containers in the
first place how the evolution of virtualization brought us the current state of containers the underlying
Technologies within Linux that enable containers to work and how Docker fits into the picture with that as our base
we were then able to move on to learn how to use containers we used public third party containers for things like
databases and interactive test environments we built out a multi service web application containerized it
established a nice development workflow and then finally deployed our application to the cloud I hope that you
enjoyed taking this course as much as I enjoyed making it also if you'd like to join a community of other devops
practitioners I run a Discord server with lots of amazing folks where we talk about this type of thing all the time
and answer each other's questions there's a link in the description if you'd like to join if you want more
devops and Docker content YouTube should be recommending another video over there that's it for this course and remember
just keep building
You will learn Docker fundamentals (motivation, history, and underlying Linux technologies), how to run and build containers, optimize container images (using multi-stage builds, layer caching, and minimal base images), manage multi-service applications using Docker Compose, set up development workflows (like hot reloading and debugging), and deploy to production with Docker Swarm (including zero-downtime deployments, health checks, and secrets management).
You need basic familiarity with web application development and an understanding of basic Linux shell commands. No prior Docker or containerization experience is required.
The course is divided into three parts: Part 1 covers foundations (why containers exist, Linux kernel features, Docker Desktop installation, and running first containers). Part 2 builds containerized applications using a three-tier demo app (React, Node.js/Go API, PostgreSQL) and teaches Dockerfile creation and optimization. Part 3 covers production deployment with Docker Swarm, including zero-downtime deployments, health checks, and secrets management.
The course teaches image security (using minimal base images like Alpine, scanning with Docker Scan or Trivy, pinning exact versions, multi-stage builds) and runtime security (running as a non-root user, dropping unnecessary capabilities, limiting CPU/memory, using read-only filesystems, and implementing secrets management).
Docker Run is a command-line tool to run a single container with options like -d, -it, and --rm. Docker Compose uses a declarative YAML file to define multi-container applications, making it easier to manage complex setups (like the three-tier app) compared to multiple docker run commands. The course covers both methods.
The course primarily covers Docker Swarm for production deployment, including health checks for zero-downtime updates, built-in secrets management, and scaling from single-node to multi-node. It also includes bonus content on alternative deployments like Railway (PaaS) and Kubernetes, along with CI/CD using GitHub Actions and ephemeral environments via Shipyard.
The course uses a three-tier web application: a React frontend (with Vite for development and Nginx for production), APIs implemented in Node.js and Go, and a PostgreSQL database. This app is used to teach Dockerfile optimization, Docker Compose for multi-service orchestration, and production deployment strategies.
Keep this summary
Save it to LunaNotes and it becomes a real note in your library — editable, searchable, and ready to turn into flashcards or a diagram. Free to start.
Save to LunaNotesOr summarise for another video.
This summary and transcript were automatically generated using AI with the Free YouTube Transcript Summary Tool by LunaNotes.
Related summaries
Docker for Beginners: A Comprehensive Guide to Containerization
Learn Docker with hands-on labs, concepts, and advanced orchestration tools like Kubernetes.
Docker Containers and Kubernetes Fundamentals Course Summary
This comprehensive course, led by expert Ghee Barrette, covers the fundamentals of Docker containers and Kubernetes, providing beginners with a solid foundation in containerization and orchestration. Participants will learn about setting up their environment, microservices architecture, cloud-native concepts, and how to manage applications using Kubernetes.
Docker Tutorial: Comprehensive Guide from Basics to Advanced Concepts
This video provides a complete overview of Docker, covering installation, predefined images, Docker Hub, container management, and advanced topics like volumes and networking. By the end, viewers will have a solid understanding of Docker's functionalities and how to effectively use it in their projects.
The Ultimate Docker Course: From Zero to Hero - Complete Guide & Tutorial
it works on my machine
Comprehensive System Design Series: From Monolith to Microservices and Beyond
This extensive video series covers crucial system design concepts essential for software engineers, students, and developers preparing for FAANG interviews or building scalable startup systems. Dive deep into foundational topics like monolithic vs microservice architectures, API gateways, load balancers, networking protocols, caching strategies, distributed systems, rate limiting, SSL certificates, database choices, avoiding single points of failure, messaging queues, consistent hashing, and more with real-world examples and hands-on coding projects.
Most viewed summaries
A Comprehensive Guide to Using Stable Diffusion Forge UI
Explore the Stable Diffusion Forge UI, customizable settings, models, and more to enhance your image generation experience.
Kolonyalismo at Imperyalismo: Ang Kasaysayan ng Pagsakop sa Pilipinas
Tuklasin ang kasaysayan ng kolonyalismo at imperyalismo sa Pilipinas sa pamamagitan ni Ferdinand Magellan.
Mastering Inpainting with Stable Diffusion: Fix Mistakes and Enhance Your Images
Learn to fix mistakes and enhance images with Stable Diffusion's inpainting features effectively.
Pamamaraan at Patakarang Kolonyal ng mga Espanyol sa Pilipinas
Tuklasin ang mga pamamaraan at patakaran ng mga Espanyol sa Pilipinas, at ang epekto nito sa mga Pilipino.
How to Install and Configure Forge: A New Stable Diffusion Web UI
Learn to install and configure the new Forge web UI for Stable Diffusion, with tips on models and settings.
Found this summary useful?
Take it with you. One click puts it in your own LunaNotes library.
Save to LunaNotes