copy-edit viget posts

This commit is contained in:
David Eisinger
2023-10-24 20:48:09 -04:00
parent 0438a6d828
commit f86f391e82
77 changed files with 1663 additions and 1380 deletions

View File

@@ -2,8 +2,8 @@
title: "Local Docker Best Practices"
date: 2022-05-05T00:00:00+00:00
draft: false
needs_review: true
canonical_url: https://www.viget.com/articles/local-docker-best-practices/
featured: true
---
Here at Viget, Docker has become an indispensable tool for local
@@ -12,7 +12,7 @@ running different stacks and versions, and being able to package up a
working dev environment makes it much, much easier to switch between
apps and ramp up new devs onto projects. That's not to say that
developing with Docker locally isn't without its
drawbacks[^1^](#fn1){#fnref1 .footnote-ref role="doc-noteref"}, but
drawbacks[^1], but
they're massively outweighed by the ease and convenience it unlocks.
Over time, we've developed our own set of best practices for effectively
@@ -32,12 +32,12 @@ involves the following containers, orchestrated with Docker Compose:
So with that architecture in mind, here are the best practices we've
tried to standardize on:
1. [Don\'t put code or app-level dependencies into the
1. [Don't put code or app-level dependencies into the
image](#1-dont-put-code-or-app-level-dependencies-into-the-image)
2. [Don\'t use a Dockerfile if you don\'t have
2. [Don't use a Dockerfile if you don't have
to](#2-dont-use-a-dockerfile-if-you-dont-have-to)
3. [Only reference a Dockerfile once in
`docker-compose.yml`](#3-only-reference-a-dockerfile-once-in-docker-compose-yml)
`docker-compose.yml`](#3-only-reference-a-dockerfile-once-in-docker-composeyml)
4. [Cache dependencies in named
volumes](#4-cache-dependencies-in-named-volumes)
5. [Put ephemeral stuff in named
@@ -55,7 +55,7 @@ tried to standardize on:
------------------------------------------------------------------------
### 1. Don't put code or app-level dependencies into the image [\#](#1-dont-put-code-or-app-level-dependencies-into-the-image "Direct link to 1. Don't put code or app-level dependencies into the image"){.anchor} {#1-dont-put-code-or-app-level-dependencies-into-the-image}
### 1. Don't put code or app-level dependencies into the image
Your primary Dockerfile, the one the application runs in, should include
all the necessary software to run the app, but shouldn't include the
@@ -71,7 +71,7 @@ into the image means that it'll have to be rebuilt every time someone
adds a new one, which is both time-consuming and error-prone. Instead,
we install those dependencies as part of a startup script.
### 2. Don't use a Dockerfile if you don't have to [\#](#2-dont-use-a-dockerfile-if-you-dont-have-to "Direct link to 2. Don't use a Dockerfile if you don't have to"){.anchor} {#2-dont-use-a-dockerfile-if-you-dont-have-to}
### 2. Don't use a Dockerfile if you don't have to
With point #1 in mind, you might find you don't need to write a
Dockerfile at all. If your app doesn't have any special dependencies,
@@ -82,7 +82,7 @@ infrastructure (e.g. Rails needs a working version of Node), but if you
find yourself with a Dockerfile that contains just a single `FROM` line,
you can just cut it.
### 3. Only reference a Dockerfile once in `docker-compose.yml` [\#](#3-only-reference-a-dockerfile-once-in-docker-compose-yml "Direct link to 3. Only reference a Dockerfile once in docker-compose.yml"){.anchor} {#3-only-reference-a-dockerfile-once-in-docker-compose-yml}
### 3. Only reference a Dockerfile once in `docker-compose.yml`
If you're using the same image for multiple services (which you
should!), only provide the build instructions in the definition of a
@@ -91,17 +91,19 @@ the additional services. So as an example, imagine a Rails app that uses
a shared image for running the development server and
`webpack-dev-server`. An example configuration might look like this:
services:
rails:
image: appname_rails
build:
context: .
dockerfile: ./.docker-config/rails/Dockerfile
command: ./bin/rails server -p 3000 -b '0.0.0.0'
```yaml
services:
rails:
image: appname_rails
build:
context: .
dockerfile: ./.docker-config/rails/Dockerfile
command: ./bin/rails server -p 3000 -b '0.0.0.0'
node:
image: appname_rails
command: ./bin/webpack-dev-server
node:
image: appname_rails
command: ./bin/webpack-dev-server
```
This way, when we build the services (with `docker-compose build`), our
image only gets built once. If instead we'd omitted the `image:`
@@ -109,7 +111,7 @@ directives and duplicated the `build:` one, we'd be rebuilding the exact
same image twice, wasting your disk space and limited time on this
earth.
### 4. Cache dependencies in named volumes [\#](#4-cache-dependencies-in-named-volumes "Direct link to 4. Cache dependencies in named volumes"){.anchor} {#4-cache-dependencies-in-named-volumes}
### 4. Cache dependencies in named volumes
As mentioned in point #1, we don't bake code dependencies into the image
and instead install them on startup. As you can imagine, this would be
@@ -118,34 +120,36 @@ time we restarted the services (hello NOKOGIRI), so we use Docker's
named volumes to keep a cache. The config above might become something
like:
volumes:
gems:
yarn:
services:
rails:
image: appname_rails
build:
context: .
dockerfile: ./.docker-config/rails/Dockerfile
command: ./bin/rails server -p 3000 -b '0.0.0.0'
volumes:
- .:/app
- gems:/usr/local/bundle
- yarn:/app/node_modules
```yaml
volumes:
gems:
yarn:
node:
image: appname_rails
command: ./bin/webpack-dev-server
volumes:
- .:/app
- yarn:/app/node_modules
services:
rails:
image: appname_rails
build:
context: .
dockerfile: ./.docker-config/rails/Dockerfile
command: ./bin/rails server -p 3000 -b '0.0.0.0'
volumes:
- .:/app
- gems:/usr/local/bundle
- yarn:/app/node_modules
node:
image: appname_rails
command: ./bin/webpack-dev-server
volumes:
- .:/app
- yarn:/app/node_modules
```
Where specifically you should mount the volumes to will vary by stack,
but the same principle applies: keep the compiled dependencies in named
volumes to massively decrease startup time.
### 5. Put ephemeral stuff in named volumes [\#](#5-put-ephemeral-stuff-in-named-volumes "Direct link to 5. Put ephemeral stuff in named volumes"){.anchor} {#5-put-ephemeral-stuff-in-named-volumes}
### 5. Put ephemeral stuff in named volumes
While we're on the subject of using named volumes to increase
performance, here's another hot tip: put directories that hold files you
@@ -155,7 +159,7 @@ thinking specifically of `log` and `tmp` directories, in addition to
wherever your app stores uploaded files. A good rule of thumb is, if
it's `.gitignore`'d, it's a good candidate for a volume.
### 6. Clean up after `apt-get update` [\#](#6-clean-up-after-apt-get-update "Direct link to 6. Clean up after apt-get update"){.anchor} {#6-clean-up-after-apt-get-update}
### 6. Clean up after `apt-get update`
If you use Debian-based images as the starting point for your
Dockerfiles, you've noticed that you have to run `apt-get update` before
@@ -164,11 +168,13 @@ precautions, this is going to cause a bunch of additional data to get
baked into your image, drastically increasing its size. Best practice is
to do the update, install, and cleanup in a single `RUN` command:
RUN apt-get update &&
apt-get install -y libgirepository1.0-dev libpoppler-glib-dev &&
rm -rf /var/lib/apt/lists/*
```dockerfile
RUN apt-get update &&
apt-get install -y libgirepository1.0-dev libpoppler-glib-dev &&
rm -rf /var/lib/apt/lists/*
```
### 7. Prefer `exec` to `run` [\#](#7-prefer-exec-to-run "Direct link to 7. Prefer exec to run"){.anchor} {#7-prefer-exec-to-run}
### 7. Prefer `exec` to `run`
If you need to run a command inside a container, you have two options:
`run` and `exec`. The former is going to spin up a new container to run
@@ -181,7 +187,7 @@ spin up and doesn't carry any chance of leaving weird artifacts around
(which will happen if you're not careful about including the `--rm` flag
with `run`).
### 8. Coordinate services with `wait-for-it` [\#](#8-coordinate-services-with-wait-for-it "Direct link to 8. Coordinate services with wait-for-it"){.anchor} {#8-coordinate-services-with-wait-for-it}
### 8. Coordinate services with `wait-for-it`
Given our dependence on shared images and volumes, you may encounter
issues where one of your services starts before another service's
@@ -191,43 +197,43 @@ script](https://github.com/vishnubob/wait-for-it), which takes a web
location to check against and a command to run once that location sends
back a response. Then we update our `docker-compose.yml` to use it:
volumes:
gems:
yarn:
services:
rails:
image: appname_rails
build:
context: .
dockerfile: ./.docker-config/rails/Dockerfile
command: ./bin/rails server -p 3000 -b '0.0.0.0'
volumes:
- .:/app
- gems:/usr/local/bundle
- yarn:/app/node_modules
```yaml
volumes:
gems:
yarn:
node:
image: appname_rails
command: [
"./.docker-config/wait-for-it.sh",
"rails:3000",
"--timeout=0",
"--",
"./bin/webpack-dev-server"
]
volumes:
- .:/app
- yarn:/app/node_modules
services:
rails:
image: appname_rails
build:
context: .
dockerfile: ./.docker-config/rails/Dockerfile
command: ./bin/rails server -p 3000 -b '0.0.0.0'
volumes:
- .:/app
- gems:/usr/local/bundle
- yarn:/app/node_modules
node:
image: appname_rails
command: [
"./.docker-config/wait-for-it.sh",
"rails:3000",
"--timeout=0",
"--",
"./bin/webpack-dev-server"
]
volumes:
- .:/app
- yarn:/app/node_modules
```
This way, `webpack-dev-server` won't start until the Rails development
server is fully up and running.
[]{#9-start-entrypoint-scripts-with-set-e-and-end-with-exec}
### 9. Start entrypoint scripts with `set -e` and end with `exec "$@"`
### 9. Start entrypoint scripts with `set -e` and end with `exec "$@"` [\#](#9-start-entrypoint-scripts-with-set-e-and-end-with-exec "Direct link to 9. Start entrypoint scripts with set -e and end with exec "$@""){.anchor aria-label="Direct link to 9. Start entrypoint scripts with set -e and end with exec \"$@\""}
The setup we\'ve described here depends a lot on using
The setup we've described here depends a lot on using
[entrypoint](https://docs.docker.com/compose/compose-file/#entrypoint)
scripts to install dependencies and manage other setup. There are two
things you should include in **every single one** of these scripts, one
@@ -239,13 +245,13 @@ at the beginning, one at the end:
- At the end of the file, put `exec "$@"`. Without this, the
instructions you pass in with the
[command](https://docs.docker.com/compose/compose-file/#command)
directive won\'t execute.
directive won't execute.
[Here\'s a good StackOverflow
[Here's a good StackOverflow
answer](https://stackoverflow.com/a/48096779) with some more
information.
### 10. Target different CPU architectures with `BUILDARCH` [\#](#10-target-different-cpu-architectures-with-buildarch "Direct link to 10. Target different CPU architectures with BUILDARCH"){.anchor} {#10-target-different-cpu-architectures-with-buildarch}
### 10. Target different CPU architectures with `BUILDARCH`
We're presently about evenly split between Intel and Apple Silicon
laptops. Most of the common base images you pull from
@@ -260,10 +266,12 @@ As mentioned previously, we'll often need a specific version of Node.js
running inside a Ruby-based image. A way we'd commonly set this up is
something like this:
FROM ruby:2.7.6
```dockerfile
FROM ruby:2.7.6
RUN curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz
| tar xzf - --strip-components=1 -C "/usr/local"
RUN curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz
| tar xzf - --strip-components=1 -C "/usr/local"
```
This works fine on Intel Macs, but blows up on Apple Silicon -- notice
the `x64` in the above URL? That needs to be `arm64` on an M1. The
@@ -281,22 +289,24 @@ conditional functionality in the Dockerfile spec, we can do a little bit
of shell scripting inside of a `RUN` command to achieve the desired
result:
FROM ruby:2.7.6
```dockerfile
FROM ruby:2.7.6
ARG BUILDARCH
ARG BUILDARCH
RUN if [ "$BUILDARCH" = "arm64" ];
then curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-arm64.tar.gz
| tar xzf - --strip-components=1 -C "/usr/local";
else curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz
| tar xzf - --strip-components=1 -C "/usr/local";
fi
RUN if [ "$BUILDARCH" = "arm64" ];
then curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-arm64.tar.gz
| tar xzf - --strip-components=1 -C "/usr/local";
else curl -sS https://nodejs.org/download/release/v16.17.0/node-v16.17.0-linux-x64.tar.gz
| tar xzf - --strip-components=1 -C "/usr/local";
fi
```
This way, a dev running on Apple Silicon will download and install
`node-v16.17.0-linux-arm64`, and someone with Intel will use
`node-v16.17.0-linux-x64`.
### 11. Prefer `docker compose` to `docker-compose` [\#](#11-prefer-docker-compose-to-docker-compose "Direct link to 11. Prefer docker compose to docker-compose"){.anchor} {#11-prefer-docker-compose-to-docker-compose}
### 11. Prefer `docker compose` to `docker-compose`
Though both `docker compose up` and `docker-compose up` (with or without
a hyphen) work to spin up your containers, per this [helpful
@@ -307,24 +317,12 @@ to Go with the rest of the docker project."
*Thanks [Dylan](https://www.viget.com/about/team/dlederle-ensign/) for
this one.*
[[Learn More]{.util-breadcrumb-md .mb-8 .group-hover:translate-y-20
.group-hover:opacity-0 .transition-all .ease-in-out
.duration-500}](https://www.viget.com/careers/application-developer/){.relative
.flex .group .flex-col .p-32 .md:p-40 .lg:p-64 .z-10}
### We're hiring Application Developers. Learn more and introduce yourself. {#were-hiring-application-developers.-learn-more-and-introduce-yourself. .text-20 .md:text-24 .lg:text-32 .font-bold .leading-[170%] .group-hover:-translate-y-20 .transition-transform .ease-in-out .duration-500}
![](data:image/svg+xml;base64,PHN2ZyBjbGFzcz0icmVjdC1pY29uLW1kIHNlbGYtZW5kIG10LTE2IGdyb3VwLWhvdmVyOi10cmFuc2xhdGUteS0yMCB0cmFuc2l0aW9uLWFsbCBlYXNlLWluLW91dCBkdXJhdGlvbi01MDAiIHZpZXdib3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMuNzg0OCAxOS4zMDkxQzEzLjQ3NTggMTkuNTg1IDEzLjAwMTcgMTkuNTU4MyAxMi43MjU4IDE5LjI0OTRDMTIuNDQ5OCAxOC45NDA1IDEyLjQ3NjYgMTguNDY2MyAxMi43ODU1IDE4LjE5MDRMMTguNzg2NiAxMi44MzAxTDQuNzUxOTUgMTIuODMwMUM0LjMzNzc0IDEyLjgzMDEgNC4wMDE5NSAxMi40OTQzIDQuMDAxOTUgMTIuMDgwMUM0LjAwMTk1IDExLjY2NTkgNC4zMzc3NCAxMS4zMzAxIDQuNzUxOTUgMTEuMzMwMUwxOC43ODU1IDExLjMzMDFMMTIuNzg1NSA1Ljk3MDgyQzEyLjQ3NjYgNS42OTQ4OCAxMi40NDk4IDUuMjIwNzYgMTIuNzI1OCA0LjkxMTg0QzEzLjAwMTcgNC42MDI5MiAxMy40NzU4IDQuNTc2MTggMTMuNzg0OCA0Ljg1MjEyTDIxLjIzNTggMTEuNTA3NkMyMS4zNzM4IDExLjYyNDQgMjEuNDY5IDExLjc5MDMgMjEuNDk0NSAxMS45NzgyQzIxLjQ5OTIgMTIuMDExOSAyMS41MDE1IDEyLjA0NjEgMjEuNTAxNSAxMi4wODA2QzIxLjUwMTUgMTIuMjk0MiAyMS40MTA1IDEyLjQ5NzcgMjEuMjUxMSAxMi42NEwxMy43ODQ4IDE5LjMwOTFaIj48L3BhdGg+Cjwvc3ZnPg==){.rect-icon-md
.self-end .mt-16 .group-hover:-translate-y-20 .transition-all
.ease-in-out .duration-500}
---
So there you have it, a short list of the best practices we've developed
over the last several years of working with Docker. We'll try to keep
this list updated as we get better at doing and documenting this stuff.
If you're interested in reading more, here are a few good links:
- [Ruby on Whales: Dockerizing Ruby and Rails
@@ -334,12 +332,9 @@ If you're interested in reading more, here are a few good links:
- [Docker + Rails: Solutions to Common
Hurdles](https://www.viget.com/articles/docker-rails-solutions-to-common-hurdles/)
------------------------------------------------------------------------
1. [Namely, there's a significant performance hit when running Docker
on Mac (as we do) in addition to the cognitive hurdle of all your
stuff running inside containers. If I worked at a product shop,
where I was focused on a single codebase for the bulk of my time,
I'd think hard before going all in on local
Docker.[↩︎](#fnref1){.footnote-back role="doc-backlink"}]{#fn1}
[^1]: Namely, there's a significant performance hit when running Docker
on Mac (as we do) in addition to the cognitive hurdle of all your
stuff running inside containers. If I worked at a product shop,
where I was focused on a single codebase for the bulk of my time,
I'd think hard before going all in on local
Docker.