Skip to content

Commit

Permalink
dev containers
Browse files Browse the repository at this point in the history
  • Loading branch information
btholt committed Apr 21, 2024
1 parent 3b46c5a commit 8987441
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 1 deletion.
File renamed without changes.
3 changes: 2 additions & 1 deletion lessons/06-docker-features/A-bind-mounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ This is how you do bind mounts. It's a bit verbose but necessary. Let's dissect
- The target is where we want those files to be mounted in the container. Here we're putting it in the spot that NGINX is expecting.
- As a side note, you can mount as many mounts as you care to, and you mix bind and volume mounts. NGINX has a default config that we're using but if we used another bind mount to mount an NGINX config to `/etc/nginx/nginx.conf` it would use that instead.

Again, it's preferable to bake your own container so you don't have to ship the container and the code separately; you'd rather just ship one thing that you can run without much ritual nor ceremony. But this is a useful trick to have in your pocket.
Again, it's preferable to bake your own container so you don't have to ship the container and the code separately; you'd rather just ship one thing that you can run without much ritual nor ceremony. But this is a useful trick to have in your pocket. It's kind of like [serve][serve] but with real NGINX.

[storage]: https://docs.docker.com/storage/
[project]: https://github.com/btholt/project-files-for-complete-intro-to-containers-v2/blob/main/static-asset-project
[serve]: https://github.com/vercel/serve
70 changes: 70 additions & 0 deletions lessons/06-docker-features/B-volumes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
---

Bind mounts are great for when you need to share data between your host and your container as we just learned. Volumes, on the other hand, are so that your containers can maintain state between runs. So if you have a container that runs and the next time it runs it needs the results from the previous time it ran, volumes are going to be helpful. Volumes can not only be shared by the same container-type between runs but also between different containers. Maybe if you have two containers and you want to log to consolidate your logs to one place, volumes could help with that.

They key here is this: bind mounts are file systems managed the host. They're just normal files in your host being mounted into a container. Volumes are different because they're a new file system that Docker manages that are mounted into your container. These Docker-managed file systems are not visible to the host system (they can be found but it's designed not to be.)

Let's make a quick Node.js app that reads from a file that a number in it, prints it, writes it to a volume, and finishes. Create a new Node.js project.

```bash
mkdir docker-volume
cd docker-volume
touch index.js Dockerfile
```

Inside that Node.js file, put this:

```javascript
const fs = require("fs").promises;
const path = require("path");

const dataPath = path.join(process.env.DATA_PATH || "./data.txt");

fs.readFile(dataPath)
.then((buffer) => {
const data = buffer.toString();
console.log(data);
writeTo(+data + 1);
})
.catch((e) => {
console.log("file not found, writing '0' to a new file");
writeTo(0);
});

const writeTo = (data) => {
fs.writeFile(dataPath, data.toString()).catch(console.error);
};
```

Don't worry too much about the Node.js. It looks for a file `$DATA_PATH` if it exists or `./data.txt` if it doesn't and if it exists, it reads it, logs it, and writes back to the data file after incrementing the number. If it just run it right now, it'll create a `data.txt` file with 0 in it. If you run it again, it'll have `1` in there and so on. So let's make this work with volumes.

```dockerfile
FROM node:20-alpine
COPY --chown=node:node . /src
WORKDIR /src
CMD ["node", "index.js"]
```

Now run

```bash
docker build -t incrementor .
docker run --rm incrementor
```

Every time you run this it'll be the same thing. This is nothing is persisted once the container finishes. We need something that can live between runs. We could use bind mounts and it would work but this data is only designed to be used and written to within Docker which makes volumes preferable and recommended by Docker. If you use volumes, Docker can handle back ups, clean ups, and more security for you. If you use bind mounts, you're on your own.

So, without having to rebuild your container, try this

```bash
docker run --rm --env DATA_PATH=/data/num.txt --mount type=volume,src=incrementor-data,target=/data incrementor
```

Now you should be to run it multiple times and everything should work! We use the `--env` flag to set the DATA_PATH to be where we want Node.js to write the file and we use `--mount` to mount a named volume called `incrementor-data`. You can leave this out and it'll be an anonymous volume that will persist beyond the container but it won't automatically choose the right one on future runs. Awesome!

## named pipes, tmpfs, and wrap up

Prefer to use volumes when you can, use bind mounts where it makes sense. If you're still unclear, the [official Docker][storage] docs are pretty good on the subject.

There are two more that we didn't talk about, `tmpfs` and `npipe`. The former is Linux only and the latter is Windows only (we're not going over Windows containers at all in this workshop.) `tmpfs` imitates a file system but actually keeps everything in memory. This is useful for mounting in secrets like database keys or anything that wouldn't be persisted between container launches but you don't want to add to the Dockerfile. The latter is useful for mounting third party tools for Windows containers. If you need more info than that, refer to the docs. I've never directly used either.
46 changes: 46 additions & 0 deletions lessons/06-docker-features/C-dev-containers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
---

So far we've talking about taking an app and using containers to prepare the apps to run. This is an obvious use case for them and one you're going to use a lot. But let's talk about a different use case for them: building development environments for your apps.

Let's paint a picture. Let's say you got a new job with a company and they're a Ruby shop (if you know Ruby, pretend you don't for a sec.) When you arrive, you're going to be given a very long, likely-out-of-date, complicated README that you're going to have to go look for and struggle to set up the proper version of Ruby, the correct dependencies installed, and that Mercury is in retrograde (just kidding.) Suffice to say, it's a not-fun struggle to get new apps working locally, particularly if it's in a stack that you're not familiar with. Shouldn't there be a better way? There is! (I feel like I'm selling knives on an informercial.)

Containers! What we can do is define a Dockerfile that sets up all our dependencies so that it's 100% re-createable with zero knowledge of how it works to everyone that approaches it. With bind mounts, we can mount our local code into the container so that we can edit locally and have it propagate into the development container. Let's give it a shot!

## Ruby on Rails

I am not a Rails developer but I will confess I have always had an admiration for talented Rails developers. On one hand, I really don't like all the black magic that Rails entails. I feel like you whisper an arcane incantation into the CLI and on the other side a new website manifests itself from the ether. On the other hand, a really good Rails dev can make stuff so much faster than me because they can wield that sorcery so well.

So let's say we got added to a new Rails project and had to go set it up. Open this project in VS Code.

[⛓️ Link to the project][project]

If you do this in VS Code, it should show you a prompt in the bottom to reopen in a dev container. Say yes.

![VS Code UI showing a prompt to reopen in dev container](/images/dev-containers.jpg)

If you miss the notification or want to do it later, you can either do in the [Command Palette][command] with the command "Dev Containers: Open Workspace in Container" or with the `><` UI element in the bottom left of VS Code and clicking "Reopen in Container".

![VS Code UI showing a prompt to reopen in dev container](/images/vscode-ui.png)

This should build the container, setup all the Ruby dependencies and put you in a container. From here, you can open the terminal and see that you're now inside a Linux container. Run `rails server` and it will open the container and automatically forward the port for you to open `localhost:3000` in your own browser. There you go! Rails running without very little thought about it on our part. This is even running SQLite for us. You can make pretty complicated dev environments (using Docker Compose, we'll talk about that later), this was just a simple example.

Personally, this took a good 30 mins of messing around just to get set up, but with a dev container it was just instant, and that's kind of the magic: it's a ready-made dev environment to go.

> Just to be super clear, you dev containers and production containers will be different. You wouldn't want to ship your dev environment to production. So in these cases your project may have multiple Dockerfiles doing different things.
## Dev Containers Outside of VS Code

While dev containers is a decidedly Microsoft / GitHub initative to start up, they have opened it into an open standard and other companies can now use dev containers. Here's a few other tools that work with dev containers.

- [DevContainer CLI][cli] – Run dev containers from the CLI directly and then you can just use them without any IDE needed to manage it. Maintained by Microsoft and GitHub
- [Visual Studio][vs]
- [JetBrain IntelliJ][jetbrains]
- [GitHub Codespaces][gh] – Any time you open a project with a dev container in it in Codespaces, Codespaces will automatically use that dev container for you.

[project]: https://github.com/btholt/project-files-for-complete-intro-to-containers-v2/blob/main/dev-containers
[command]: https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette
[cli]: https://github.com/devcontainers/cli
[vs]: https://devblogs.microsoft.com/cppblog/dev-containers-for-c-in-visual-studio/
[jetbrains]: https://blog.jetbrains.com/idea/2023/06/intellij-idea-2023-2-eap-6/#SupportforDevContainers
[gh]: https://docs.github.com/en/codespaces/overview
Binary file added public/images/dev-containers.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/vscode-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 8987441

Please sign in to comment.