
Welcome to Part 3 of the “Where to Start with Docker” series! In Part 1, we explored the fundamentals of Docker. In Part 2, we built and optimized Docker images using multi-stage builds. Now, it’s time to focus on one of the security aspects when working with Docker.
Containers are lightweight, portable, and efficient, but without proper precautions, they can introduce vulnerabilities into your applications and infrastructure. From misconfigured Dockerfiles to insecure images or running containers with excessive privileges, small oversights can lead to significant security risks.
In this part of the series, we’ll dive into best practices for hardening Docker containers, covering key areas such as:
- Minimizing security risks in your Dockerfiles: Choosing minimal base images, managing secrets, and avoiding unnecessary tools or dependencies
- Running containers with the least privileges: Reducing permissions and limiting what containers can access.
- Securing image sources and registries: Ensuring images come from trusted sources and managing private registries securely.
- Leveraging Docker security tools and features: Using built-in features and external tools to identify vulnerabilities and enhance container security.
By the end of this post, you’ll have a solid understanding of how to create secure container environments. You will ensure your applications are protected against common threats. Let’s dive in and make your containers not just efficient, but resilient and secure!
Minimizing security risks in your Dockerfiles
A well-constructed Dockerfile is your first line of defense against security vulnerabilities. By following these best practices, you can create secure, efficient, and maintainable container images.
Use Minimal Base Images
Selecting a minimal base image is a crucial step toward securing your Docker containers. Minimal images include only the necessary components for your application. Examples are scratch, distroless, or alpine. This reduces the attack surface. This approach minimizes vulnerabilities associated with unused or unnecessary software.
Why Choose Minimal Base Images?
- Reduced Attack Surface: Smaller images inherently contain fewer libraries and tools, lowering the chances of exploitable vulnerabilities. For example,
scratchis completely empty, making it ideal for statically compiled binaries. - Improved Performance: Lightweight images lead to faster build times, smaller image sizes, and quicker deployments.
- Fewer Dependency Risks: By limiting dependencies, you avoid inheriting vulnerabilities present in unused libraries or binaries.
Research Application Requirements
Analyze your application’s needs and choose an image that provides only the required components.
- For static Go binaries, use
scratchto avoid unnecessary runtime dependencies, for example. - For applications needing runtime support like
glibcoropenssl, opt fordistrolessorchainguard.
Avoid Unnecessary Dependencies
Large base images like ubuntu or debian often include unnecessary tools and libraries, significantly increasing the risk of vulnerabilities. Always assess if your application can function without them.
# Build Stage
FROM golang:1.23.3 AS build
# Set the working directory
WORKDIR /workspace
# Copy project files and build the binary
COPY . /workspace
RUN go mod download
RUN go build -ldflags="-w -s" -o /bin/project
# Deployment Stage
FROM gcr.io/distroless/static:nonroot
# Copy the binary from the build stage
COPY --from=build /bin/project /bin/project
# Set the user to a non-root user for security
USER 50000:50000
# Set the default command
ENTRYPOINT ["/bin/project"]
Benefits of Minimal Base Images
By selecting minimal base images, you achieve:
- Smaller Image Sizes: Faster uploads and downloads in your CI/CD pipeline.
- Fewer Vulnerabilities: Reduced exposure to exploits from unused software.
- Easier Maintenance: Simplified image updates and patching.
Adopting this best practice ensures a secure and streamlined runtime environment for your containers.
Security check of common base images
| CRITICAL | HIGH | MEDIUM | |
| debian:latest (debian 12.8) | 1 | 5 | 14 |
| alpine:latest (alpine 3.20.3) | 0 | 0 | 0 |
| gcr.io/distroless/static-debian12 (debian 12.8) | 0 | 0 | 0 |
| cgr.dev/chainguard/static (wolfi 20230201) | 0 | 0 | 0 |
Some of my favourite base images
- https://hub.docker.com/_/scratch
- Empty base image ideal for statically compiled binaries.
- Contains no libraries, files, or shell.
- https://github.com/GoogleContainerTools/distroless
- Google’s secure base images include essential runtime files (e.g., CA certificates,
glibc,openssl). - Lacks a package manager to prevent unwanted modifications.
- Google’s secure base images include essential runtime files (e.g., CA certificates,
- https://github.com/chainguard-images/images#chainguard-images
- Images built with security in mind, leveraging the
WolfiLinux distribution. - Includes hardened configurations and minimal packages.
- Images built with security in mind, leveraging the
- https://github.com/alpinelinux/docker-alpine
- A versatile 5 MB image with access to the Alpine package repository.
- Useful when additional lightweight tools are required.
Avoid Secrets in Dockerfiles
Hardcoding sensitive information such as API keys, tokens, or passwords in Dockerfiles is a serious security risk. Even if you delete these secrets in later layers, they can still persist in the image’s history. This persistence makes them accessible to anyone who inspects the image.
Why secrets in Docker files are dangerous
- Persistent Layer Caching: Docker builds images layer by layer. If a secret is added in an earlier layer, it becomes part of the image’s history, even if later removed. Tools like
docker historyordocker image savecan expose these secrets. - Image Sharing Risks: When an image contains secrets and is pushed to a registry, it becomes accessible. Anyone with access to the image can retrieve these secrets. This becomes a particular risk in shared or public repositories.
Best Practices for Managing Secrets in Docker Images
1. Pass Secrets at Runtime
Instead of hardcoding secrets in Dockerfiles, inject them dynamically when starting the container using environment variables.
Bad Practice: Hardcoding Secrets
ENV API_KEY="your-secret-key"
Good Practice: Passing Secrets at Runtime
docker run -e API_KEY="your-secret-key" your-image
2. Use Docker BuildKit for Temporary Secrets
Docker BuildKit provides a secure method to handle secrets during the build process. Secrets are mounted temporarily during the build and are not included in the final image layers.
Securely Use Secrets During Build
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
How to enable BuildKit
DOCKER_BUILDKIT=1 docker build --secret id=mysecret,src=/path/to/secret .
Secrets passed this way are securely mounted during the build and are never stored in the image or its layers.
3. Avoid Introducing Secrets During Build
Secrets should never be part of the image or build process. Always pass them dynamically at runtime.
For example:
- Use tools like
docker-composewith environment variables to inject secrets. - Use orchestrators like Kubernetes to mount secrets as files or environment variables.
4. Do Not Use --build-arg for Secrets
The --build-arg option might seem like a convenient way to pass secrets during the build process. However, it is insecure. Build arguments are embedded in the image metadata. An attacker can retrieve them using docker history.
docker build --build-arg API_KEY=your-secret-key -t your-image .
An attacker can extract the secret with
docker history --no-trunc your-image
Instead, rely on Docker BuildKit as shown above.
Summary of Best Practices
- Never Hardcode Secrets: Avoid adding sensitive data directly to Dockerfiles.
- Use Runtime Secrets: Inject secrets at runtime through environment variables or secret management tools.
- Leverage BuildKit: For build-time secrets, use Docker BuildKit to securely mount secrets without persisting them.
- Audit Your Images: Regularly check your images for accidental secret exposure using tools like
docker history.
By following these practices, you can ensure your sensitive data remains protected, even when sharing or deploying Docker images.
Run as a Non-Root User
By default, Docker containers often run as the root user (UID 0), which poses significant security risks. A better approach is to create a dedicated non-root user. Use this user with only the necessary permissions to run your application. This minimizes the privileges available to processes within the container, reducing its attack surface.
Why Avoid Running as Root?
- Elevated Risk of Exploits
An exploited vulnerability in your containerized application gives the attacker full administrative control of the container. The attacker gains control by running it as root. Worse, they might escalate their privileges to access the host system. - Principle of Least Privilege
Run as a non-root user. This ensures that even if an attacker gains access to the container, their capabilities are limited. This protection helps both your application and the host.
Choosing a User ID (UID)
Here’s a breakdown of UID ranges and recommendations for container users:
- UID 0: Reserved for the root user.
- UIDs 1–999: Reserved for system accounts, services, and special use cases.
- UIDs 1000–1999: Typically used for regular administrative users.
- UIDs 2000–65533: Ideal for non-root users in containers. My recommendation is to choose a UID in this range for Docker images.
Avoid using UID 65534 (commonly mapped to the nobody account). This account was designed for specific use cases like NFS. It may conflict with other services.
How to Create and Use a Non-Root User
Define a non-root user directly in your Dockerfile, for example with the following steps:
# Create a user and switch to it
RUN adduser -u 2001 -D appuser
# Switch to the non-root user
USER appuser
Explanation of Commands:
adduser -u 2001 -D testuser: Adds a system userappuserwith:- UID 2001 (a safe range for non-root users).
- Assigned to the
appusergroup. - Don’t assign a password (
-D).
USER appuser: Setsappuseras the default user for all subsequent container operations.
Understanding UID Mapping Between Container and Host
The UID inside the container maps to a corresponding UID on the host system. For example:
- In the Dockerfile,
appuseris assigned UID 2001. - If a host user (e.g., Alice) has UID 2001, processes inside the container appear to be owned by Alice from the host’s perspective.
Why This Matters:
- File Ownership: Files created by the container’s
appuserwill appear as owned by the host user with UID 2001 (e.g., Alice). - Privilege Sharing: If the host user (e.g., Alice) has elevated privileges, this could inadvertently apply to container processes.
Best Practices for Running as a Non-Root User
- Define Non-Root Users in Dockerfiles: Always create and use a specific non-root user for containerized applications.
- Choose UIDs Carefully: Avoid conflicts between container UIDs and host system UIDs to prevent unintentional privilege sharing.
- Explicitly Specify
USERfor All Containers: Ensure all containers switch to a non-root user explicitly. This prevents the default root user behavior. - Verify Application Compatibility: Confirm your application can run with limited permissions. Some older applications might assume root-level access and require adjustments.
Why Running as a Non-Root User is Critical
Using a non-root user in your Docker containers aligns with security best practices. It isolates container processes. It also enforces the principle of least privilege. With a properly defined user and thoughtful UID management, you can mitigate many common container security risks.
Choose your Ports wisely
When running containerized applications, configuring port mappings carefully is critical for both security and operational reliability. Mismanaged ports can expose your system to risks. Here’s how to manage them effectively.
Restrict Host-to-Container Port Mappings
Only expose the ports your application needs to function, and avoid exposing debugging or development ports in production. Use the -p flag to bind container ports to specific host ports explicitly.
docker run -p 8080:80 nginx-alpine
- Maps port 80 inside the container to port 8080 on the host.
- Exposes the application externally only through port 8080.
For sensitive applications, bind ports to specific network interfaces to limit access.
docker run -p 127.0.0.1:8080:80 nginx-alpine
This configuration ensures the application is accessible only from the host machine.
Avoid Privileged Ports
Privileged ports (0–1024) require root permissions to bind. These ports are commonly used for standard services like HTTP (port 80) or HTTPS (port 443). To reduce risks:
- Use unprivileged ports (e.g., 8080 for HTTP, 8443 for HTTPS) whenever possible.
- Choose container images designed for non-root operation, such as
nginx-unprivileged.
docker run -p 8080:8080 nginx-unprivileged
This image allows NGINX to run on unprivileged ports without root permissions inside the container.
Document Port Usage
Clearly document the ports your application uses to enhance maintainability and reduce confusion for other developers. Use the EXPOSE instruction in your Dockerfile to indicate which ports the container listens on:
# Expose container ports
EXPOSE 8080
This doesn’t publish ports to the host but serves as a reference for developers and tools.
Best Practices for Port Management
- Expose Only Necessary Ports: Avoid exposing unnecessary or debugging ports, especially in production.
- Avoid Privileged Ports: Use ports above 1024 to minimize security risks and avoid running as root.
- Use Specialized Images: Leverage images like
nginx-unprivilegedto bypass the need for privileged ports without compromising functionality. - Limit Access: Bind ports to specific network interfaces or internal networks to control accessibility.
- Document Consistently: Keep a clear record of port mappings for easy reference.
By carefully managing your container’s ports, you can reduce its attack surface, enhance security, and ensure better operational control.
Minimize Layers and Dependencies
A clean, efficient Dockerfile is key to building secure and optimized container images. Reducing unnecessary layers and dependencies not only decreases image size but also enhances security, performance, and maintainability.
Why Minimize Layers and Dependencies?
- Smaller Image Size: Streamlined images are faster to pull, deploy, and start.
- Enhanced Security: Fewer tools and libraries mean fewer potential vulnerabilities.
- Improved Build Times: Cleaner Dockerfiles lead to faster builds and simpler maintenance.
Leverage Multi-Stage Builds
Multi-stage builds allow you to separate build-time dependencies from runtime requirements. This ensures the final image contains only what the application needs to run. This reduces image size and improves security by discarding unnecessary build tools and intermediate artifacts.
Key Benefits of Multi-Stage Builds:
- Security: Keeps the runtime image lean by excluding the build environment.
- Efficiency: Minimizes storage and bandwidth usage with lightweight images.
- Simplicity: Eases maintenance with a purpose-built runtime image.
Example: Multi-Stage Build for a Go Application
# Stage 1: Build Stage
FROM golang:1.20 AS build
WORKDIR /app
# Copy and compile the application
COPY . .
RUN go build -o myapp
# Stage 2: Runtime Stage
FROM alpine:3.20
WORKDIR /usr/local/bin
# Copy the compiled binary from the build stage
COPY --from=build /app/myapp .
# Define the container’s entry point
ENTRYPOINT ["myapp"]
Visualization of multi-stage Builds

Best Practices for Minimizing Layers and Dependencies
- Use Multi-Stage Builds: Separate build and runtime environments for smaller, cleaner images.
- Choose Minimal Base Images: Start with lightweight options like
alpineto reduce unnecessary dependencies. - Consolidate Commands: Combine commands to reduce intermediate layers and avoid leaving residual files.
- Remove Build Tools: Exclude temporary files and build tools from the final image to maintain minimalism.
By adopting these practices, you can create container images that are smaller, faster, and more secure. This also simplifies deployment and maintenance.
Set File Permissions
Proper file permissions inside a container are critical for enhancing security and ensuring only authorized processes can access sensitive files. Use these guidelines:
- Assign Ownership to Non-Root Users: Ensure application directories and files are owned by a non-root user.
- Restrict Permissions: Grant only the required access permissions to limit potential exploitation.
Example: Setting File Permissions in a Dockerfile
# Create an application directory
RUN mkdir /app && \
chown -R appuser:appuser /app && \
chmod 750 /app
# Switch to the non-root user
USER appuser
In this example:
chown -R appuser:appuser /app: Assigns ownership of/appto the non-root userappuser.chmod 750 /app: Ensures only the owner has full access (read, write, execute), while others have limited or no access.
By restricting file permissions, you reduce the risk of unauthorized access and enhance container security.
Regularly Update Base Images
Keeping base images up to date is essential for securing containerized applications. Outdated images may contain vulnerabilities that attackers can exploit. Follow these best practices:
- Use Official and Trusted Images: Always start with verified base images. Get them from trusted sources like Docker Hub. Alternatively, use your organization’s internal registry.
- Automate Updates: Use tools like Dependabot, Renovate, or CI/CD pipelines to monitor and apply updates to your images and dependencies.
- Pin Versions for Reproducibility: Specify image versions explicitly to ensure consistent builds while avoiding unintentional updates.
Example: Updating Base Images
# Use a specific version of an official base image
FROM nginx:1.25.2-alpine
# Regularly check for and apply updated base images
Pro Tip: Automated Image Scanning
Integrate tools like Trivy, Grype, or Docker Scout into your build pipeline. Use them to scan images for vulnerabilities. You will receive alerts about outdated dependencies.
Regular updates ensure your containers remain secure and performant, reducing the risk of running outdated or vulnerable software.
Running containers with the least privileges
Ensuring that your containers operate with the minimal permissions required for their functionality is a cornerstone of container security. Adhering to the principle of least privilege reduces the risk of malicious exploitation. It also improves the overall security posture of your applications.
Why Least Privilege Matters
Containers running with excessive privileges can expose the host system and other containers to unnecessary risk. Vulnerabilities in your application or runtime environment may be exploited to:
- Escalate privileges within the container.
- Compromise sensitive host resources or data.
- Gain control over other containers or services running on the host.
Best Practices for Running Containers with the Least Privileges
Avoid Running as Root By default, many container images run processes as the root user. However, this should be avoided unless absolutely necessary.
- Use a non-root user with only the permissions required for the application.
- Explicitly specify the user in your
Dockerfileor when starting the container.docker container run -it --user 5000:5000 alpine /bin/ash
Restrict Capabilities Linux capabilities allow granular control over what operations a container can perform. By default, Docker containers run with a reduced set of capabilities, but even these can be further restricted.
docker run --read-only --tmpfs /tmp myapp
--read-only: Mounts the entire container filesystem as read-only.--tmpfs /tmp: Allows temporary write access to/tmp.
Limit Networking Privileges Containers often don’t need access to all network interfaces or capabilities.
- Use network namespaces or bind containers to specific interfaces.
- Consider disabling networking entirely for isolated workloads.
docker run --network=none myapp
Control Volume Mount Permissions Avoid mounting sensitive host directories into containers. When volumes are necessary:
- Use the
:rooption to mount volumes as read-only. - Restrict access to specific files or directories.
docker run -v /data:/app/data:ro myapp
Audit and Minimize Privileges Periodically review and minimize permissions granted to users, processes, and containers. Use tools like AppArmor, SELinux, or seccomp to enforce runtime restrictions.
docker run --security-opt seccomp=/path/to/profile.json myapp
Summary of Best Practices
- Always use non-root users.
- Drop unnecessary Linux capabilities.
- Configure containers with read-only filesystems.
- Restrict network and volume access.
- Use security frameworks like AppArmor, SELinux, and seccomp for enhanced isolation.
By running containers with the least privileges, you significantly improve their security while maintaining functionality. This proactive approach reduces the potential impact of vulnerabilities and ensures better isolation for your workloads.
Securing image sources and registries
Container images are the foundation of any Dockerized application. Ensuring that images come from trusted sources and managing private registries securely is critical to maintaining a secure containerized environment. Neglecting this can expose your infrastructure to malicious code, outdated dependencies, and unverified software.
Why Image Security Matters
Containers are only as secure as the images they are built upon. If an image contains vulnerabilities or has been tampered with, every container running that image is at risk. By securing your image sources and registries, you can:
- Prevent unauthorized access to sensitive images.
- Ensure images are free from malware or other security issues.
- Maintain compliance with organizational or regulatory standards.
Best Practices for Securing Image Sources
Use Trusted Sources
- Always pull images from reputable public registries. These includes for example Docker Hub, Red Hat Quay, or Google Artifact Registry. Use registries you trust.
- Prefer official images or verified publisher images when using public registries.
docker pull nginx:stable-alpine
Official images are maintained by trusted sources and regularly updated to address vulnerabilities.
Verify Image Integrity
- Use image digests (
sha256) to ensure you’re running the exact version of an image. - Avoid relying solely on mutable tags (e.g.,
latest), which can change over time and lead to unintended updates.
docker pull nginx@sha256:abc1234...
Scan Images for Vulnerabilities Regularly scan images using tools like:
- Docker Scout or Trivy for vulnerability assessments.
- Automated CI/CD pipeline integrations to detect issues before deployment.
trivy image nginx:stable-alpine
Avoid Using Untrusted or Deprecated Images
- Remove outdated or unsupported images from your registry to avoid accidental use.
- Avoid using images from unknown maintainers or third-party sources unless verified.
Managing Private Registries Securely
If you’re hosting a private container registry (e.g., Docker Registry, AWS Elastic Container Registry, Azure Container Registry), it’s essential to secure it properly.
Require Authentication and Authorization
- Enforce access controls to ensure only authorized users or systems can push or pull images.
- Use role-based access control (RBAC) to assign granular permissions.
- Using Azure Container Registry with RBAC
- Assign roles such as
AcrPullorAcrPushto users or service accounts, limiting access based on their needs.
- Assign roles such as
Enable Transport Security
- Use HTTPS to encrypt traffic between clients and the registry.
- Ensure SSL/TLS certificates are correctly configured and updated.
docker run -d \
--restart=always \
--name registry \
-v "$(pwd)"/certs:/certs \
-e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
-p 443:443 \
registry:2
Regularly Scan and Harden the Registry
- Perform vulnerability scans on hosted images.
- Use tools like Clair or Harbor for comprehensive registry security.
Implement Image Signing and Verification
- Use Docker Content Trust (DCT) or other signing tools like Notary to ensure image integrity.
- Verify signatures when pulling images to prevent tampered or unauthorized images.
export DOCKER_CONTENT_TRUST=1
docker pull nginx:stable-alpine
Monitor and Audit Registry Activity
- Enable logging to monitor who accesses the registry and what actions are performed.
- Use security information and event management (SIEM) tools for continuous analysis.
Summary of Best Practices
- Always use images from trusted sources and verify their integrity.
- Regularly scan images for vulnerabilities and remove deprecated ones.
- Secure private registries with authentication, encryption, and access controls.
- Sign images and verify signatures to ensure integrity.
- Monitor registry activity and respond to unauthorized actions.
Securing your image sources and registries is essential. This action creates a strong first line of defense for your containerized applications. It minimizes risks and ensures operational reliability.
Leveraging Docker security tools and features
Container security is a critical aspect of managing Dockerized applications. Docker offers several built-in features. It integrates with external tools to help identify vulnerabilities. These integrations enforce best practices and enhance overall security. Properly leveraging these tools ensures a robust and secure container environment.
External Tools for Enhancing Container Security
- Vulnerability Scanning
Regularly scan images for known vulnerabilities using tools like: - Runtime Security
Monitor container behavior at runtime using tools like:- Falco: Detects unexpected behavior in containers, such as privilege escalation or sensitive file access.
- Sysdig Secure: Provides runtime security, compliance checks, and visibility into containerized environments.
- Container Hardening
- Use tools like OpenSCAP or Lynis to audit container images. These tools check the images against security benchmarks. An example is the Center for Internet Security (CIS) Docker Benchmark.
- Policy Enforcement
Implement policies to control which images and configurations are allowed in your environment:- OPA Gatekeeper: Enforces Open Policy Agent (OPA) policies for Kubernetes clusters.
- Docker Bench for Security: Audits your Docker configuration for best practices.
Automating Security in the CI/CD Pipeline
Incorporating security checks into your CI/CD pipeline ensures vulnerabilities are identified and addressed early in the development lifecycle.
Steps for Securing CI/CD Workflows:
- Automated Patching: Leverage tools like Dependabot or Renovate to keep dependencies up to date.
- Image Scanning: Integrate tools like Trivy or Snyk into your build pipeline to scan images before they are deployed.
- Policy Validation: Use policy enforcement tools to check for compliance with organizational standards.
Summary of Best Practices for Leveraging Security Tools
- Enable and configure built-in Docker security features, such as Content Trust, seccomp profiles, and user namespaces.
- Use external tools like Trivy, Falco, and Snyk for vulnerability scanning and runtime security.
- Automate security checks in CI/CD pipelines to identify vulnerabilities early.
- Regularly audit container images and Docker configurations against security benchmarks.
By combining Docker’s built-in capabilities with external tools, you can build a multi-layered security strategy. This strategy protects your containerized applications from vulnerabilities. It also shields them from misconfigurations.
Conclusion
We are approaching the conclusion of Part 3 in the Where to Start with Docker series. It becomes evident that containerization provides unparalleled efficiency. It also ensures portability for modern applications. However, with great power comes the responsibility to ensure security at every layer.
Selecting minimal base images is crucial. Managing secrets effectively is also important. Configuring containers with the least privileges is another key step. Each best practice we discussed contributes to a robust and secure container environment. You can leverage Docker’s built-in features to further fortify your applications. Combine these with external tools like Trivy and Falco to guard against vulnerabilities and misconfigurations.
Docker’s flexibility empowers developers and operators alike, but true mastery lies in balancing innovation with security. By incorporating these practices into your workflow, you protect your applications. You also pave the way for scalable, resilient, and compliant deployments.
The journey doesn’t end here—container security is a continuous process that evolves alongside the ever-changing landscape of technology and threats. Stay curious, stay vigilant, and always strive to build better containers.
What’s next? If you found this guide helpful, revisit Part 1 and Part 2. These sections complete the picture of Docker fundamentals and image optimization. Together, these chapters form a strong foundation for mastering containerization.
Have questions or experiences to share? Let’s continue the conversation—your feedback and insights are always welcome!

You must be logged in to post a comment.