in

6 Best Practices for using Containers

For many organizations, containers have become an indispensable part of deploying and scaling applications. According to Red Hat’s 2021 State of Enterprise Open Source report, 56% of enterprises now run containerized workloads in production, up from 50% in 2020. [1] With the popularity of tools like Docker, Kubernetes, and OpenShift, containers are revolutionizing how applications are built, shipped, and run across diverse environments.

However, simply containerizing an app doesn’t automatically make it portable or efficient. To fully realize the benefits of containers – including rapid scaling, resilience, and security – certain best practices must be followed. In this comprehensive guide as a DevOps practitioner and container expert, I’ll share key recommendations for effectively using containers based on industry research and hands-on experience.

Start with Small, Trusted Base Images

One of the biggest advantages of containers is minimal overhead. But bulky base images can sabotage that, bloating your container images with hundreds of unnecessary megabytes.

For example, a basic “Hello World” Node.js app built on the standard node:latest image weighs in around 690MB. Yet the actual Node.js app may only be a few kilobytes!

Base Image Size
node:latest 690MB
node:alpine 68MB

By starting with a minimal base like Alpine Linux, you avoid all the extra utilities and packages in heavy distributions like Ubuntu. At under 5MB, Alpine provides a compact Linux environment tailored for containers.

Not only are smaller images faster to deploy, but they also offer a smaller attack surface for vulnerabilities. According to Aqua Security, “Large container images provide extra real estate for vulnerabilities to hide.” [2]

Also, beware of the default latest tag which points to the newest base image. These rolling releases can introduce bugs or breaking changes. Always pin your base images to specific, immutable versions for production reliability.

Build Efficient Images with Multi-Stage Dockerfiles

For compiled languages like Go, Rust, and Java, multi-stage Docker builds optimize containers by splitting buildtime and runtime requirements:

  1. Builder stage – Compiles application and installs dependencies
  2. Runtime stage – Contains just the compiled application

This approach prevents bloated containers by shipping only the compiled artifact, not the compilers, toolchain, and source code needed to build it.

Here is an example for a simple Go web server:

# Builder
FROM golang:1.16 AS build
WORKDIR /app
COPY . .  
RUN go build -o server

# Runtime 
FROM alpine  
WORKDIR /app
COPY --from=build /app/server .
CMD ["./server"]  

The end result is a production container running just the Go server binary in a lightweight Alpine environment. This container is focused on app execution rather than development.

For a compiled Node.js application, the difference can be even more stark:

Image Size
node:latest 690MB
node:alpine (multi-stage) 55MB

By keeping containers lean, you minimize network bandwidth, storage needs, and attack surfaces.

Use Semantic Version Tagging

Tagging container images is vital for managing deployments and rollbacks. Names should clearly identify the application version and stability:

  • SemVer tags – app:1.2.3
  • Git SHA tags – app:83748bc
  • Latest tags – Use only for development

Unique semantic tags let you safely replicate and scale applications across nodes:

docker service scale app=5 --image app:1.2.3

All 5 tasks will start identical containers running app version 1.2.3.

Tags should also distinguish between:

  • Stable images – base images receiving frequent updates (nginx:stable)
  • Fixed images – app images unchanged in production (app:1.2.3)

This separation prevents production apps from suddenly updating to new versions. Stable base images take patches transparently.

For extra protection, make deployed image tags immutable with --immutable to prevent accidental overwriting:

docker tag app:1.2.3 app:production --immutable

Harden Containers and Cluster Security

While containers provide isolation, additional controls should be implemented:

  • Drop root privileges – Avoid root and use Docker‘s USER directive to run as a standard user.
  • Read-only filesystems – Make container filesystems read-only to prevent tampering.
  • Signed images – Only use images from trusted sources and check signatures.
  • AppArmor / seccomp – Limit container permissions via security profiles.
  • Scanning – Continuously scan images for OS and app vulnerabilities.
  • TLS – Secure Docker daemon and registry communication with TLS.
  • RBAC – Implement role-based access control for personnel.
  • Network policies – Restrict traffic between namespaces.
  • CI/CD pipelines – Automate security practices into build and deployment workflows.

Standards like CIS Docker Benchmarks provide prescriptive container hardening guidance based on consensus recommendations. Implementing controls in layers is key to holistic container security.

One Application Per Container

Unlike virtual machines, containers are optimized to run a single application or process. For example, a LAMP stack would be decomposed into:

  • Container 1 – MySQL
  • Container 2 – Apache
  • Container 3 – PHP
Approach Pros Cons
Multiple apps per container Simplified architecture Tight coupling, harder scaling
One app per container Loose coupling, portable More moving parts

Running multiple processes in a single container leads to interdependencies. If the web server crashes, it could take down the whole container including the database. Individual containers provide targeted scaling, updates, and resilience for each service.

While technically possible to containerize multiple apps together, this strays from container best practices according to Docker:

"Containers should be as granular as possible. Generally, one container should represent one microservice." [3]

Externalize State for Portability

Persisting data within containers hinders portability and scaling. Containers are intended to be ephemeral, so application state should live externally:

  • Object storage (S3) for files
  • Databases like MySQL for structured data
  • Caching systems like Redis for sessions
  • Block storage for attached volume mounts

This statelessness allows freely moving and duplicating containers without data loss. Stateful containers turn into "pet" servers requiring maintenance.

Some cases like databases do warrant stateful containers. But data should still be externalized where possible:

  • Database data on mounted volumes
  • Configs and logs on object storage

Properly orchestrated stateful apps can reap many cloud native benefits like scaling and auto-healing. Just be mindful of storage, backups, and data gravity.

Conclusion

Mastering just a few container best practices goes a long way in managing complexity. Keep images petite but complete. Standardize tagging and access control. Externalize state for mobility. Monitor actively for risks.

Of course, seamlessly integrating containers into CI/CD pipelines, infrastructure, and workflows is equally important. But by following core principles like loose coupling and immutable infrastructure, organizations can tame even the most tangled containerized architectures.

Hopefully this guide has provided a helpful overview of container best practices from an industry perspective. Please feel free to reach out with any additional questions!

AlexisKestler

Written by Alexis Kestler

A female web designer and programmer - Now is a 36-year IT professional with over 15 years of experience living in NorCal. I enjoy keeping my feet wet in the world of technology through reading, working, and researching topics that pique my interest.