Write Your First Dockerfile 🐳 | FROM, WORKDIR, COPY, RUN Explained
1. Why Do We Need a Dockerfile? (Problem Statement & Status Quo)
In previous sessions, we explored individual Docker containers and running basic commands via the command line. While running raw commands works for ad-hoc experimentation, it breaks down in production pipelines.
- The Naive Approach (
docker run): Executing raw commands likedocker run -d -p 8080:80 nginxis entirely temporary and manual. It is difficult to repeat accurately because you must remember and re-pass complex parameters every single time. - The Production Solution (Dockerfile): A Dockerfile is a permanent, text-based script containing a sequential set of instructions required to build and run an application.
- Key Advantages of Dockerfiles:
- Version Control: Track changes to your infrastructure and application packaging over time using Git.
- Automation: Build identical environments consistently across development, staging, and production servers.
- Standardization: Acts as a definitive "recipe book" that any developer or CI/CD pipeline can execute to produce identical results.
2. Core Mental Model: The Recipe Book Analogy
Real-Life Analogy: Think of a
docker runcommand as cooking a dish instantly off the top of your head without a recipe—fast for a single plate, but nearly impossible to replicate identically tomorrow.In contrast, a Dockerfile is a cookbook recipe. It lists every ingredient (
FROM,COPY), preparation step (RUN), and serving requirement (EXPOSE,CMD). Multiple chefs (developers, CI/CD runners, cloud servers) can follow the exact same recipe book to cook the exact same dish every single time.
3. Naive CLI Execution vs. Dockerfile Evolution
Before writing a Dockerfile, let us examine how we perform container operations manually on an AWS EC2 instance, and how those actions translate directly into Dockerfile instructions.
Step 1: Cleaning the Local Environment
Before running our tests, we clear out existing containers and images to start with a pristine environment:
# Stop and remove all running containers
docker rm -f $(docker ps -aq)
# Remove all local Docker images
docker rmi $(docker images -q)
# Verify clean state
docker images
Step 2: Running Nginx Manually
To start an Nginx server manually and map host port 8080 to container port 80:
docker run -d -p 8080:80 nginx
-d: Run the container in the background (detached mode).-p 8080:80: Map port8080on the host machine to port80inside the container.nginx: The base image pulled from Docker Hub.
Verifying the container is running:
Browser
|
| sends HTTP request to http://<EC2-Public-IP>:8080
v
Host Machine (Port 8080)
|
| forwards traffic via port mapping
v
Nginx Container (Port 80) -> Returns default "Welcome to nginx!" page
4. Step-by-Step Evolution of Our First Dockerfile
Let's build our first simple Dockerfile to package standard Nginx without custom HTML files.
Creating the Dockerfile (10-Dockerfile)
Create a file named Dockerfile (with an uppercase D) in Visual Studio Code:
# Use official Nginx image as the base image
FROM nginx:latest
# Expose port 80 for incoming traffic to the Nginx server
EXPOSE 80
# Start Nginx in the foreground to keep the container alive
CMD ["nginx", "-g", "daemon off;"]
Building and Running the Image
-
Build the image:
BASHdocker build -t new-ng .(Note: The trailing
.tells Docker to look for theDockerfilein the current working directory). -
Verify image layers: Each instruction (
FROM,EXPOSE,CMD) creates an immutable image layer. Comments are ignored. Check your images:BASHdocker images -
Run the container:
BASHdocker run -d -p 8080:80 new-ng:latest
5. Hosting Custom Code: Index.html & Volume Mounting
Real applications require custom code rather than default welcome pages. Let's see how manual volume mounting works before encoding it into a Dockerfile.
The Manual Volume Mounting Approach
If we have a custom index.html file on our host machine, we can mount it directly into Nginx's default web root inside a running container:
docker run -d -p 8080:80 -v $(pwd)/index.html:/usr/share/nginx/html/index.html nginx
While volume mounting is useful for local development, it requires the file to exist on the host machine at runtime. For true portability, we need to bake the code directly into the Docker image using COPY and RUN.
6. Deep Dive: Key Dockerfile Instructions Explained
Let's refactor our Dockerfile to permanently bundle our custom index.html webpage, strip out default Nginx placeholder files, and prepare the image for Docker Hub distribution.
Final Production Dockerfile Structure
# 1. Set the foundational base image
FROM nginx:latest
# 2. Expose network port for container runtime traffic
EXPOSE 80
# 3. Copy local custom web files into Nginx's hosting directory
COPY index.html /usr/share/nginx/html/index.html
# 4. Clean up default conflicting Nginx files during build time
RUN rm -rf /usr/share/nginx/html/default.html
# 5. Execute container startup command
CMD ["nginx", "-g", "daemon off;"]
Keyword Reference Table
7. Real-World Practical Pitfalls & Debugging Mindset
When building and deploying Dockerfiles in enterprise environments, developers frequently encounter specific failure modes:
- Capitalization Errors: The filename must strictly be
Dockerfilewith a capital D. Lowercasedockerfilewill cause build errors on case-sensitive Linux filesystems. - Context Path Mistakes: Forgetting the trailing dot (
.) indocker build -t app .results in a build failure because Docker cannot locate the build context directory. - Default Page Conflicts: Nginx ships with default HTML welcome pages. If you copy your custom
index.htmlinto the wrong directory or fail to clean up existing template files, Nginx may continue serving default assets. Always verify target paths (/usr/share/nginx/html/). - Foreground vs. Background Processes: Containers exit immediately if their main process (
PID 1) exits. This is why Nginx must be run withdaemon off;in theCMDinstruction—to keep it running persistently in the foreground.
8. Pushing Custom Images to Docker Hub
Once your Dockerfile successfully builds your custom app image, you can share it globally via Docker Hub using tagging and pushing.
Step-by-Step Release Workflow
-
Build the Custom Image:
BASHdocker build -t new-app . -
Tag the Image (Rename it to include your Docker Hub username namespace):
BASHdocker tag new-app your-dockerhub-username/new-app:latest -
Push to Docker Hub:
BASHdocker push your-dockerhub-username/new-app:latest
9. Summary and Key Takeaways
- **- Dockerfiles vs. Docker Run:
docker runis a manual, temporary, hard-to-repeat command. A Dockerfile provides a permanent, version-controlled, automated recipe for building images. - **- The Foundation (
FROM): Every Dockerfile must begin with aFROMinstruction establishing a base image layer (e.g., Nginx, Node.js, Python, Ubuntu). - **- Building Layers: Each line in a Dockerfile adds an immutable layer to the final container image, optimizing caching and deployment speed.
- **- File Integration (
COPY): Use theCOPYinstruction to bundle local source code and static assets directly into the container image file system. - **- Build-Time Execution (
RUN): UseRUNto execute commands (like package managers or file cleanups) while the image is being built. - **- Runtime Execution (
CMD): UseCMDto define the default process that launches when a container starts up from your image. - **- Port Exposure (
EXPOSE): Document runtime network ports clearly so container networking and port mappings (-p host:container) operate correctly. - **- Image Distribution: Tagging images with your Docker Hub username enables seamless cloud sharing, eliminating manual file transfers across team environments.