Docker for Continuous Delivery: The Basics

DevOpsDevOps Tools

YOUR COSMETIC CARE STARTS HERE

Find the Best Cosmetic Hospitals

Trusted • Curated • Easy

Looking for the right place for a cosmetic procedure? Explore top cosmetic hospitals in one place and choose with confidence.

“Small steps lead to big changes — today is a perfect day to begin.”

Explore Cosmetic Hospitals Compare hospitals, services & options quickly.

✓ Shortlist providers • ✓ Review options • ✓ Take the next step with confidence

This article is based on a post originally published by the engineering team at Cars.com (tech.cars.com), describing how they adopted Docker to manage Node.js build environments in their CI/CD pipelines.

Why Docker in a CI/CD Pipeline?

The Cars.com engineering team reached for Docker out of practical necessity. Their Jenkins build agents had only one version of Node.js installed, but their repositories required different versions—some depended on Node 4, others on Node 6. The alternatives were unappealing: install multiple Node versions and juggle them with a version manager like n, or tag Jenkins agents so that specific jobs ran on specific agents. Neither approach scales well.

Instead, the team used Docker to pin the Node.js version at the project level, inside the container definition. This eliminated the version-conflict problem entirely and, as a side effect, unlocked a set of broader benefits that make Docker a natural fit for continuous delivery.

What Is Docker and Why Should You Use It?

Docker is a containerisation platform that provides tooling to build and run containers simply and consistently. To understand why it matters, it helps to understand what a container actually is.

Containers are a lightweight form of virtualisation. Unlike a virtual machine, a container shares the host kernel and does not carry the overhead of a full guest operating system. The typical pattern is to run a single process per container and bundle that container’s dependencies—binaries, runtimes, configuration—inside the container image itself. The result is an isolated, self-contained application that travels with everything it needs to run.

Bridging the Gap Between Dev and Ops

One of the most valuable properties of Docker for continuous delivery is environment consistency. When every lifecycle task—linting, testing, building, packaging—runs inside a container, developers and build servers execute against exactly the same dependencies in the same operating system. This narrows the classic “works on my machine” failure surface significantly and makes it easier for a developer to reproduce a build failure locally before pushing a fix.

It also shifts dependency ownership in a healthy direction. Developers define the runtime, language version, and OS their application needs by writing a Dockerfile and committing it to source control. Operations teams no longer have to provision every tool a new project requires, and new languages or frameworks can be adopted without waiting for infrastructure changes. The constraint that technology choices must be limited to what the ops team is comfortable deploying is effectively removed.

Isolation Benefits for Testing and Deployment

Containers are isolated from the host and from each other. This matters in several practical ways:

  • Multiple dependency versions on a single host — Two applications needing different versions of the same library can run side by side without conflict.
  • Port management — Even if multiple containers want to bind to the same internal port, Docker’s port-mapping layer resolves conflicts at the host level.
  • Integration tests — Tests that spin up mock servers or companion services on fixed ports no longer require coordination with other pipelines running on the same agent.
  • Security posture — Process isolation limits the blast radius if one application misbehaves on a shared host.

Docker Basics: Running Your First Container

The fastest way to understand Docker is to run a container and observe its isolation properties directly.

docker container run -it ubuntu bash

This command runs an Ubuntu container in interactive mode (-it), which attaches your terminal to the container’s stdin/stdout, and starts a bash shell. You will land at a shell prompt inside the container.

Note: Older Docker documentation uses shorthand commands like docker run and docker images. These still work, but Docker 1.13 introduced a restructured CLI with explicit object types: docker container run, docker image ls, and so on. The new form is preferred.

Now try running a curl command from inside the container:

curl wttr.in/Chicago

You will likely see: bash: curl: command not found. Even if curl is installed on your host machine, the container cannot see it. The container has its own isolated filesystem. To use curl inside the container, you must install it there:

apt-get update -y && apt-get install -y curl
curl wttr.in/ChicagoCode language: JavaScript (javascript)

The command now works. This hands-on example makes the isolation model concrete: the container’s environment is fully self-contained.

Images vs Containers: An Important Distinction

Before going further, it is worth being precise about terminology. A useful analogy: a Docker image is like a class definition, and a container is like an instance of that class.

More precisely:

  • A Docker image is a read-only, layered filesystem snapshot. Images are typically built from a series of instructions, with each instruction adding a new read-only layer on top of the previous one.
  • A Docker container is a running instance created from an image. When a container starts, Docker adds a thin, writable layer on top of the read-only image layers. Changes made inside the container (installing packages, creating files) go into this writable layer.

This layered, copy-on-write filesystem is efficient because multiple containers can share the same underlying image layers, only materialising differences in their own writable layers.

Committing a Container to an Image (and Why You Shouldn’t)

Docker does allow you to snapshot the current state of a running container into a new image using docker container commit. Open a second terminal while your Ubuntu container is still running, find its ID, and run:

docker container ls
docker container commit <container-id> yourusername/curl
docker image lsCode language: HTML, XML (xml)

You can now start a new container from this image, and curl will be present:

docker container run -it yourusername/curl bash
curl wttr.in/Chicago

This works, but manual commits are not a good practice. They are not reproducible, they leave no audit trail, and they cannot be code-reviewed or stored in version control. Think of this as a learning exercise only. The right approach is Dockerfiles.

Dockerfiles: Configuration as Code

A Dockerfile is a plain-text script that declaratively describes how to build a Docker image. It gives you reproducibility, auditability, and the ability to store your build environment definition alongside your application code in version control—all core tenets of continuous delivery.

Here is a Dockerfile equivalent to what we built manually above:

FROM ubuntu

RUN apt-get update -y && apt-get install -y curl

CMD ["curl", "wttr.in/Chicago"]Code language: CSS (css)

Breaking down each directive:

  • FROM ubuntu — Declares the base image. Every Dockerfile starts here. This pulls the official Ubuntu image from Docker Hub if it is not already cached locally.
  • RUN apt-get update -y && apt-get install -y curl — Executes a shell command during the image build. Chaining the update and install into a single RUN instruction is intentional: each RUN creates one new image layer. Using two separate RUN statements would create two layers unnecessarily.
  • CMD ["curl", "wttr.in/Chicago"] — Sets the default command run when a container starts without an explicit command override. The array (exec) form is preferred over a plain string because it avoids shell interpretation differences across operating systems.

Building and Running the Image

Save the Dockerfile in a directory and build the image from that directory:

docker build -t yourusername/curl .

Key points about this command:

  • -t yourusername/curl — Tags the resulting image with a name. Prefixing with your Docker Hub username is best practice when you intend to publish the image to the public registry. For a private registry, substitute the registry’s DNS hostname for the username prefix, and Docker will push to that registry instead.
  • . (the build context) — Tells Docker to use the current directory as the build context. Docker will look for a file named Dockerfile here by default. You can override the filename or path with the -f flag. The build context also determines what host files are accessible inside the build process.

Once the build completes, run the container:

docker run -i yourusername/curl

The container will start, execute the curl command defined in CMD, print its output, and return you to your host shell. No command argument is needed because the default is baked into the image.

Key Takeaways

  • Docker containers package an application together with its dependencies, giving you consistent, reproducible builds regardless of the host environment.
  • Running builds inside containers bridges the gap between developer workstations and CI servers, reducing “works on my machine” problems.
  • Container isolation allows multiple projects with conflicting dependency versions to coexist on the same build agent without interference.
  • Dockerfiles replace manual container configuration with version-controlled, reviewable build instructions—a prerequisite for reliable continuous delivery.
  • Each Dockerfile instruction (except FROM) adds a layer; combine related commands into a single RUN to minimise unnecessary layers.
  • Use the exec array form for CMD to avoid shell portability issues.
  • Prefix image names with your Docker Hub username (or private registry hostname) to avoid naming collisions in shared registries.

What’s Next

This introduction covers the foundational Docker concepts you need before integrating containers into a CI/CD pipeline. A follow-up post from the Cars.com team goes deeper into how Docker is used in production pipelines, including common Dockerfile patterns, multi-stage builds, and design decisions that emerged from real-world usage. The core primitives covered here—images, containers, Dockerfiles, layering, and the build context—are the building blocks for all of those more advanced patterns.

0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x