You can run containers on a laptop with docker run. You cannot run a fleet of customer-facing containers that way. This final lesson surveys the practices and tools that take containers from your machine to production.
Image Security
Every line in your Dockerfile is part of your supply chain. Treat it that way.
- Pin base images.
FROM node:20.11-alpine3.19, notFROM node:latest. Better still, pin by digest. - Run as non-root.
RUN useradd -r -u 1000 -g app app USER app - Drop capabilities.
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp— the container loses every Linux capability except the one it actually needs. - Read-only root filesystem.
docker run --read-only --tmpfs /tmp myapp— even if the app is compromised, an attacker can't write a webshell. - Don't run privileged.
--privilegedhands the keys to the host. Avoid it. - Scan, scan, scan. Every CI build should run a vulnerability scan and fail on critical CVEs.
- Sign your images. Use cosign (part of Sigstore) so consumers can verify the image is yours and unmodified.
Building Images in CI
Move all production builds into CI — never push images built on a developer laptop to production. A typical GitHub Actions workflow:
name: docker
on:
push:
branches: [main]
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Equivalent pipelines exist for GitLab CI, Azure Pipelines, AWS CodeBuild, Google Cloud Build, and CircleCI.
Where Containers Actually Run
You rarely run containers on bare docker run in production. Instead:
| Service | Cloud | Model |
|---|---|---|
| ECS (Fargate) | AWS | Run containers without managing servers |
| EKS | AWS | Managed Kubernetes |
| App Runner | AWS | Container-to-URL with autoscaling |
| Cloud Run | Google Cloud | Serverless containers, scale to zero |
| GKE | Google Cloud | Managed Kubernetes |
| Azure Container Apps | Azure | Serverless containers with KEDA autoscaling |
| Azure Container Instances | Azure | Single containers on demand |
| AKS | Azure | Managed Kubernetes |
| Fly.io / Railway / Render | Independent | Container-first PaaS |
Observability
You can't manage what you can't see. In production, every container should:
- Log to stdout/stderr in structured JSON — log routers (Fluent Bit, Vector) ship them to Loki, CloudWatch, or Datadog.
- Expose
/healthzand/readyzendpoints so the orchestrator knows when to send traffic. - Emit metrics on a Prometheus-compatible endpoint or via OpenTelemetry.
- Propagate trace headers so distributed traces are stitched together across services.
The Path Forward
You now know enough to:
- Containerise any application you build
- Develop locally with Compose
- Ship images to a registry with a CI pipeline
- Deploy to a managed container service
From here, the major directions are:
- Kubernetes — when a single host or simple managed service stops being enough. See our Kubernetes Basics course.
- Service mesh (Istio, Linkerd) — for advanced traffic management, mTLS, and observability across hundreds of services.
- GitOps (Argo CD, Flux) — declarative continuous delivery for Kubernetes from Git.
- Supply chain security (SLSA, in-toto, SBOMs) — provenance and integrity for everything you ship.
Whatever you build next, the container is the universal unit. The skills you've practised here transfer directly.