 My name is Laura Frank. I'm a senior engineer at CenturyLink Labs. We're a pretty small R&D offshoot of CenturyLink proper, so we don't really deal with any telephone polls or cable subscriptions, but what we do is a lot of R&D around Docker and the technologies that are kind of tangentially related to Docker like Fleet, CoroS, Kubernetes, you may have heard some of these. We kind of look at how they're serving the development community, how we work with them, and then hopefully try to make them better. I'm on the internet, you can find me. I also share a name with a reporter from a Colorado newspaper. I'm not her. So like I said, I work with Docker a lot. Here I'm going to talk about Docker and Ruby together. Over the course of this talk, I'm going to try to answer three kind of large, ambitious questions for you. The first being, what are Docker containers? The second, how can you use Docker and Ruby together? And then finally, how might you architect an application using Docker and Ruby together and actually have it work? I assume that most of you have heard about Docker, but maybe know nothing about it, and that's just perfect. I'm going to go over a lot of the pretty basic introduction stuff to Docker. If you are a Docker user already, hopefully I'll shed some light on some of the technical underpinnings of Docker. I know last night there was a Docker boss. Did anyone attend that in the audience? A few? Okay. One guy? All right. I said like did not attend. I was stuffing my face full of fish because I live in the Midwest and it was pretty special. So what is Docker? We've probably heard about Docker because companies like Google, Spotify, Twitter, guilt group, countless others have been really talking a lot about how they're using Docker in production and all of the fun and complicated problems that it has solved for them. But the truth is that Docker is really not, or the idea of container-based virtualization is not really anything new. LXC or Linux containers has been around since 2008. But Docker kind of puts this nice, pretty front-end on it and it makes it a lot easier for everyone to use it. And also, as more people are using the Internet all the time, these production workloads can fail at a much more rapid pace than maybe they were before everyone was on their smartphones all the time. So Docker has this really big need that it can fill. So Docker at its core is a packaging tool. It sits on top of existing components of the Linux kernel and packages these containers and puts them into isolated execution environments. That's really all that it is. Docker has a very limited scope. It does not schedule jobs for you. There are lots of things that it does not do for you. What it does do for you is package your stuff and put it in a container. And a container really is nothing more than just a self-contained execution environment. I'm going to talk about how exactly this is set up in just a little bit, but think about it as something in a box that is isolated from everything else around it. It has the benefit of having the shared kernel of the host system that it's on with having isolation. So they're sharing and isolation, those things together give it a very fast boot time and low overhead and also makes things work very efficiently because you're getting rid of a lot of duplicitous work. And in fact, you will see a big performance benefit from a service that's running in a virtual machine or a service that's running in a container. And this is due to very, very big differences between how these services actually run on the host. So here's an example of a pretty typical virtual machine set up. Probably have something like this running right now. In this example, we have two different services. We have app one, which is one service, two instances of that service, and then app two. And they're in virtual machines. On each machine, there's the library. There's a guest OS. We have a hypervisor underneath all of those. Of course, sitting on a host OS and sitting on top of hardware. In the Docker world, though, we can get rid of the hypervisor. There's really no need for it. And we can get rid of the guest OS on every virtual machine. And instead, we're going to introduce something called the Docker engine. This is the actual packaging tool that's sitting on the host, putting things into containers and deploying them for you. You'll notice that the libraries now, instead of being in the container, are actually underneath the container. This lets each container that's identical reference back to the same library. There's no need to have duplicitous copies of everything. Maybe an easier way to understand it is at the San Diego Zoo. It was just there on Sunday. The koalas are all in their own little self-contained environments, but each koala does not need to have its own healthcare giver, and they have access to the same food supply. I mean, it would be really silly to have a separate veterinarian per koala. You kind of don't want to have a separate library for each of your container containers. Also, koalas are very cute. So, how does this work? Containers seem kind of very abstracted and maybe difficult to understand. And they are. I mean, we spent the last nine months working on a tool that sits on top of Docker, and we still kind of want to throw our laptops out the window most days. But there's really four big things that make Docker run the way that it does and give you all these great performance benefits. The first one is lib container. This is the container format. So, maybe several months ago, I don't know exactly remember when, but Docker launched lib container, which is its own equivalent to LXC. Previously, Docker was just building on top of LXC, which was why it could only run on Linux. But now, lib container is a different container format that's very, very similar to LXC, but actually has the capability of running anywhere. You can probably, if you pay attention to Docker blogs, you'll see that they're starting to think about running it on Windows. Maybe it can run on a Mac natively soon. It's all due to this lib container, container format. For our purposes, and the examples we'll look at today, we're just going to use lib container. We could use LXC. It's kind of a layer of complexity that we don't need to spend too much time on. The next thing, no, this is like the three really important things, namespaces. This is what makes the container isolated from everything else. So, this is process and not resource. So, this is networking. This is process ID or PID. All of these are happening in its little namespace so that it's not going to conflict with other containers that are on the same host. This is what puts it in its little partition. On the other side of that is a thing called C groups, which is a container group. This is what allows these containers to be good multi-tenant citizens and access shared resources when needed, like the library or the veterinarian if our containers are koalas. The last thing is the union file system. This is particularly what makes Docker very fast. The layered file system is the copy-on-write. Everything that happens when you're building the image to run your container from happens sequentially and everything is stacked and stacked and stacked. Think of it as your Git log. If you want to go and redo, maybe just rebuild something on your image, you can point it back at maybe three layers before and rebuild from there. You don't have to go all the way back and get rid of your Git directory and clone it again. You can just reference back and then build on top of it. Those are the technical things that make Docker this lightweight runtime and packaging tool using components of the Linux kernel. The other great thing about Docker, though, it's more than that. It's a development workflow and an ecosystem, which is why Docker is so attractive to developers working in small teams and developers working for companies that have production loads like Google. This is sort of what Docker looks like. On one hand, we have the engine, which is what I just talked about. That's all the technical stuff, the code execution, et cetera. On this other side, we have a hub. This is where people go to collaborate and share things with one another related to Docker. The cornerstone of this hub part of Docker is called the registry. You can find it at registry.hub.docker.com. It looks a little bit like this. This is what it looked like when we took the screenshot. This is where images go to live. If you work on a Docker image and you would like to share it with people, you can push it to this public registry. You can use a private registry if you have some proprietary images. It's a whole nother talk, though. You'll notice about a third of the way down, there are these things called official repositories. These are companies or communities that have been using Docker and say, it would be really great if we would just put an official image up here that people can use so they don't have to do the same work over and over again. For example, if you wanted to use MongoDB in your project, you don't have to build that image. Just use the one that MongoDB has put up there for you to use. Almost all of these, actually, all of them do have great documentation about bootstrapping your own project to use these official images. We have to install Docker before we can use it. This is one of my favorite tweets because Docker does get a lot of flak that it is very abstract things a lot, maybe too much for some people's comfort. There's many layers usually between the hands that are typing the code and the code that's actually running in a container, whatever that is, somewhere on a host. I'm not going to lie. It can be a hurdle to get over when you start using Docker. The good news is there's a lot of tools to help you kind of bridge that huge gap that it can feel like you have between you and your code. If you're on Linux, like I said before, Docker was built at first off of Linux containers, you can just install Docker with official packages and you can be ready to go. But if you're on anything else, you will have to run a VM. I realize that sounds maybe in vast contrast to what I just told you about how VMs are evil and bloated, but I guarantee that running one VM where you can run thousands of containers or hundreds of containers, depending on how big that VM is, is not really going to impact your performance very much. In fact, Docker has this great tool called Boot to Docker, and you can find that on Docker.com when you look in the installation instructions. It's basically a very lightweight tool that will spin up a VM for you in the background, get Docker running on it, make everything work for you, and it will sync all of your folders so that you can interact with Docker on your Mac or Windows machine, just like you're just on your normal terminal. I'm actually going to use this in my presentation. It will look like I am just typing and interacting with Docker directly. In fact, I'm using Boot to Docker and there is a layer of abstraction. Everything I'll do in this presentation is running on a Boot to Docker VM. When you do have Docker installed, maybe with Boot to Docker, maybe your own vagrant file, certainly whatever you prefer will work, you're going to interact with Docker, probably via the CLI. There's also a REST API for interacting with Docker, and both of these have extensive, wonderful, beautifully written prose documentation at docs.docker.com. All right, on to the good part. Using Ruby with Docker. So specifically, how might you use the API and the CLI and an image to make a container that's running Ruby and that can run your Ruby application? Before we really get into spinning containers up, we have to take a step back and really acquaint ourselves with what an image is. Think of a Docker image as the class and a container is an instance of that class. You have to have an image in order to start a container. You cannot start a container without an image. The container will have nothing in it. Each image is controlled by a Docker file. This is basically a list of instructions. We're going to look at a Docker file in just a few seconds and we're actually going to build an image together. It will be really fun. Images are built by saying Docker build dash t is for tag. And then you give it a name. Generally, a convention that's used is like your GitHub name slash name of your image. You can also just call it whatever you want. And then a dot to signify where the Docker file resides. Most often it will be in the directory that you're in. Going back to that Docker registry that I mentioned before, so whether or not it's a public registry or a private registry that you are running yourself, you can Docker pull an image down, pull it down just by using the name of the image, which again is usually the person's GitHub name and then the name of the image. Optionally, there is a tag for a version, which is a colon tag. We'll see this when we look at the Ruby image, which is right here. This is a Docker file. This is a very, very, very simple application. You can see that it's based off of 2.1.2. It does some stuff, and then eventually down here we have a command that we're going to start the container with. So why don't we build this? Maybe, hopefully it works. Okay. You guys can all see that? So I'm in this, sorry. In my directory I have kind of all the junk I need to run this application. Most notably, I have a Docker file, which is the same Docker file that we just looked at. So I am going to Docker build dash t. I'm just going to call this hello world and then a dot, because it's where I am now. And we can see this layered file system already in use. We can see step four, and it's giving us kind of the place in memory where all this stuff is stored. So this might take a little while. So while we do that, hopefully that works. Live coding or live demo is always a little nerve wracking for everyone involved, including the audience. So thank you, guys. Great. Again, this Docker file is super simple. The from command is a requirement. You have to have a base image that your image inherits from. So similar to that class object analogy that I used earlier, inheritance also works. So anything that the CenturyLink base image has will also be in my image because I am referencing it and pulling in all of its stuff. Notice the colon 2.1.2 because this is Ruby 2.1.2. You can use any version CenturyLink. We have spent a lot of time optimizing Ruby images. If you're interested in using them, you can find them on the hub at the CenturyLink repository. The next thing is maintainer. So that's me. And if it breaks, then you can blame me and know where to find me. The next thing is an expose command. This just kind of advertises that your service is available on a port. It will advertise that to other containers that are running on the same host. Next, we have a set of run and add. A run command will run the command that is passed to it. So in this case, we're going to make a directory, an app directory, and then add is basically like a copy command. So we're going to copy everything from the current directory into that new app directory that we just made. And then next, we will set that app directory as our working directory. From this point forward, everything that's passed to the Docker file via run happens in context of this work. So when we run bundle install, it's going to run specifically only the gem file that is in that directory that we just made. And then last but not least, this is the thing that actually makes the container do something. This is command or cmd. So for this, we're just going to enter with like very basic ruby hello world.rb. And this is one more that's kind of important. You'll probably see it in a lot of the other Docker files that are available on Docker registry. This is an environment variable. It takes the syntax of key in upcase and then value in downcase. And any environment variable that you give it in the Docker file will be understood and known and used by any subsequent run command. And then the next one is volume. You can either say give it a path or you can give it an array of paths. This just creates a mount point. This is how you would get code into a container by creating a mounted volume of that code. And before we check on our image that's hopefully downloaded, this from thing is actually pretty powerful and really important and can take your Docker workflow from being like okay to being really super great. Basically in a perfect world, we'd be using the same version of Ruby in all of our services, hopefully. But really only one base image. You can reference that base image from other Docker files for other services that you're running. And then also, this has Ruby baked into it. You don't need to have a gem set manager or a version manager because there's just one thing. There's just one set of gems. There's just one version. It's baked into your base image. If you do want to update Ruby then, you can just update the base image and then that, again, it will be shared across everything. Okay, cool. Successfully built. That's excellent news. I'm just going to clear this for you in the back. That bottom part might be hard to see. All right, so Docker images is the way you look at all the images that are on your host. I think I had a partial failed build which is why I have this weird none none. But you can see that here. Century link Ruby base is actually treated as a whole separate image. Remember that's my base image. And then this hello world is, you know, everything from the base image forward is in that hello world. And in fact, if we delete this hello world, I'm not going to do it right now. Actually, I think that would be a bad idea. And then rebuilt it, we would see that the work that Docker had to do to get that Ruby base image doesn't happen anymore. We don't need to do it. We've already stored it. We're just going to delete everything from the base image. And since the base image would be shared again because I'm running the same thing, it would just do the work whatever those seven instructions were after building the base image in order to kind of rebuild the same thing that I did before. So again, take translate that into maybe having multiple services that share the same base image. It's a lot of work that you can save by just putting all of your kind of inherited dependencies in that base image. So why don't we run this thing? That would be a great idea. The way to look for processes that are running on your host is Docker PSA that will give you all of the processes that are happening. And you can see we have nothing happening right now. So let's Docker run. I'm going to pass a port binding because I want you all to see this in a browser. So that's dash P. And I'm just going to bind 4567 kind of to itself. And then the last argument I'll pass is the name of the image. And that was it. And now we have a containerized application running before your very eyes. So it really was... Thank you. So think about how long it took to run that. It was like literally milliseconds. Think about how long it might have taken a virtual machine to spin up and run that same application. Probably many, many milliseconds. Think about how many times you have to restart a container or restart a service over the course of your daily work when you're developing stuff. If you can shave that down from minutes down to milliseconds, you have a lot more time to look at cat pictures or whatever you want to do. So cool. We have this here. It's running. You know, I can see it. And then I'll just shut it down and we can see that container. Oops. Container is running. It's exited. We get exit codes for Docker containers. And if I wanted to start it again, I can either reference its identifier or its fun name, which in this case is High Shockly. It's always like some combination of adjective and like scientist or something. Usually this looks a little nicer. I just have the font blown up pretty big on here. All right. So that's it. That was running a Ruby application. Cool. So in this case, I used our own CenturyLink virtual image or base image. Ruby does have a lot of official images and they are up on the Docker hub. Pretty much anything 1.9.3 and newer you can grab. And again, just a Docker pull and really great instructions for bootstrapping. And like I said before, the base Ruby images, the official ones are a little big. Specifically, my team has spent a lot of time trying to make them very small. And eventually, I think we would love to have our images be the official Ruby images, but we're just not quite there yet. So you're welcome to use either, depending on what you want to do. And lastly, you might need some gems. Swipely has done a lot of work with Docker and they have a few really good gems. The one that we use for the projects that we work on is the Docker API gem. So this allows you to interact with the Docker API from inside of your application. If you go to Ruby gems, there's like 30, probably 35 other ones. Anything from building images to, you know, running Docker PSA and having a nice output of all the processes that are running on your host. So just poke around and some of them are obviously more developed than others and some of them have more users than others. But you could probably find anything that you need there. And also debugging in a container is kind of a pain. Sorry. Like I said before, Docker has a lot of kind of a lot of flak because it has so much abstraction between your code that's running like somewhere out in the ether on a container and you actually doing the work. What we found on our team is that we just, we use pry if we need to. We try to run things kind of locally before we put them in a container but that's not always really that possible. So I hope you're all using pry already and if you're not, require the thing and then call the thing and you'll be dropped into a nice debugging session and apparently you can time travel now with pry so I'm going to try that as soon as I get back to Chicago. The one piece of advice I have for all of you, if you're using pry, you may think, oh, it's in a container, it's away from me, I have to use a remote debugging session. You really don't need to in most cases. In some cases you might have a container running in the background and then you might have to set up a remote session. By and large, generally, if you're just kind of tinkering, you don't need a remote session. Alternatively, you can pass in the pry command directly to that Docker run string and be dropped into a debugging session with the Docker run name of the image and then after the image name, you can say pry, that's the equivalent to the command that we saw in the Docker file and you can just be dropped right in. So now that we know how to fix stuff that breaks, we could probably start putting some stuff together. So how do I build a thing with Docker? So let's talk about architecture, which is everyone's favorite topic usually. Docker architecture is service-oriented architecture, plain and simple. Think about how fast it was milliseconds to get that service running. That really is beneficial if you have just one service per container. You may look at a container and say, wow, this is really fast. It's taking up a lot less space. I can just dump everything into one container and then it'll be really fast and great, which is kind of true. But if you have each service in a different container, if any of those fail, you can spin it up very, very quickly. Also for scaling, it's much easier to scale individual containers than like giant containers that are monolithic black boxes. So here we have a nice illustration of one service in one container. This is a very simple application. It has a web framework and then a database behind it. So we might look at this and say, okay, but a container is isolated. So how does container one know that container two exists? And furthermore, how can I even get them to exchange information with one another? And the answer to that is there's a few of them. You can link them together. I'm using something called the Docker link. But more commonly, you'll probably use some kind of combination of port mapping and then environment variables, depending on particularly what requirements your services have. All of this is just configuration. I mean, it's really nothing more than just configuration. And this configuration happens in two places. We've seen already that the Docker file can handle a lot of these options. That's baking them into actually the base image, like that class that then becomes the object, which is the container. But then also, you saw me just before make a port mapping rule in the Docker run string. So there's a lot of different flags that you can use in the Docker run string. And most of them kind of have shared work. You can either do it in the Docker file or you can do it at runtime. So let's take a look again at this dummy app and look at the Docker file. I'm going to try to help you understand maybe some best practices of what should go where in terms of making a choice between where you want your configuration options to live. So we have this Docker file. More or less, this is just fine. I don't have anything too specific or too particular. So for example, I might be using this for one reason, but if I put it on the Docker hub and have someone else use it, I don't know what they're going to be using it for. So I don't really want to hard code anything, like maybe a port binding rule. I don't want to put that in there. And I really, really don't want to put environment variables in the Docker file. It sounds funny to think about, but if you don't know maybe what you're doing, you might accidentally put something in a Docker file and commit it to GitHub or to the registry and then everyone has your password. So that is particularly one thing that you want to do just at runtime. Don't put that in the Docker file. So let's look at the kind of equivalent of that Docker file, but with a Docker run string. So again, Docker run. We have dash P here for the port binding rule or port mapping rule, whatever you like to call it. And then here is where your environment variables belong. This is dash E. You can put quotes around it or you cannot, depending on how you're feeling that day, I guess, always again, password and upcase equals and then unless your value is case sensitive, it should be in downcase and there should not be a space in between the equal sign. I think keynote just got a little ambitious there. And then finally, that linking thing that I talked about earlier. Docker has a capability for linking. You can say that one thing, in this case the web container that I'm running with a Docker run string depends on the database. And you give it a name and an alias. So DB, DB in this case. And then again, the last thing that you pass, if you're not passing a command, is the name of the image that you're actually running. And again, a word of caution about using a Docker link. When you're using Docker directly on your own host, that's great, you can use a Docker link. But if you're doing any kind of scaling, you might be using Kubernetes, you might be using Marathon and Mezos or Fleet with various service discovery components. That's not Docker. And it doesn't know what a Docker link is. So if you're going to have some kind of schedule, scheduler orchestrating your Docker containers, don't use a link. Use the environment variable and it's kind of like port mapping way instead. It's kind of advanced stuff. If you have any questions, of course you can talk about it later. But I won't take up any more time talking about it right now. So a little bit more about the Docker run string is that each container has its own. You can only run one container with one command. Multiple containers can use the same image to run from. It's not like you have to have a different image for each container. You can use the same image to run multiple containers. And again, you want to find the balance between the Docker run string and then the Docker file for configuration so that you're not exposing anything that shouldn't be exposed. But again, you have to do this whole configuration business every time you run a container. And I'm really lazy. That's why I became an engineer. And I would rather make a computer do the work for me than do it myself. And the good news is that you can do that with Docker. There's this idea called application templating and that's what my team and I spend our time working on is kind of the equivalent of forming up but for Docker containers. So if you understand and configure your container or your application once, you can put it into an application template and instead of running each container independently, you run the template that will take care of running all of the containers with the various configuration options that you've already specified. You can use your own images. You can use images from the Docker registry, specify your options beforehand so you don't need to, like, do them on the fly. And then, again, you can just run the application as a whole instead of running each container independently. So a popular one is FIG. This is actually a part of Docker proper now. It used to be its own thing and then Docker acquired them. And that's a great thing because that means that FIG standards are very tightly coupled with Docker standards. How it works is you dump your application requirements into a YAML file called fig.yaml and then run fig up and then your application is running for you. It's at fig.sh. And again, this is a proper Docker project and maintained by the Docker community. It is command line only, though. So if you're looking for something that is a little bit more visual as you first get started or maybe you prefer to work that way, the software that I work on with my team is called Panamax. And we have t-shirts, if you'd like one. It's a Docker workflow tool. It itself is a containerized application, depending on how you're using it. When you run Panamax, you'll actually be running anywhere from three to five at Ruby services in containers. Panamax does use a lot of these Ruby or Docker adjacent technologies like CoreOS, Fleet at CD for things like service orchestration and service discovery. And you can find us and all that good stuff at panamax.io. So Panamax on purpose, we made this very similar to FIG. If you start with FIG and you decide you want to try Panamax or the other way around, it is really easy to go back and forth between the two tools. In addition to just having a UI, the other thing that Panamax has is support for remote deployment. So if you do get to a point where you're deploying this to a cluster running on Google Compute Engine, you can do that with the click of a button from Panamax. And if you want to download it, you can go to panamax.io, get Panamax. It is an open source project. It is all in Ruby. We are all Rubyists. So if you want to contribute, that would be super great. This is what an application template looks like with Panamax. So this is kind of an abbreviated version because there's a lot of other stuff that's maybe not so pertinent to this example. But we give it a name and a description. There's also support for things like documentation in there. And then you just start listing your images. So in this case, we have our Rails application with Postgres. And the first thing that we have is our Web Tier category, which is of course Rails. We have a source image. And then description of it, kind of type. The good stuff is down here in ports. All this configuration you just do once. And it stays with this template. You can, of course, change the template if you'd like to. But every time you run this template, the same exact things are going to happen over and over again. So why don't we look at Panamax. This is the homepage of Panamax. This is hooked directly to the Docker registry. We also have these fancy little buttons. So if you're interested in maybe running a Rails application, you can click on Rails in all of the pre-existing templates, which there are quite a few. There's probably 30 to 35 public templates that have been vetted by this lovely team of engineers that's sitting in the front row and myself to make sure that they do work. And from here you can just press run template. So you don't have to type anything. You can run locally. Or you can deploy it to your remote target, for example, like your Google Compute cluster or whatever kind of cluster you want to run. I'm just going to run it locally. And we'll see Panamax starting the application. And this may take a little bit and has to download the images. So everything that started with the Docker run string, it's just going on kind of in the background here. But great news, it was successfully created. And then I can see both of the services that I have here as well as my journal. So let's go in to download the image. And our familiar little friend, the Docker run string is over here. So what we really try to do with Panamax is make Docker easy, but not secret to use. And to that end, we do keep all of the artifacts around. We do show you the Docker run string. For example, if I wanted to add an environment variable in here, we can see the Docker run string auto update itself, just as I typed that. So when you use something with Panamax, you'll know exactly the equivalent of what you would be doing in real Docker on the CLI, except that you don't have to do it. You can just save it, make it a template, and then run it with a click of a button. I love application templating. It's kind of the game changer when you go from just tinkering with Docker to actually using it for maybe bootstrapping your development environment or managing all of your dependencies. If you have Postgres and Elasticsearch for every service or every application that you work on, just make a template with them and press like run. And then you have them, you don't have to kind of go through and set up all of your VMs or containers. And again, it's a Ruby open source Ruby project. You can find it on GitHub at CenturyLink Labs. And there's a few issues in the backlog if you want to work on it. Just saying. Okay. So that's it. We talked about what Docker is, what containers are, how to run them, how to run them with Ruby, how we might architect an application, and then finally how to template those applications to save yourself a lot of time. A few parting thoughts as you start on your containerized journey. Use Boot to Docker. It's really great. As you saw when I was packing away at Docker here, that was actually using Boot to Docker. I wasn't on my own machine. That was on a VM, but it sure didn't feel like it. Extract commonalities and put them in a base image. Anywhere you can use inheritance, please do it because it will save you so much time and resources because of that layer. Again, please don't put any passwords in your Docker file. And then use templating. It's a really fast way to get started. If you're looking for other resources or maybe you want to get started, some pretty good places to start, you want to look at the Docker club, see what's already out there and already exists. There's no use in reinventing the wheel. So you can use one of those official images, go for it. Docker has great documentation. Again, Boot to Docker. And then if you are interested in templating, you can look at Panamax or FIG at fig.sh. And then finally, the team of engineers I work with, we all write like pretty sometimes long form blog posts on different Docker images and sometimes do tutorials and you can find that at centurylinklabs.com. So thank you so much for being a great audience. And I hope you guys learned something about Docker. If anyone has any questions, I'll be happy to answer them or pretend like I know the answer.