Creating Dockerfiles for Different Languages

Crafting efficient container blueprints for your applications

Introduction to Dockerfiles

A Dockerfile is a text document containing a series of instructions that Docker uses to automatically build an image. Think of it as a recipe for creating a container—it specifies the ingredients (base image, dependencies) and the steps (commands, configurations) needed to prepare your application for containerization.

Dockerfiles are essential because they allow you to:

graph TD A[Dockerfile] -->|docker build| B[Docker Image] B -->|docker run| C[Docker Container] A --> D[Version Control] A --> E[CI/CD Pipeline] A --> F[Documentation]

Dockerfile Syntax and Structure

Dockerfiles follow a specific syntax with instructions written in all caps, followed by arguments. Here's the basic structure:

# Comment explaining the Dockerfile
FROM base-image:tag

LABEL maintainer="name@example.com"

# Environment setup
ENV KEY=value

# Set working directory
WORKDIR /app

# Copy files
COPY source destination

# Run commands
RUN command

# Expose ports
EXPOSE port

# Define default command
CMD ["executable", "param1", "param2"]

Key Dockerfile Instructions

The order of these instructions matters because each instruction creates a new layer in the image, and layers are cached. Organizing your Dockerfile efficiently can significantly speed up builds.

Dockerfile Best Practices

Following best practices helps create efficient, secure, and maintainable container images:

Use Specific Base Image Tags

Always specify exact versions of base images to ensure reproducible builds:

# Bad practice: can lead to unpredictable builds
FROM node

# Good practice: specifies exact version
FROM node:16.15.1-alpine3.16

Minimize Layers

Combine commands to reduce the number of layers and image size:

# Bad practice: creates 3 separate layers
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get clean

# Good practice: creates a single layer
RUN apt-get update && \
    apt-get install -y package1 && \
    apt-get clean

Leverage Build Cache

Order instructions from least to most likely to change to maximize cache usage:

# Better caching: system dependencies change less frequently
COPY package.json package-lock.json ./
RUN npm install
# Source code changes more frequently
COPY . .

Use .dockerignore

Create a .dockerignore file to exclude files not needed in your image:

# Example .dockerignore file
node_modules
npm-debug.log
.git
.gitignore
README.md
Dockerfile
.dockerignore
tests
coverage

Use Multi-stage Builds

Multi-stage builds help create smaller production images by separating build and runtime environments:

# Build stage
FROM node:16-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Run as Non-root User

Improve security by running containers with a non-root user:

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
CMD ["node", "app.js"]

Include Health Checks

Add health checks to help container orchestrators monitor your application:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost/ || exit 1

Creating Dockerfiles for Node.js Applications

Node.js is a popular JavaScript runtime for building server-side applications. Let's explore how to create efficient Dockerfiles for Node.js projects:

Basic Node.js Dockerfile

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["node", "index.js"]

Production-Ready Node.js Dockerfile

This enhanced version includes security considerations and optimizations:

FROM node:16-alpine

# Create app directory
WORKDIR /app

# Install app dependencies
COPY package*.json ./

# Install only production dependencies
RUN npm ci --only=production

# Bundle app source
COPY . .

# Create a non-root user
RUN addgroup -S nodejs && adduser -S nodejsuser -G nodejs
USER nodejsuser

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:3000/health || exit 1

# Define environment variable
ENV NODE_ENV production

# Start the application
CMD ["node", "index.js"]

Node.js Development Dockerfile

For development with hot-reloading:

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

# Use nodemon for development
CMD ["npm", "run", "dev"]

Multi-stage Build for Node.js

For applications that require a build step (like TypeScript or webpack):

# Build stage
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Production stage
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
ENV NODE_ENV production
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
graph TD A[Node.js Dockerfile] --> B[Development Dockerfile] A --> C[Production Dockerfile] A --> D[Multi-stage Build] B --> E[Hot Reloading] B --> F[Developer Tools] C --> G[Minimal Dependencies] C --> H[Security Hardening] D --> I[Separate Build/Runtime] D --> J[Smaller Image Size]

Creating Dockerfiles for Python Applications

Python is a versatile language used for web applications, data science, and more. Here's how to containerize Python applications:

Basic Python Dockerfile

FROM python:3.10-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

Production-Ready Python Dockerfile

FROM python:3.10-slim

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=off \
    PIP_DISABLE_PIP_VERSION_CHECK=on

WORKDIR /app

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

# Create non-root user
RUN adduser --disabled-password --gecos "" appuser
USER appuser

# Copy application code
COPY --chown=appuser:appuser . .

# Expose port
EXPOSE 5000

# Health check
HEALTHCHECK --interval=30s --timeout=5s \
  CMD curl -f http://localhost:5000/health || exit 1

# Run the application
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:5000"]

Python Data Science Dockerfile

For data science and machine learning projects:

FROM python:3.10

# Install system dependencies
RUN apt-get update && apt-get install -y \
    libpq-dev gcc build-essential \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

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

# Copy project files
COPY . .

# For Jupyter notebooks
EXPOSE 8888

# Start Jupyter
CMD ["jupyter", "notebook", "--ip='0.0.0.0'", "--port=8888", "--no-browser", "--allow-root"]

Django/Flask Dockerfile with PostgreSQL

FROM python:3.10-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

# Install PostgreSQL client and other dependencies
RUN apt-get update && apt-get install -y \
    postgresql-client libpq-dev gcc \
    && rm -rf /var/lib/apt/lists/*

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

# Copy project
COPY . .

# Run migrations and start server
CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"]

EXPOSE 8000

Multi-stage Python Build

# Build stage
FROM python:3.10 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# For Python applications that need compilation or other build steps
RUN python setup.py build

# Production stage
FROM python:3.10-slim
WORKDIR /app
COPY --from=builder /app/build /app/build
COPY --from=builder /app/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 5000
CMD ["python", "-m", "app"]

Creating Dockerfiles for Java Applications

Java applications often require a build tool like Maven or Gradle. Here's how to handle Java applications in Docker:

Basic Java Dockerfile

FROM openjdk:17-jdk-slim

WORKDIR /app

COPY target/myapp.jar app.jar

EXPOSE 8080

CMD ["java", "-jar", "app.jar"]

Spring Boot Dockerfile

FROM openjdk:17-jdk-slim

VOLUME /tmp

COPY target/*.jar app.jar

ENV JAVA_OPTS=""

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -q -T 3 -O - http://localhost:8080/actuator/health | grep UP || exit 1

Multi-stage Java Build with Maven

# Build stage
FROM maven:3.8.6-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
# Download dependencies
RUN mvn dependency:go-offline -B
COPY src ./src
# Build the application
RUN mvn package -DskipTests

# Runtime stage
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Java with Gradle Build

# Build stage
FROM gradle:7.4-jdk17 AS build
WORKDIR /app
COPY --chown=gradle:gradle . .
RUN gradle build --no-daemon

# Runtime stage
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]
Java Docker Build Process Source Code Build Stage JAR File Runtime Stage Container Copy Generate Copy Discard Run

Creating Dockerfiles for PHP Applications

PHP is commonly used for web development, often with frameworks like Laravel or WordPress. Here's how to containerize PHP applications:

Basic PHP Dockerfile

FROM php:8.1-apache

WORKDIR /var/www/html

COPY . .

RUN chown -R www-data:www-data /var/www/html

EXPOSE 80

CMD ["apache2-foreground"]

PHP with Composer Dockerfile

FROM php:8.1-fpm

# Install dependencies
RUN apt-get update && apt-get install -y \
    libzip-dev \
    zip \
    unzip \
    git \
    && docker-php-ext-install pdo_mysql zip

# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

# Copy Composer files
COPY composer.json composer.lock ./

# Install dependencies
RUN composer install --no-scripts --no-autoloader

# Copy application files
COPY . .

# Generate autoload files
RUN composer dump-autoload --optimize

# Set permissions
RUN chown -R www-data:www-data /var/www/html

# Expose port 9000 for FPM
EXPOSE 9000

CMD ["php-fpm"]

Laravel Dockerfile

# Build stage
FROM composer:2 as build
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-scripts --no-autoloader --no-dev
COPY . .
RUN composer dump-autoload --optimize

# Production stage
FROM php:8.1-fpm
WORKDIR /var/www/html

# Install dependencies
RUN apt-get update && apt-get install -y \
    libzip-dev \
    zip \
    && docker-php-ext-install pdo_mysql zip

# Copy application
COPY --from=build /app .
COPY --from=build /app/public .

# Set permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 9000

CMD ["php-fpm"]

WordPress Dockerfile

FROM wordpress:php8.1-apache

# Install WP-CLI
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
    && chmod +x wp-cli.phar \
    && mv wp-cli.phar /usr/local/bin/wp

# Install additional PHP extensions
RUN docker-php-ext-install mysqli pdo pdo_mysql

# Copy custom configuration
COPY php.ini /usr/local/etc/php/
COPY wp-config.php /var/www/html/

# Copy custom themes and plugins
COPY ./themes/ /var/www/html/wp-content/themes/
COPY ./plugins/ /var/www/html/wp-content/plugins/

# Set proper permissions
RUN chown -R www-data:www-data /var/www/html

EXPOSE 80

CMD ["apache2-foreground"]

Creating Dockerfiles for Go Applications

Go (Golang) is known for producing statically-linked binaries, which are perfect for containerization. Here's how to create efficient Dockerfiles for Go applications:

Basic Go Dockerfile

FROM golang:1.18-alpine

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o main .

EXPOSE 8080

CMD ["./main"]

Multi-stage Go Build

Go applications benefit greatly from multi-stage builds to create extremely small images:

# Build stage
FROM golang:1.18-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Final stage
FROM alpine:3.16
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]

Distroless Go Container

For even smaller and more secure containers, you can use Google's distroless images:

# Build stage
FROM golang:1.18 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Final stage
FROM gcr.io/distroless/static
COPY --from=builder /app/main /
EXPOSE 8080
CMD ["/main"]
graph TD A[Go Dockerfile Strategies] --> B[Basic Single-Stage] A --> C[Multi-Stage Build] A --> D[Distroless Container] B --> E["Larger Image
(~800MB+)"] C --> F["Small Image
(~10-20MB)"] D --> G["Minimal Image
(<10MB)"] C --> H["Alpine Base"] D --> I["No Shell/Tools
More Secure"]

Go's ability to create statically linked binaries makes it ideal for creating extremely small and efficient containers. The multi-stage build approach is particularly effective for Go applications.

Creating Dockerfiles for Frontend Applications

Frontend applications built with frameworks like React, Angular, or Vue.js typically require a build step followed by serving static files. Here's how to containerize them:

React Application Dockerfile

# Build stage
FROM node:16-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Angular Application Dockerfile

# Build stage
FROM node:16-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build -- --prod

# Production stage
FROM nginx:alpine
COPY --from=build /app/dist/* /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Vue.js Application Dockerfile

# Build stage
FROM node:16-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Sample nginx.conf for SPA

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Support for SPA routing
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000";
    }
}

Creating Dockerfiles for Database Containers

While it's often best to use official database images directly, sometimes you need to customize them. Here are some examples:

PostgreSQL with Custom Configuration

FROM postgres:14-alpine

# Add custom configuration
COPY postgresql.conf /etc/postgresql/postgresql.conf

# Initialize database with scripts
COPY ./init-scripts/ /docker-entrypoint-initdb.d/

# Set environment variables
ENV POSTGRES_USER=myuser
ENV POSTGRES_PASSWORD=mypassword
ENV POSTGRES_DB=mydb

# Command to run Postgres with custom config
CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]

MySQL with Data Import

FROM mysql:8.0

# Add custom configuration
COPY my.cnf /etc/mysql/conf.d/

# Initialize database with scripts
COPY ./init-scripts/ /docker-entrypoint-initdb.d/

# Set environment variables
ENV MYSQL_ROOT_PASSWORD=rootpassword
ENV MYSQL_DATABASE=mydb
ENV MYSQL_USER=myuser
ENV MYSQL_PASSWORD=mypassword

EXPOSE 3306

MongoDB with Custom Configuration

FROM mongo:5.0

# Add custom configuration
COPY mongod.conf /etc/mongod.conf

# Initialize database with scripts
COPY ./init-scripts/ /docker-entrypoint-initdb.d/

# Set environment variables
ENV MONGO_INITDB_ROOT_USERNAME=admin
ENV MONGO_INITDB_ROOT_PASSWORD=adminpassword
ENV MONGO_INITDB_DATABASE=mydb

# Run MongoDB with custom config
CMD ["mongod", "--config", "/etc/mongod.conf"]

When working with database containers, it's generally better to use the official images directly and configure them using environment variables and volume mounts rather than building custom images, especially for production environments.

Debugging Dockerfile Issues

When your Dockerfile doesn't work as expected, use these techniques to debug the issues:

Common Dockerfile Problems

Debugging Techniques

Iterative Development Tips

Practice Activities

Activity 1: Create a Basic Node.js Dockerfile

  1. Create a simple Node.js application:
    mkdir node-docker-demo
    cd node-docker-demo
    npm init -y
    npm install express
    # Create index.js with the following content
    console.log("Creating index.js file...")
    cat > index.js << 'EOF'
    const express = require('express');
    const app = express();
    const port = process.env.PORT || 3000;
    
    app.get('/', (req, res) => {
      res.send('Hello from Docker!');
    });
    
    app.listen(port, () => {
      console.log(`Server running on port ${port}`);
    });
    EOF
  2. Create a Dockerfile in the same directory
  3. Build and run your containerized application
  4. Access the application in your browser

Activity 2: Implement a Multi-stage Build

  1. Create a simple React application:
    npx create-react-app react-docker-demo
    cd react-docker-demo
  2. Create a Dockerfile using multi-stage builds
  3. Create an nginx.conf file for serving the app
  4. Build and run the containerized React application
  5. Compare the size of your multi-stage image with a single-stage version

Activity 3: Optimize an Existing Dockerfile

  1. Start with this unoptimized Dockerfile:
    FROM ubuntu:20.04
    RUN apt-get update
    RUN apt-get install -y python3
    RUN apt-get install -y python3-pip
    RUN pip3 install flask
    RUN mkdir /app
    COPY . /app
    WORKDIR /app
    EXPOSE 5000
    CMD ["python3", "app.py"]
  2. Create a simple Flask application (app.py):
    from flask import Flask
    app = Flask(__name__)
    
    @app.route('/')
    def hello():
        return "Hello, Docker!"
    
    if __name__ == "__main__":
        app.run(host='0.0.0.0')
  3. Optimize the Dockerfile following best practices
  4. Build both versions and compare the size and build time

Resources for Further Learning

Summary

In this lecture, we've explored how to create effective Dockerfiles for various programming languages and frameworks:

Understanding how to create proper Dockerfiles is essential for modern application development and deployment. In our next lecture, we'll explore Docker Compose for multi-container applications.