Skip to content

Instantly share code, notes, and snippets.

@Popsiclestick
Last active May 29, 2023 19:10
Show Gist options
  • Save Popsiclestick/666be072aa3eeda4f8f9ed6a7b31e283 to your computer and use it in GitHub Desktop.
Save Popsiclestick/666be072aa3eeda4f8f9ed6a7b31e283 to your computer and use it in GitHub Desktop.
Dockerfile example with best practices
### Generic Dockerfile demonstrating good practices
### Imports
# Bad. You risk both the stability and security of your application
# You don't know what they might merge into their image or who they may give control of the project
# https://twitter.com/b0rk/status/1226856930875932672/photo/1
FROM random-person/golang:latest
# Bad-ish. We don't need Ubuntu, it comes with unnecessary bloat
# Nor do we want latest. Predictibility is better, especially when using in a build system, etc
FROM ubuntu:latest
# Good. Using a small image since our app has no dependency on Ubuntu
FROM alpine:3.11
# Good. Use an image that is specifically built for the needed dependencies
FROM golang:1.13-alpine3.11
# Sometimes it is better to build your own image from scratch
# You can build slim containers with exactly what you need. Default packages are typically large and increase attack surface
### Installing packages
# Bad. Every command causes the previous image to change, thus creating a new layer
# This also leaves package cache files that will persist in the image
# Install in one command unless there is a logical separation between commands
RUN apk update
RUN apk add wget
RUN apk add git
# Better. Lets update, add, and clean up on one line. Also use --update instead of using a separate build commit
RUN apk add --update wget git \
&& rm -rf /var/cache/apk/*
# Best. Avoid caching content when possible
RUN apk add --no-cache wget git
### Users & Groups
# If a service can run without privileges, use USER to change to a non-root user.
# Start by creating the user and group. We do not want to be running as the root user.
# The goal is to setup our users applications, drop our permissions, and run our application
RUN adduser -D -u 1000 -g 1000 container-user
### Fetching files
# Bad. We do not want to keep temporary or unused file between commands, always clean up in the same command
RUN wget http://example.com/my/large/app.tar.gz -O /root/app.tar.gz
RUN mkdir /opt/somedir
RUN mv /root/app.tar.gz /opt/somedir/app.tar.gz
RUN tar -zxvf /opt/somedir/app.tar.gz
# Good. Doing this as a one liner and piping to tar leaves nothing on the server but the extracted files
# Note: If you don't switch to your container user, you'll need to remember to `chown -R` your files later
USER container-user
RUN mkdir /opt/somedir \
&& wget -q -O- http://example.com/my/large/app.tar.gz \
| tar -zxv -C /opt/somedir/
### Config files
# Non-ideal. Don't bake configuration into an image as a general rule of thumb, especially with sensitive data
COPY super_secret_database.conf /opt/app/config/database.yml
# Better. Write a wrapper script that will take arguments
COPY entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"] # where entrypoint takes config information and passes to the app or updates the config
# Better. Utilize environment variables. Update database.yml to read from ENV and pass them when running
# docker run -e PROD_DB=localhost myapp
# Best. Utilize your orchestrator to mount sensitive config files and secrets on volumes backed by tmpfs (Memory filesystem).
# Kubernetes, Swarm, and Nomad support memory based filesystem mounts
# Note: Try to avoid passing secrets in environment variables. Application misconfigurations could leak secrets.
### Drop user privileges
# Optimal. If you didn't already drop privileges. You want to do this before ENTRYPOINT/CMD options
# We want to drop privileges down to our application user to avoid running as root
USER 1000
### Allow default arguments with CMD
# Bad. Leaves little to no flexibility
ENTRYPOINT ["/entrypoint.sh", "--db localhost"]
# Good. ENTRYPOINT acts as our image's main command, CMD allows us to set default flags.
# This provides us a way to override those agruments "docker run myapp --db 192.0.2.10"
ENTRYPOINT ["/entrypoint.sh"]
CMD ["--db", "localhost"]
# Note: The main purpose of a CMD is to provide defaults for an executing container.
# These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT instruction as well.
### Run applications in the foreground - Do not daemonize applications
# Bad. NGINX runs as a daemon by default
ENTRYPOINT nginx
# Good. Capture the output to stdout/stderr and keep Nginx in the foreground
RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log
CMD ["nginx", "-g", "daemon off;"]
# Better. Run your application with a lightweight init wrapper like Tini (https://github.com/krallin/tini)
# Tini provides some added benefit by handling things like zombie processes and signals
ENTRYPOINT ["/tini", "--"]
CMD ["/your/program", "-and", "-its", "arguments"]
# Note: If you're running Nginx, I'd recommend looking at something like https://hub.docker.com/r/nginxinc/nginx-unprivileged
### Only run one application per container
# Bad. Running rails and then running NGINX in front of it
RUN apk add --update nginx ruby
RUN /opt/rails/bin/rails s
CMD ["nginx", "-g", "daemon off;"]
# Good. Run two separate containers for each application
# They can resolve and communicate through the overlay network
# docker network create --driver bridge mynetwork
# docker run --name myrailsapp --net mynetwork rails
# docker run --name nginxfrontend --net mynetwork -e RAILS_HOST=myrailsapp nginx
# Note: This is where orchestrators like Swarm, Kubernetes, and Nomad come into play
# Find more examples:
# https://github.com/Popsiclestick/container-best-practices
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment