Docker: Because “It Works on My Machine” Isn’t a Deploy Strategy

Docker

We’ve all heard it or said it. You spend hours building something, it runs beautifully on your laptop, and then the moment it hits your teammate’s machine or a production server, it implodes. Different OS, different Node version, different everything. Docker exists to make that excuse obsolete permanently.

1. Introduction

Before Docker, deploying an application meant praying that the production server had the same OS, the same runtime version, the same dependencies, and the same configuration as your local machine.

Docker solves this by packaging your application along with everything it needs to run into a single portable unit called a container. Whether that container runs on your laptop, your teammate’s Windows machine, or a Linux server in the cloud, it behaves exactly the same. No more environment mismatch, no more “works on my machine.”

2. What is Docker?

Docker is an open-source tool that provides us with a large library of images, along with CLI commands to create, manage, and monitor containers. For example, to run a web application that uses a certain version of OS, stack, or dependency, Docker helps us containerize that application with the required image, so that the application runs independent of the host system’s configuration but still uses its resources.

3. Core Concepts

Images and Containers

Before understanding Docker, you must know what images and containers are.

An image is a reusable, immutable schema of dependencies, application code, and configuration files required to run an application. It normally consists of a base OS and commands to pull the application, dependencies, build environment, copy code, etc. Having a base OS is not compulsory; a distroless image is a thing that only contains the necessary application parts.

A container is what we get when we run an image. After running an image, an instance is created and assigned namespaces and cgroups to isolate the container from the host internally. It needs temporary resources like storage and network, which get destroyed along with the container. A host can run multiple containers from multiple or the same image. The host is the base system the container runs on.

Image Container
Read-only schema file. Live read and write instance of the image.
Permanent file that can be shared and reused multiple times. Temporary instances that can be terminated/removed along with their contents.
Can be created using a Dockerfile, pulled from a registry, etc. Can be created using Docker or other applications utilizing containerd.
docker build <docker filename> / docker pull <publisher:image> docker run <image name> / docker start <container name>

Container vs VM

From the definition, both container and VM might sound the same “an isolated OS running on the host’s resources” but the underlying workings of both are far from similar.

A VM (Virtual Machine) also uses images of an entire OS, with all its system files, drivers, and environment. But it runs above the OS layer or application layer, depending on the type of VM. It is kernel-independent and can run any OS on any host.

A container, on the other hand, is mostly used to run an application, with an image containing just the instructions to run it. A container runs directly above the kernel layer, making it dependent on the host’s kernel. It is lightweight and faster than a VM because it runs closer to the kernel and only carries what it needs.

4. Docker Engine

The Docker Engine on Linux works in a layered architecture. It runs in the background on your host machine and is responsible for building images, running containers, and managing everything in between.

It operates as a client-server architecture with three main components:

  1. Docker CLI (Client): The command-line interface you interact with. When you run docker run, the CLI sends that instruction to the daemon via the Docker API.
  2. Docker API (REST API): The bridge between the CLI and the daemon. It’s also what allows other tools and applications (like Docker Desktop or CI/CD platforms) to talk to Docker programmatically.
  3. Docker Daemon (dockerd): It listens for Docker API requests and manages Docker objects, images, containers, networks, and volumes. When you type a Docker command, it’s the daemon doing the actual heavy lifting.
  4. Containerd (Runtime): Containerd manages the whole containerization process in a host. It manages container runtime, life cycle, and supervises the whole container process. It is a global containerization tool used by other applications too like Kubernetes, podman etc.
  5. Runc (OCI): Runc is the one actually running the container from an image. It calls the kernel of namespace and cgroups to start a process. It is also responsible for implementing OCI (Open Container Initiative) the standard defining container requirements.

In short: you talk to the CLI → CLI talks to the daemon via the API → daemon makes it happen.

5. Docker CLI

Docker CLI provides us with multiple commands to help with containerization. Here are some we use most frequently.

  1. docker run: Creates and starts a new container from an image. If the image isn’t found locally, Docker automatically pulls it from the registry first.

  1. docker run -d: Runs a container in detached mode (in the background), freeing up your terminal.

  1. docker run –name <name>: Assigns a custom, memorable name to your container instead of letting Docker generate a random one.

  1. docker ps: Lists all currently running containers. Add the -a flag (docker ps -a) to see all containers, including stopped ones.

  1. docker stop <container_id/name>: Gracefully stops a running container by sending a SIGTERM signal. You can use just the first few unique characters of a container ID instead of typing the whole thing.

  1. docker rm <container_id/name>: Removes/deletes a stopped container from your host.

  1. docker images: Lists all the container images currently downloaded and available on your host machine.

  1. docker run <image>:<tag>: Runs a container using a specific version (tag) of an image (e.g., docker run ubuntu:20.04). If no tag is provided, it defaults to :latest.

  1. docker rmi <image_id/name>: Removes/deletes one or more images from your host machine (provided no containers are currently using them).

  1. docker inspect <object>: Returns low-level, detailed JSON configuration data about a specific Docker object (container, image, volume, or network).

  1. docker logs <container_id/name>: Fetches and displays the log output (stdout/stderr) from a specific container essential for debugging why an app isn’t working.

6. Configuring Containers

Environment Variables

Using the -e flag, we can pass variables inside the container at runtime without hardcoding them into your image. This is how you keep secrets like database passwords out of your codebase.

docker run -e DB_PASS=mysecretpassword myapp

Inside your application, you’d read it like:

import os

db_pass = os.environ.get(‘DB_PASS’)

All the environment variables set for a container or image are listed in the docker inspect JSON output under Env.

Ports (Port Mapping / Exposing)

A container is assigned a new set of ports, different from the host’s. An application running on a container’s port is only accessible by the host. To make it available globally, we must map the port the application is running on inside the container to a port on the host.

docker run -p 8080:5000 myapp

This maps host port 8080container port 5000. So visiting http://localhost:8080 reaches your app running inside the container on port 5000.

Docker Storage and Volumes (Persistence)

When a container runs, it is assigned a temporary storage space for the data generated or required inside it, such as data in a database server. By default, after destroying a container, its contents also get destroyed. Volume mapping prevents this by storing the data in a separate storage area on the host or in a volume.

A volume is a dedicated storage area managed by Docker that exists independently of any container. You can attach it to a container so that data written inside is saved to the volume, not the container’s writable layer.

# Create a volume
docker volume create mydata
# Attach it when running
docker run -v mydata:/app/data myapp

Now even if the container is deleted, the data lives on and can be attached to a new container.

You can also use a bind mount to directly link a folder on your host to a path inside the container great for local development where you want live code changes reflected inside the container:

docker run -v /home/user/myproject:/app myapp

Network (Container Communication)

An internal network within a host is required for containers to communicate with each other. Docker provides some networking features by default, also giving us the ability to create custom networks between containers.

  1. Bridge : Each container gets its own IP on an internal virtual network. Containers can communicate with each other using their IP, but not by name unless you use a user-defined network.

  1. Host: The container shares the host’s network stack directly. No port mapping needed if your app runs on port 5000, it’s available on localhost:5000 immediately. Less isolation.

  1. None: Completely disables networking for the container. Used for batch jobs or tasks that should have zero network access.

  1. User-defined network: The recommended approach for multi-container setups. You create a custom network and attach containers to it. Docker then handles automatic DNS between them and containers can talk to each other by container name instead of IP address. DNS runs at 172.0.0.11 by default.

# Create a network
docker network create mynetwork
# Run containers on that network
docker run --network mynetwork --name db postgres
docker run --network mynetwork --name app myapp

Now your app container can reach the database at db:5432 no IPs, no guessing.

7. Building Your Own Images

Dockerfile

A Dockerfile is a plain text file that contains the step-by-step instructions Docker follows to build your image. Think of it as the recipe for your container.

FROM ubuntu:latest

# Install nodejs and npm
RUN apt-get update && apt-get install -y nodejs npm

# Copy files and set directory
COPY . /app
WORKDIR /app

# Install dependencies
RUN npm install

# Build the Next.js application (This compiles your page.tsx)
RUN npm run build


EXPOSE 5000
CMD ["npm", "run", "start"]

Here’s what each instruction does:

  1. FROM: Sets the base image. Every image starts from something here it’s the latest Ubuntu.
  2. RUN: Executes a shell command during the build process. Used to install packages, set up environment, etc.
  3. COPY: Copies files from your local machine into the image.
  4. WORKDIR: Sets the working directory for subsequent instructions (like cd inside the image).
  5. EXPOSE: Documents which port the container will use at runtime. (It’s informational you still need -p when running.)
  6. CMD: Specifies the default command to run when the container starts.

docker build / Image Creation

Once your Dockerfile is ready, you build it into an image using:

docker build -t myapp:latest .
  1. -t myapp:latest tags the image with a name and version.
  2. . tells Docker to look for the Dockerfile in the current directory.

Docker builds the image layer by layer each instruction in the Dockerfile creates a new cached layer. This means if you change only your COPY . /app step, Docker reuses the cached layers above it and only rebuilds from that point forward. Smart ordering of your Dockerfile instructions = faster builds.

Once built, you can run it:

docker run -p 8080:5000 myapp:latest

And push it to Docker Hub or any registry to share it:

docker tag myapp:latest yourusername/myapp:latest
docker push yourusername/myapp:latest

8. Conclusion

Docker fundamentally changes how we think about shipping software. Instead of fighting environment mismatches or writing a novel-length “setup guide” for every new team member, you ship a container and it just works, everywhere.

To recap:

  1. Images are the blueprints; containers are the running instances.
  2. The Docker Engine handles everything under the hood via the daemon.
  3. The CLI gives you full control over containers, images, networks, and volumes.
  4. Volumes keep your data alive beyond a container’s life; networks let containers talk to each other.
  5. Dockerfiles let you define and reproduce your environment in code.

Once you get comfortable running containers locally, the natural next step is automating it, building images and deploying containers as part of a CI/CD pipeline. Sound familiar? That’s right, Docker pairs beautifully with GitHub Actions.

Share this article:
Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *