Cloud Native Development with Containers

Containers have become the standard unit of deployment for modern applications. Whether you’re running a personal project or deploying to a Kubernetes cluster, understanding how to build, configure, and secure containers is a foundational skill. This post covers the practical essentials — not the theory, but the patterns you’ll actually use.

Why containers

The core value proposition is environment consistency. A container packages your application with its exact dependencies, runtime, and configuration. The same container that runs on your laptop runs identically in staging and production. No more “works on my machine” — the machine is defined in a Dockerfile and travels with the code.

Containers are not virtual machines. A VM virtualizes hardware and runs a full OS. A container shares the host kernel and isolates only the application and its dependencies. This makes containers faster to start, smaller in size, and more efficient with resources.

Building good Dockerfiles

A well-structured Dockerfile follows a few key principles: install dependencies before copying code (for layer caching), run as a non-root user, use slim base images, and include health checks.

FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER appuser
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0"]

The order of COPY instructions matters. Dependencies change less frequently than application code, so copying and installing requirements.txt first means Docker can cache that layer and skip reinstallation when only your code changes.

Multi-stage builds for production

Multi-stage builds use separate stages for building and running, producing significantly smaller production images:

FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER appuser
CMD ["node", "dist/server.js"]

The build stage has compilers, dev dependencies, and source code. The production stage has only the runtime and compiled output. Typical reduction: 1GB+ build image down to ~200MB.

Always set resource limits (CPU and memory) on production containers. Without limits, a single misbehaving container can consume all host resources and take down neighboring services.

Development vs production

Use separate Docker Compose files for development and production. Dev configs mount source code as volumes for hot-reload and expose debug ports. Production configs use built images, set resource limits, and configure health checks.

Container security starts with simple habits: run as non-root, use slim base images to reduce attack surface, scan images for vulnerabilities with tools like Trivy, and never store secrets in images.


Want to dig deeper? Explore the project repository for ready-to-use Dockerfiles, Docker Compose configurations for dev and prod, and a container best practices guide.

Cloud Native Development with Containers
Cloud Native Development with Containers