copy-edit viget posts
This commit is contained in:
@@ -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}
|
||||
|
||||
{.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.
|
||||
|
||||
Reference in New Issue
Block a user