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:
- Builder stage – Compiles application and installs dependencies
- 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!