Dockerfile

Dockerfile

1. What is a Dockerfile?

A Dockerfile is a configuration file for creating Docker images. When you write commands in a text file, Docker executes them in order to create an image.

Dockerfile → docker build → Docker Image → docker run → Container
(Blueprint)    (Build)       (Template)      (Run)      (Instance)

Why use a Dockerfile?

Advantage Description
Reproducibility Create identical images repeatedly
Automation No manual setup needed
Version control Track history with Git
Documentation Environment setup recorded as code

2. Dockerfile Basic Syntax

Basic Structure

# Comment
INSTRUCTION argument

Main Instructions

Instruction Description Example
FROM Base image FROM node:18
WORKDIR Working directory WORKDIR /app
COPY Copy files COPY . .
RUN Execute command during build RUN npm install
CMD Container startup command CMD ["npm", "start"]
EXPOSE Expose port EXPOSE 3000
ENV Environment variable ENV NODE_ENV=production

3. Instruction Details

FROM - Base Image

Every Dockerfile starts with FROM.

# Basic
FROM ubuntu:22.04

# Node.js image
FROM node:18

# Lightweight Alpine image (recommended)
FROM node:18-alpine

# Multi-stage build
FROM node:18 AS builder
FROM nginx:alpine AS production

WORKDIR - Working Directory

Sets the directory where subsequent commands will execute.

WORKDIR /app

# Subsequent commands execute in /app
COPY . .          # Copy to /app
RUN npm install   # Execute in /app

COPY - Copy Files

Copies files from host to image.

# Copy file
COPY package.json .

# Copy directory
COPY src/ ./src/

# Copy all files
COPY . .

# Copy multiple files
COPY package.json package-lock.json ./

ADD vs COPY

# COPY: Simple copy (recommended)
COPY local-file.txt /app/

# ADD: URL download, archive extraction
ADD https://example.com/file.tar.gz /app/
ADD archive.tar.gz /app/  # Auto-extracts

RUN - Execute Build Command

Executes during image build.

# Basic
RUN npm install

# Multiple commands (layer optimization)
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

# Separate for cache utilization
COPY package*.json ./
RUN npm install
COPY . .

CMD - Container Startup Command

Executes when container starts.

# exec form (recommended)
CMD ["npm", "start"]
CMD ["node", "app.js"]

# shell form
CMD npm start

ENTRYPOINT vs CMD

# ENTRYPOINT: Always executes (hard to change)
ENTRYPOINT ["node"]
CMD ["app.js"]
# Executes: node app.js

# docker run myimage other.js
# Executes: node other.js (only CMD changed)

ENV - Environment Variables

# Single variable
ENV NODE_ENV=production

# Multiple variables
ENV NODE_ENV=production \
    PORT=3000 \
    DB_HOST=localhost

EXPOSE - Document Port

# Expose port (documentation purpose, actual mapping with -p option)
EXPOSE 3000
EXPOSE 80 443

ARG - Build-time Variables

# Variables passed during build
ARG NODE_VERSION=18
FROM node:${NODE_VERSION}

ARG APP_VERSION=1.0.0
ENV APP_VERSION=${APP_VERSION}
# Pass value during build
docker build --build-arg NODE_VERSION=20 .

4. Practice Examples

Example 1: Node.js Application

Project structure:

my-node-app/
├── Dockerfile
├── package.json
└── app.js

package.json:

{
  "name": "my-node-app",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

app.js:

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({ message: 'Hello from Docker!', version: '1.0.0' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Dockerfile:

# Base image
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy dependency files (utilize cache)
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy source code
COPY . .

# Expose port
EXPOSE 3000

# Run command
CMD ["npm", "start"]

Build and run:

# Build image
docker build -t my-node-app .

# Run container
docker run -d -p 3000:3000 --name node-app my-node-app

# Test
curl http://localhost:3000

# Cleanup
docker rm -f node-app

Example 2: Python Flask Application

Project structure:

my-flask-app/
├── Dockerfile
├── requirements.txt
└── app.py

requirements.txt:

flask==3.0.0
gunicorn==21.2.0

app.py:

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def hello():
    return jsonify(message='Hello from Flask in Docker!')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Dockerfile:

FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy source
COPY . .

EXPOSE 5000

# Run with Gunicorn (production)
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Build and run:

docker build -t my-flask-app .
docker run -d -p 5000:5000 my-flask-app
curl http://localhost:5000

Example 3: Static Website (Nginx)

Project structure:

my-website/
├── Dockerfile
├── nginx.conf
└── public/
    └── index.html

public/index.html:

<!DOCTYPE html>
<html>
<head>
    <title>My Docker Website</title>
</head>
<body>
    <h1>Hello from Nginx in Docker!</h1>
</body>
</html>

Dockerfile:

FROM nginx:alpine

# Copy custom config (optional)
# COPY nginx.conf /etc/nginx/nginx.conf

# Copy static files
COPY public/ /usr/share/nginx/html/

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

5. Multi-stage Build

Separate build and runtime environments to reduce image size.

React App Example

# Stage 1: Build
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Runtime (serve with nginx)
FROM nginx:alpine

COPY --from=builder /app/build /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Go App Example

# Stage 1: Build
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY . .
RUN go build -o main .

# Stage 2: Runtime (minimal image)
FROM alpine:latest

WORKDIR /app
COPY --from=builder /app/main .

EXPOSE 8080
CMD ["./main"]

Size comparison:

golang:1.21-alpine    ~300MB (build environment)
Final image           ~15MB (runtime environment)

6. Best Practices

.dockerignore File

Exclude unnecessary files from build.

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
*.md
Dockerfile
.dockerignore

Layer Optimization

# Bad: Full reinstall every time
COPY . .
RUN npm install

# Good: Reinstall only when package.json changes
COPY package*.json ./
RUN npm install
COPY . .

Use Small Images

# Large
FROM node:18           # ~1GB

# Recommended
FROM node:18-alpine    # ~175MB

# Minimal
FROM node:18-slim      # ~200MB

Security

# Run as non-root user
FROM node:18-alpine

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

WORKDIR /app
COPY --chown=appuser:appgroup . .

7. Image Build Commands

# Basic build
docker build -t imagename .

# Specify tag
docker build -t myapp:1.0 .

# Use different Dockerfile
docker build -f Dockerfile.prod -t myapp:prod .

# Pass build arguments
docker build --build-arg NODE_ENV=production -t myapp .

# Build without cache
docker build --no-cache -t myapp .

# Verbose build output
docker build --progress=plain -t myapp .

Command Summary

Dockerfile Instruction Description
FROM Specify base image
WORKDIR Set working directory
COPY Copy files/directories
RUN Execute command during build
CMD Container startup command
EXPOSE Document port
ENV Set environment variable
ARG Build-time variable
ENTRYPOINT Fixed execution command

Next Steps

Learn how to manage multiple containers together in 04_Docker_Compose.md!

to navigate between lessons