Containerization has revolutionized software development and deployment by packaging applications and their dependencies into isolated units called containers. This approach ensures consistency across different environments, from development to production, and significantly streamlines the Continuous Integration/Continuous Delivery (CI/CD) pipeline. This document outlines best practices for containerizing applications, focusing on building efficient and secure images, managing them in private registries, deploying them with Helm charts to Kubernetes, and integrating robust DevSecOps practices with Aqua Security.
Multi-stage builds are a powerful Docker feature that allows you to use multiple FROM
statements in your Dockerfile. Each FROM
instruction can use a different base image, and you can selectively copy artifacts from one stage to another, discarding everything else. This significantly reduces the final image size and attack surface.
# Stage 1: Build the application
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production # Install production dependencies first for caching
COPY . .
RUN npm run build # Build your application (e.g., React, Angular, Vue)
# Stage 2: Create the final lightweight runtime image
FROM node:18-alpine
WORKDIR /app
# Copy only the necessary files from the builder stage
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist # Or wherever your build output is
COPY --from=builder /app/package.json ./package.json # Only if needed for runtime scripts
# If it's a web server, expose the port
EXPOSE 3000
CMD ["node", "dist/index.js"] # Adjust to your application's entry point
Always start with the smallest possible base image that meets your application's needs. Alpine Linux-based images (e.g., alpine
, node:18-alpine
, openjdk:17-alpine
) are generally much smaller and have fewer vulnerabilities than their full OS counterparts.
COPY . .
) after less frequently changing ones (e.g., COPY package.json
, RUN npm ci
).By default, containers run as the root
user. This is a security risk. Always create and switch to a non-root user within your Dockerfile.
# ... (previous stages) ...
FROM node:18-alpine
# Create a non-root user
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
WORKDIR /app
# ... (copy files) ...
EXPOSE 3000
CMD ["node", "dist/index.js"]
ENV
instructions for non-sensitive configuration that changes between environments (e.g., ENV NODE_ENV=production
).Define CPU and memory limits for your containers in your Kubernetes deployment manifests. This prevents a single container from consuming all available cluster resources and improves stability.
Azure Container Registry (ACR) is a managed, private Docker registry service in Azure. It's used to store and manage your private Docker container images and related artifacts (like Helm charts).
company.azurecr.io/project/app
or company.azurecr.io/lob/app
). This simplifies management and access control while maintaining separation between projects.# ... (after build and scan steps) ...
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }} # Azure service principal credentials
- name: Docker Login to ACR
run: |
az acr login --name
- name: Tag and Push Docker Image to ACR
run: |
docker tag my-app:latest .azurecr.io/my-app:$(git rev-parse --short HEAD)
docker push .azurecr.io/my-app:$(git rev-parse --short HEAD)
Note: AZURE_CREDENTIALS
should be a GitHub Secret containing a JSON object for an Azure Service Principal with AcrPush
role on your ACR.
Helm is the package manager for Kubernetes. It allows you to define, install, and upgrade even the most complex Kubernetes applications.
Chart.yaml
, values.yaml
, templates/
, charts/
).values.yaml
.values.yaml
files or use --set
flags during deployment.values.yaml
(use Kubernetes Secrets).Chart.yaml
and helm dependency update
.helm upgrade --install
for idempotent deployments.helm rollback
) for quick recovery.helm lint
to check for common issues and helm test
for chart-specific tests.my-app-chart/
├── Chart.yaml # Information about the chart
├── values.yaml # Default values for the chart
├── templates/
│ ├── deployment.yaml # Kubernetes Deployment manifest
│ ├── service.yaml # Kubernetes Service manifest
│ ├── ingress.yaml # Kubernetes Ingress manifest (optional)
│ ├── _helpers.tpl # Helper templates
│ └── NOTES.txt # Instructions for users
└── charts/ # Subcharts (dependencies)
Integrating security throughout the container lifecycle is critical. Aqua Security provides comprehensive solutions for container security, including vulnerability scanning, compliance checks, and runtime protection.
An SBOM is a formal, machine-readable inventory of software components and dependencies. It's crucial for understanding your application's supply chain risks. Aqua Trivy can generate SBOMs in various formats (e.g., SPDX, CycloneDX).
# .github/workflows/ci-cd.yml
name: Container Build, Scan, Deploy
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-scan-sbom:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t my-app:latest .
- name: Run Aqua Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'my-app:latest'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH' # Fail on critical/high vulnerabilities
- name: Generate SBOM with Aqua Trivy
run: |
trivy image --format spdx-json --output sbom.spdx.json my-app:latest
# Upload SBOM as a workflow artifact for later use/storage
# This SBOM can be consumed by other security tools or for compliance
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom-my-app
path: sbom.spdx.json
# ... (Subsequent steps for pushing to ACR and deploying with Helm) ...
mvn compile jib:dockerBuild
gradle jibDockerBuild
openjdk:17-jre-slim-buster
or openjdk:17-jdk-slim-bullseye
for build, then openjdk:17-jre-slim-bullseye
for runtime).-Xms
, -Xmx
, garbage collector settings) within the container to optimize memory usage and performance based on allocated container resources.java -jar your-app.jar
as the ENTRYPOINT
or CMD
.node_modules
if required for production) in the final layer to optimize image size and security..dockerignore
file to exclude unnecessary files (e.g., node_modules
from the host, .git
, dist
if built in a separate stage, npm-debug.log
).npm ci
in CI/CD and Docker builds for clean, reproducible installations based on package-lock.json
.npm ci --only=production
in the final image stage to install only production dependencies.NODE_ENV=production
in the production image.Containerization is a powerful enabler for DevOps, providing consistency, portability, and efficiency. By adopting best practices such as multi-stage builds, minimal base images, and robust security scanning with tools like Aqua Security, teams can build secure and optimized container images. Leveraging private registries like Azure Container Registry for image management and Helm charts for Kubernetes deployments ensures a streamlined and reliable path from code to production. These practices collectively contribute to a mature, secure, and agile software delivery pipeline.