5. Group commands by how likely they are to change individually
FROM ruby:2.5.5# We usually only need to run this onceRUN apt update && \
apt install -y mysql-client postgresql-client nginx# We usually run this every time we add a new dependencyRUN bundle installCMD ruby -e "puts 1 + 2"
FROM ruby:2.5.5RUN gem install sinatra -v 2.0.5RUNecho'require "sinatra"; run Sinatra::Application.run!' > config.ru# Hmmm... Which user will run this?CMD rackup
7. Avoid running your application as root
FROM ruby:2.5.5RUN gem install sinatra -v 2.0.5# Create a dedicated user for running the applicationRUN adduser -D my-sinatra-user# Set the user for RUN, CMD or ENTRYPOINT calls from now on# Note that this doesn't apply to COPY or ADD, which use a --chown argument insteadUSER my-sinatra-user# Set the base directory that will be used from now onWORKDIR /home/my-sinatra-userRUNecho'require "sinatra"; run Sinatra::Application.run!' > config.ru# This is run under the my-sinatra-user userCMD rackup
8.
FROM ruby:2.5.5-alpine
RUN adduser -D my-sinatra-userUSER my-sinatra-userWORKDIR /home/my-sinatra-user# Hmmm... Which user owns these files?COPY Gemfile Gemfile.lock ./RUN bundle installCMD rackup
8. When running COPY or ADD (as a different user) use --chown
FROM ruby:2.5.5-alpine
RUN adduser -D my-sinatra-userUSER my-sinatra-userWORKDIR /home/my-sinatra-user# The files will be owned by my-sinatra-userCOPY --chown=my-sinatra-user Gemfile Gemfile.lock ./RUN bundle installCMD rackup
9. Avoid leaking secrets inside your image
FROM ruby:2.5.5ENV DB_PASSWORD "secret stuff"
Secrets should never appear inside your Dockerfile in plain text!
9. Avoid leaking secrets inside your image
Alternatives:
Build-time arguments: the ARG command and --build-arg Docker argument.
Environment variables: the -e or --env-file Docker arguments.
Kubernetes secrets or similar.
10.
FROM ruby:2.5.5ARG PRIVATE_SSH_KEY
# Hmmm...RUNecho"${PRIVATE_SSH_KEY}" > /root/.ssh/id_rsa# More hmmm...RUN bundle install# Cleaning up the private keyRUN rm /root/.ssh/id_rsa
10. Always clean up injected secrets within the same build step
11. Fetching private dependencies via a Github token injected through the gitconfig
FROM ruby:2.5.5ARG GITHUB_TOKEN
# This is a private gem that GITHUB_TOKEN has access toRUNecho'source "https://rubygems.org"; gem "some-private-gem", git: "git@github.com:myorg/some-private-gem"' > Gemfile# First insteadOf is for Gemfile, second is for package.jsonRUN git config --global url."https://${GITHUB_TOKEN}:x-oauth-basic@github.com/myorg".insteadOf git@github.com:myorg && \
git config --global --add url."https://${GITHUB_TOKEN}:x-oauth-basic@github.com/myorg".insteadOf ssh://git@github && \
bundle install && \
rm ~/.gitconfig
docker build --build-arg GITHUB_TOKEN=xxx .
12.
# Hmmm...FROM ruby:2.5.5CMD ruby -e "puts 1 + 2"
> docker images -a | grep base-image
normal-base-image 869MB
12. Minimize image size by opting for small base images when possible
13. Use multi-stage builds to reduce the size of your image
# The "builder" image will build nokogiriFROM ruby:2.5.5-alpine AS builder
RUN apk add --update \
build-base \
libxml2-dev \
libxslt-devRUNecho'source "https://rubygems.org"; gem "nokogiri"' > GemfileRUN bundle install# The final image: we start clean and copy over the built bundleFROM ruby:2.5.5-alpine
COPY --from=builder /usr/local/bundle/ /usr/local/bundle/CMD /bin/sh
13. Use multi-stage builds to reduce the size of your image
FROM ruby:2.5.5-alpine
# This is a secretARG PRIVATE_SSH_KEY
# Our Gemfile contains a private dependencyRUNecho'source "https://rubygems.org"; gem "a-private-gem"' > Gemfile# We require the secret for installing the private dependenciesRUN mkdir -p /root/.ssh/ && \
echo"${PRIVATE_SSH_KEY}" > /root/.ssh/id_rsa && \
bundle install && \
rm /root/.ssh/id_rsaCMD ruby -e "puts 1 + 2"
docker build --build-arg PRIVATE_SSH_KEY=xxx .
14.
> docker history my-fancy-image
IMAGE CREATED CREATED BY SIZE COMMENT
67e60c0853ab 19 seconds ago /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "ruby… 0B
94dd778c4b5d 20 seconds ago |1 PRIVATE_SSH_KEY=xxx /bin/sh -c mkdir -p /… 30.9MB
32a993af7bfb About a minute ago |1 PRIVATE_SSH_KEY=xxx /bin/sh -c echo'sour… 45B
2be964ad91c7 About a minute ago /bin/sh -c #(nop) ARG PRIVATE_SSH_KEY 0B
44723f3ab2bd 4 months ago /bin/sh -c #(nop) CMD ["irb"] 0B
<missing> 4 months ago /bin/sh -c mkdir -p "$GEM_HOME" && chmod 777… 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV PATH=/usr/local/bundl… 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV BUNDLE_PATH=/usr/loca… 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV GEM_HOME=/usr/local/b… 0B
<missing> 4 months ago /bin/sh -c set -ex && apk add --no-cache -… 45.5MB
<missing> 4 months ago /bin/sh -c #(nop) ENV RUBYGEMS_VERSION=3.0.3 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV RUBY_DOWNLOAD_SHA256=… 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV RUBY_VERSION=2.5.5 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV RUBY_MAJOR=2.5 0B
<missing> 4 months ago /bin/sh -c mkdir -p /usr/local/etc && { e… 45B
<missing> 4 months ago /bin/sh -c apk add --no-cache gmp-dev 3.4MB
<missing> 4 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 4 months ago /bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6a… 5.53MB
14. Use multi-stage builds to avoid leaking secrets in your docker history
# The builder image makes use of the secretFROM ruby:2.5.5-alpine AS builder
ARG PRIVATE_SSH_KEY
RUNecho'source "https://rubygems.org"; gem "a-private-gem"' > GemfileRUN mkdir -p /root/.ssh/ && \
echo"${PRIVATE_SSH_KEY}" > /root/.ssh/id_rsa && \
bundle install && \
rm /root/.ssh/id_rsa# The final image doesn't need the secretFROM ruby:2.5.5-alpine
COPY --from=builder /usr/local/bundle/ /usr/local/bundle/CMD ruby -e "puts 1 + 2"
14. Use multi-stage builds to avoid leaking secrets in your docker history
> docker history my-fancy-image
IMAGE CREATED CREATED BY SIZE COMMENT
2706a2f47816 8 seconds ago /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "ruby… 0B
86509dba3bd9 9 seconds ago /bin/sh -c #(nop) COPY dir:e110956912ddb292a… 3.16MB
44723f3ab2bd 4 months ago /bin/sh -c #(nop) CMD ["irb"] 0B
<missing> 4 months ago /bin/sh -c mkdir -p "$GEM_HOME" && chmod 777… 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV PATH=/usr/local/bundl… 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV BUNDLE_PATH=/usr/loca… 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV GEM_HOME=/usr/local/b… 0B
<missing> 4 months ago /bin/sh -c set -ex && apk add --no-cache -… 45.5MB
<missing> 4 months ago /bin/sh -c #(nop) ENV RUBYGEMS_VERSION=3.0.3 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV RUBY_DOWNLOAD_SHA256=… 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV RUBY_VERSION=2.5.5 0B
<missing> 4 months ago /bin/sh -c #(nop) ENV RUBY_MAJOR=2.5 0B
<missing> 4 months ago /bin/sh -c mkdir -p /usr/local/etc && { e… 45B
<missing> 4 months ago /bin/sh -c apk add --no-cache gmp-dev 3.4MB
<missing> 4 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 4 months ago /bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6a… 5.53MB
15.
FROM ruby:2.5.5RUNecho'source "https://rubygems.org"; gem "sinatra"' > GemfileRUN bundle install# A simple Sinatra app which prints out HUUUUUP when the process receives the HUP signal.RUNecho'require "sinatra"; set bind: "0.0.0.0"; Signal.trap("HUP") { puts "HUUUUUP" }; run Sinatra::Application.run!' > config.ruCMD bundle exec rackup
15. When setting the CMD instruction, prefer the exec format over the shell format
FROM ruby:2.5.5RUNecho'source "https://rubygems.org"; gem "sinatra"' > GemfileRUN bundle install# A simple Sinatra app which prints out HUUUUUP when the process receives the HUP signal.RUNecho'require "sinatra"; set bind: "0.0.0.0"; Signal.trap("HUP") { puts "HUUUUUP" }; run Sinatra::Application.run!' > config.ruCMD ["bundle", "exec", "rackup"]
> docker exec $(docker ps -q) ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 29.0 0.2 43988 25632 pts/0 Ssl+ 14:47 0:00 /usr/local/bundle/bin/rackup
root 8 0.0 0.0 7640 2668 ? Rs 14:47 0:00 ps aux
# DockerfileFROM ruby:2.5.5-alpine
# A Gemfile that contains a test dependency (minitest)RUNecho'source "https://rubygems.org"; gem "sinatra"; group(:test) { gem "minitest" }' > GemfileRUNecho'require "sinatra"; run Sinatra::Application.run!' > config.ruRUNecho'require "minitest/spec"; require "minitest/autorun"; class TestIndex < MiniTest::Test; def test_it_passes; assert(true); end; end' > test.rb# By default we don't install development or test dependenciesRUN bundle install --without development testCMD ["rackup"]
# development.DockerfileFROM ruby:2.5.5-alpine
# A Gemfile that contains a test dependency (minitest)RUNecho'source "https://rubygems.org"; gem "sinatra"; group(:test) { gem "minitest" }' > GemfileRUNecho'require "sinatra"; run Sinatra::Application.run!' > config.ruRUNecho'require "minitest/spec"; require "minitest/autorun"; class TestIndex < MiniTest::Test; def test_it_passes; assert(true); end; end' > test.rb# Here we want the development dependenciesRUN bundle installCMD ["rackup"]
16. Combine production, test and development build processes into a single Dockerfile by using multi-stage builds
FROM ruby:2.5.5-alpine AS builder
# A Gemfile that contains a test dependency (minitest)RUNecho'source "https://rubygems.org"; gem "sinatra"; group(:test) { gem "minitest" }' > GemfileRUNecho'require "sinatra"; run Sinatra::Application.run!' > config.ru# By default we don't install development or test dependenciesRUN bundle install --without development test# A separate build stage installs test dependencies and runs your testsFROM builder AS test
RUN bundle install --with testRUNecho'require "minitest/spec"; require "minitest/autorun"; class TestIndex < MiniTest::Test; def test_it_passes; assert(true); end; end' > test.rbRUN bundle exec ruby test.rb# The production artifact doesn't contain any test dependenciesFROM ruby:2.5.5-alpine
COPY --from=builder /usr/local/bundle/ /usr/local/bundle/COPY --from=builder /config.ru ./CMD ["rackup"]
16. Combine production, test and development build processes into a single Dockerfile by using multi-stage builds