Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 6

Running Containers as a Non-Root User

Recommended resources

Hello! Welcome to the sixth lesson in our module on "Production-Grade Dockerfiles."

In our previous lesson, we focused on runtime reliability by implementing the HEALTHCHECK instruction. Today, we're continuing our journey towards secure, production-ready containers by tackling a fundamental security principle: running containers as a non-root user.

By default, Docker runs containers as the root user. This is convenient but dangerous. If an attacker compromises your application, they gain root privileges inside the container, giving them full control over the container's environment, filesystem, and processes. This violates the principle of least privilege.

In this lesson, you will learn how to use the USER instruction to create and run your container as a dedicated, unprivileged user. This simple change drastically reduces the potential attack surface and is a non-negotiable step for any production workload. Given your background in DevOps and interest in cybersecurity, you'll immediately see how this fits into a broader defense-in-depth strategy.

1. The Risk of Running as Root

Before we fix the problem, let's see it in action. A container running as root can do anything inside its namespace—install packages, modify files, and more. This gives an attacker who finds a vulnerability in your application a powerful foothold.

Docker security best practices| How to secure your container.

This short video demonstrates the problem clearly. Watch how a container running as the default root user has unrestricted permissions, allowing for arbitrary software installation—a significant security risk.

Watch the segment from 01:46 to 03:05. Notice how the user is 'root' and can easily run apt update and install new packages like nmap.

This ability to modify the container environment at runtime is precisely what we want to prevent.


2. The Solution: Create and Use a Non-Root User

The solution involves two main steps within your Dockerfile:

  1. Create a user and group: Use a RUN instruction with standard Linux commands like useradd or adduser.
  2. Switch to the new user: Use the USER instruction to specify that all subsequent instructions (and the final running container) should execute as this new user.

To get a comprehensive overview of the best practices for this, we'll turn to the official Docker blog.

Understanding the Docker USER Instruction

This article from the official Docker blog, 'Understanding the Docker USER Instruction', provides an excellent guide on the topic. It covers the 'why' and the 'how', along with crucial best practices.

Please read the introduction, the 'UID/GID: A refresher' section, the 'Best practices' section, and the 'Example' section. Pay close attention to the Dockerfile examples that create and switch to a custom user.

Now, let's see a practical demonstration of this fix and its effect.

Docker security best practices| How to secure your container.

The same video we watched earlier also shows the solution. It demonstrates how to add a user and then confirms that the new, unprivileged user cannot perform administrative tasks.

Watch the segment from 04:09 to 07:22. Observe the use of groupadd and useradd in the Dockerfile, followed by the USER instruction. Critically, notice how attempts to create directories or install software now fail with 'permission denied'.

This before-and-after clearly illustrates the security benefit. Here is a simple visual summary of what this looks like in a Dockerfile.

Dockerfile Example for Non-Root User
This Dockerfile uses `adduser` to create a non-root user and then the `USER` instruction to switch to it, ensuring subsequent commands run with lower privileges.

3. Best Practices for User Management

As highlighted in the Docker blog post, simply adding a user is just the start. For production-grade files, consider these practices:

  • Specify User by UID/GID: Instead of USER myuser, prefer using the numeric ID, like USER 1001:1001. Usernames can sometimes conflict or be inconsistent across different base images, but UIDs and GIDs are more reliable.
  • Manage File Permissions: When you switch to a new user, that user must have permission to read/write to the necessary directories (like your application's working directory). You often need to pair user creation with chown. For example:
    # Create app directory
    RUN mkdir /app
    
    # Create a non-root user and group
    RUN addgroup -S appgroup && adduser -S appuser -G appgroup
    
    # Give the new user ownership of the app directory
    RUN chown -R appuser:appgroup /app
    
    # Switch to the new user
    USER appuser
    
    # Set working directory
    WORKDIR /app
    
    COPY . .
    
    CMD [ "node", "server.js" ]
    
  • Temporary Root for Privileged Operations: It's a common and valid pattern to perform administrative tasks as root before switching to your application user.
    # Start as root
    FROM ubuntu:22.04
    
    # Perform privileged operations
    RUN apt-get update && apt-get install -y curl
    
    # Now, create and switch to the non-root user
    RUN useradd -m myapp
    USER myapp
    
    # Subsequent instructions run as myapp
    CMD ["/bin/bash"]
    

4. An Advanced Pattern: Handling File Permissions in Development

Here's a scenario every DevOps engineer encounters: a developer is using a bind mount to sync code from their host machine into a running container. If the container process (running as UID 1001) creates a file in that volume, the file on the host machine is now owned by user 1001. If the developer's host user is UID 1000, they'll get permission errors trying to edit that file.

We can solve this elegantly by combining the USER instruction with the ARG instruction you learned about in a previous lesson.

Running Docker Containers as a Non-root User with a Custom UID / GID

This blog post, 'Running Docker Containers as a Non-root User with a Custom UID / GID', details a professional pattern to solve the volume permission problem. It's a fantastic real-world application of several Dockerfile concepts.

Read the section 'Custom UID / GID in Your Docker Related Files' and its three subsections. This will show you how to: (1) use ARG in the Dockerfile to accept a UID/GID, (2) pass these ARGs from a docker-compose.yml file, and (3) define them in an .env file for easy configuration.

This pattern makes your Docker setup much more portable and developer-friendly by allowing each user to build the image with their own UID/GID, ensuring seamless file ownership across the container-host boundary.

Test your understanding!

You are creating a Dockerfile for a Python application. For security and developer convenience, you need to implement a non-root user with a configurable UID/GID.

Your task is to write the Dockerfile instructions that:

  1. Define build arguments APP_UID and APP_GID, both defaulting to 1001.
  2. Create a group named appgroup with the provided APP_GID.
  3. Create a user named appuser with the provided APP_UID, belonging to the appgroup. The user should have a home directory.
  4. Switch to the appuser.

Hint: On an Alpine base, you might use addgroup -g "${APP_GID}" appgroup && adduser -u "${APP_UID}" -G appgroup -h /home/appuser -D appuser.

Show answer
# Define build arguments with defaults
ARG APP_UID=1001
ARG APP_GID=1001

# Create a group and user with the specified IDs. 
# The -D flag means 'don't assign a password'.
# The -S flag means 'create a system user'.
RUN addgroup -S -g ${APP_GID} appgroup && \
    adduser -S -u ${APP_UID} -G appgroup appuser

# Switch to the created user
USER appuser

This snippet correctly uses ARG to make the IDs configurable and then applies them during user creation before finally switching the context with USER.


Conclusion

You've now mastered a critical security practice for containerization. By moving away from the default root user, you apply the principle of least privilege to significantly harden your Docker images against potential exploits.

Key Takeaways:

  • Running containers as root is a major security risk. Always create and use a dedicated non-root user.
  • The process is a two-step: RUN useradd/adduser to create the user, then USER to switch to it.
  • Remember to manage file permissions with chown so your application user can access its files and directories.
  • For advanced use cases, especially in development, parameterize the UID and GID using ARG to solve file ownership issues with volumes.

In our next lesson, we will address another subtle but important aspect of production containers: signal handling and the role of an init system. Now that we have a secure process running as the right user, we need to ensure it can start and, more importantly, stop gracefully.