How to Secure a Docker Container with Distroless Images
How can we increase security within our containers? This is the question we all ask when we begin considering our production build.
This is where distroless images come in.
The premise behind this discussion is this: we only want to install the things that we need.
With fewer packages installed, we’ll have:
- A smaller security footprint
- A smaller image size
- Lower CPU/memory utilization
- Simpler dependency management
If our current container is based on Debian, Alpine, or some other Linux distribution, it will likely have a shell installed. If an attacker gains access to the container, they may be able to use that shell to execute an attack.
We can get around this by using a base image that contains only our application in its runtime dependencies.
The good thing is that containers require far fewer things can we’d expect. For starters, there’s no need for a package manager or even a shell.
We generally won’t
ssh into the shell of a container and install a new package, and we generally shouldn’t.
With distroless images, we are essentially following the best practice of only installing what we need to run our application, which happens to add a layer of security within our containers.
A Simple Example
We can use a multi-stage build to run our production containers with a distroless image.
Multi-stage builds in our
Dockerfile allow for as many stages of the build process as we’d like. We can daisy-chain the outputs of one stage as inputs to the next stage. The only expectation is that the last stage will be our production stage with the distroless image.
We’ll only have two stages: one to build the image and another to run our application.
### Stage 1 ### FROM node:14-alpine3.13 as builder WORKDIR /usr/app COPY package*.json . RUN npm install COPY . . ### Stage 2 ### FROM gcr.io/distroless/nodejs:14 WORKDIR /usr/app COPY --from=builder /usr/app . USER 1000 CMD ["index.js"]
Here are the changes to note with this multi-stage, distroless setup.
Provide a name for the build image. We add
builder as a name for the first stage. We can then reference the files in this stage using the
COPY --from directive in the next stage.
Pull a distroless image. We use the
nodejs distroless image provided by Google.
Execute as a non-root user. By default, many containers are configured to execute as root, which is needed to install packages and make configuration settings. But after all that is done, we can change to a non-root user. We can specify a numerical ID
1000, which corresponds to a default non-root, user ID provided by
node. Usually, we can just use
USER node, but
node isn’t defined in the distroless image.
Use the CMD exec form. If we try to use the standard
ENTRYPOINT, it will try to execute a shell, and we don’t have a shell… so it’ll fail.
Now, our application is running in a container with nothing but the bare requirements to run our application.
But why is it called “distroless”? We can see in Google’s official distroless repository that these “distroless” images are based off of
debian10. So, they are a distribution… but distroless? I like it.
More Docker Articles
- How to Execute a Shell Command Immediately Inside a Docker Container
- How to Remove All Docker Images Locally
- How to Fix "Port is already allocated" Error in Docker
- How to Develop with TypeScript in Docker with Hot Reloading
- How to Build TypeScript for Production in Docker
- How to Access Environment Variables in React Production Build
- How to Dockerize the MERN Stack for Development
- How to Dockerize a Node.js/Mongo App with Live Reload (nodemon)
- How to Update Docker Containers on File Change