Home Docker Containerization
Post
Cancel
Docker Containerization | SEG

Docker Containerization

This post introduces Docker and containerization concepts. Understanding basic command-line operations and having familiarity with application deployment will help you get the most out of this guide!

What is Docker?

Imagine you’ve built an amazing application on your laptop. It works perfectly. Then you try to run it on your colleague’s machine or deploy it to a server, and suddenly: “But it works on my machine!”

This is where Docker comes to the rescue. Docker is a platform that packages your application and all its dependencies into a standardized unit called a container. This container runs the same way everywhere, on your laptop, your teammate’s computer, or in production.

Why Containerization Matters

Containerization solves several critical problems in software development:

  1. Consistency - Same environment from development to production
  2. Isolation - Applications don’t interfere with each other
  3. Portability - Run anywhere Docker is installed
  4. Efficiency - Lightweight and fast to start
  5. Scalability - Easy to replicate and scale applications

"I remember spending hours debugging why my Python application worked locally but crashed in production. The culprit? Different library versions. Docker would have saved me days of frustration."

Core Docker Concepts

Before diving into commands and examples, let’s understand the fundamental building blocks of Docker.

Container

A running instance of your application with all dependencies.

A container is a lightweight, standalone executable package that includes everything needed to run a piece of software: code, runtime, system tools, libraries, and settings. Containers are isolated from each other and the host system, but share the OS kernel.

Key characteristics:

  • Ephemeral (can be stopped and started)
  • Isolated (has its own filesystem, network, process space)
  • Lightweight (shares OS kernel, no full OS needed)
  • Fast to start (seconds, not minutes)

Image

A blueprint or template for creating containers.

An image is a read-only template with instructions for creating a container. Think of it as a snapshot or a class definition (if you’re thinking OOP). Images are built from a Dockerfile and can be stored in registries.

Key characteristics:

  • Immutable (doesn’t change)
  • Layered (built in layers for efficiency)
  • Versioned (tagged with versions like app:1.0, app:latest)
  • Shareable (pushed to and pulled from registries)

Dockerfile

A text file with instructions to build a Docker image.

A Dockerfile contains a series of commands that Docker executes to create an image. It specifies the base image, copies files, installs dependencies, sets environment variables, and defines what command runs when a container starts.

Registry

A storage and distribution system for Docker images.

A Docker registry is like GitHub for Docker images. The most popular public registry is Docker Hub. Organizations often run private registries for their images.

Docker vs Virtual Machines

Understanding the difference between Docker containers and virtual machines (VMs) is crucial:

AspectDocker ContainerVirtual Machine
SizeMegabytesGigabytes
Startup TimeSecondsMinutes
Resource UsageShares host OS kernelFull OS per VM
IsolationProcess-levelHardware-level
PortabilityHighMedium
PerformanceNear-nativeOverhead from hypervisor
flowchart TB
    subgraph VM["Virtual Machine Architecture"]
        direction TB
        HW1[Hardware]
        HOST1[Host OS]
        HYP[Hypervisor]
        VM1[Guest OS]
        VM2[Guest OS]
        APP1[App A]
        APP2[App B]
        
        HW1 --> HOST1
        HOST1 --> HYP
        HYP --> VM1
        HYP --> VM2
        VM1 --> APP1
        VM2 --> APP2
    end
    
    subgraph DOCKER["Docker Architecture"]
        direction TB
        HW2[Hardware]
        HOST2[Host OS]
        DENGINE[Docker Engine]
        C1[Container A]
        C2[Container B]
        
        HW2 --> HOST2
        HOST2 --> DENGINE
        DENGINE --> C1
        DENGINE --> C2
    end

Important: Containers share the host OS kernel, making them much lighter than VMs. However, this means Linux containers need a Linux kernel (though Docker Desktop provides this on Windows/Mac).

Basic Docker Workflow

Here’s the typical development workflow with Docker:

flowchart LR
    DF[Write Dockerfile]
    BUILD[Build Image]
    RUN[Run Container]
    TEST[Test Application]
    PUSH[Push to Registry]
    DEPLOY[Deploy/Pull]
    
    DF --> BUILD
    BUILD --> RUN
    RUN --> TEST
    TEST --> |Success| PUSH
    TEST --> |Issues| DF
    PUSH --> DEPLOY

The Development Cycle

  1. Write a Dockerfile defining your application environment
  2. Build an image from the Dockerfile
  3. Run a container from the image
  4. Test your application in the container
  5. Push the image to a registry
  6. Deploy by pulling the image on target servers

Essential Docker Commands

Let’s explore the commands you’ll use daily as a Docker developer.

Image Management

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Pull an image from Docker Hub
docker pull nginx:latest

# List all images on your system
docker images

# Build an image from a Dockerfile
docker build -t myapp:1.0 .

# Tag an image
docker tag myapp:1.0 myusername/myapp:1.0

# Push an image to a registry
docker push myusername/myapp:1.0

# Remove an image
docker rmi myapp:1.0

Container Management

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Run a container from an image
docker run -d -p 8080:80 --name webserver nginx

# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# Stop a running container
docker stop webserver

# Start a stopped container
docker start webserver

# Remove a container
docker rm webserver

# View container logs
docker logs webserver

# Execute a command in running container
docker exec -it webserver bash

Pro Tip: The -d flag runs containers in detached mode (background), and -it provides an interactive terminal.

Command Breakdown

Let’s understand a common docker run command:

1
docker run -d -p 8080:80 -v /host/path:/container/path --name myapp -e ENV_VAR=value myapp:1.0
FlagPurposeExample Value
-dDetached mode (background)-
-pPort mapping (host:container)8080:80
-vVolume mount (data persistence)/host/path:/container/path
--nameContainer namemyapp
-eEnvironment variableENV_VAR=value
Last argumentImage name and tagmyapp:1.0

Creating a Dockerfile for Java Applications

Let’s create a Dockerfile for a Java Spring Boot application. This is a practical example you’ll use frequently.

Simple Java Application Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Start from official Java base image
FROM eclipse-temurin:17-jre

# Set working directory inside container
WORKDIR /app

# Copy the JAR file from build output
COPY target/myapp.jar app.jar

# Expose the port your app runs on
EXPOSE 8080

# Command to run when container starts
ENTRYPOINT ["java", "-jar", "app.jar"]

Multi-Stage Build Dockerfile (Best Practice)

A multi-stage build compiles your application inside Docker, ensuring consistent builds:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Stage 1: Build the application
FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

# Copy dependency files first (for layer caching)
COPY pom.xml .
RUN mvn dependency:go-offline

# Copy source code and build
COPY src ./src
RUN mvn clean package -DskipTests

# Stage 2: Create runtime image
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

# Copy only the built artifact from builder stage
COPY --from=builder /build/target/*.jar app.jar

# Create non-root user for security
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

# Expose port
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
  CMD wget --quiet --tries=1 --spider http://localhost:8080/actuator/health || exit 1

# Run the application
ENTRYPOINT ["java", "-jar", "app.jar"]

Why multi-stage builds? They keep your final image small by excluding build tools (Maven, source code) that aren’t needed at runtime.

Building and Running Your Java App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Build the image
docker build -t my-spring-app:1.0 .

# Run the container
docker run -d -p 8080:8080 --name spring-app my-spring-app:1.0

# Check if it's running
docker ps

# View logs
docker logs -f spring-app

# Test the application
curl http://localhost:8080/actuator/health

Docker Compose for Multi-Container Applications

Real applications often need multiple services: your app, a database, a cache, etc. Docker Compose lets you define and run multi-container applications with a single configuration file.

Docker Compose Architecture

flowchart TB
    COMPOSE[docker-compose.yml]
    
    subgraph Services
        APP[Spring Boot App<br/>Port: 8080]
        DB[(PostgreSQL<br/>Port: 5432)]
        REDIS[Redis Cache<br/>Port: 6379]
    end
    
    subgraph Network
        NETWORK[Docker Network<br/>Services can communicate]
    end
    
    COMPOSE --> APP
    COMPOSE --> DB
    COMPOSE --> REDIS
    
    APP -.->|connects to| DB
    APP -.->|connects to| REDIS
    
    APP --> NETWORK
    DB --> NETWORK
    REDIS --> NETWORK

Example: Spring Boot + PostgreSQL

Create a docker-compose.yml file in your project root:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
version: '3.8'

services:
  # Spring Boot application
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/myappdb
      - SPRING_DATASOURCE_USERNAME=appuser
      - SPRING_DATASOURCE_PASSWORD=securepassword
    depends_on:
      - db
    networks:
      - app-network
    restart: unless-stopped

  # PostgreSQL database
  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=myappdb
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=securepassword
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - app-network
    restart: unless-stopped

networks:
  app-network:
    driver: bridge

volumes:
  postgres-data:

Docker Compose Commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Start all services
docker-compose up -d

# View running services
docker-compose ps

# View logs from all services
docker-compose logs -f

# View logs from specific service
docker-compose logs -f app

# Stop all services
docker-compose down

# Stop and remove volumes (data)
docker-compose down -v

# Rebuild and restart services
docker-compose up -d --build

Important: Volumes persist data even when containers are removed. The postgres-data volume ensures your database survives restarts.

Key Docker Compose Concepts

Services

Each container definition in your application.

Services are the building blocks of your application. Each service runs in its own container. In the example above, we have two services: app and db.

Networks

Allow services to communicate with each other.

Docker Compose automatically creates a network for your services. Services can reach each other using their service name as hostname. For example, the app connects to the database using db:5432.

Volumes

Persist data beyond container lifecycle.

Volumes are used to store data that should survive container restarts. Database data, uploaded files, and application logs are commonly stored in volumes.

depends_on

Control startup order of services.

The depends_on directive ensures that the database starts before the application. However, it only waits for the container to start, not for the database to be fully ready.

Best Practices for Container Development

Following these best practices will help you create efficient, secure, and maintainable Docker containers.

1. Keep Images Small

1
2
3
4
5
6
7
8
# ❌ Bad: Using full JDK
FROM eclipse-temurin:17

# ✅ Good: Using JRE (smaller)
FROM eclipse-temurin:17-jre

# ✅ Even better: Using Alpine variant
FROM eclipse-temurin:17-jre-alpine

Why? Smaller images mean faster builds, faster deployments, and reduced attack surface.

2. Use Layer Caching Effectively

1
2
3
4
5
6
7
8
9
# ❌ Bad: Copy everything first
COPY . .
RUN mvn clean package

# ✅ Good: Copy dependencies first
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package

Why? Docker caches each layer. If dependencies haven’t changed, Docker reuses the cached layer instead of re-downloading everything.

3. Run as Non-Root User

1
2
3
4
5
6
7
8
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Switch to non-root user
USER appuser:appgroup

# Now commands run as appuser
ENTRYPOINT ["java", "-jar", "app.jar"]

Why? Running as root is a security risk. If an attacker compromises your container, they shouldn’t have root privileges.

Security Tip: Always run containers as non-root users in production environments.

4. Use .dockerignore File

Create a .dockerignore file in your project root:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Build output
target/
build/

# IDE files
.idea/
.vscode/
*.iml

# Git
.git/
.gitignore

# Documentation
README.md
docs/

# Environment files
.env
.env.local

Why? Prevents unnecessary files from being copied into the image, reducing build time and image size.

5. Use Environment Variables for Configuration

1
2
3
4
5
6
# In Dockerfile
ENV APP_PORT=8080
ENV DB_HOST=localhost

# Or pass at runtime
docker run -e DB_HOST=production-db myapp:1.0

Why? Makes your images reusable across different environments (dev, staging, production).

6. Add Health Checks

1
2
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Why? Docker can automatically restart unhealthy containers, improving reliability.

7. Use Specific Image Tags

1
2
3
4
5
# ❌ Bad: Using latest tag
FROM openjdk:latest

# ✅ Good: Using specific version
FROM eclipse-temurin:17-jre-alpine

Why? latest tag can change, breaking your builds. Specific versions ensure reproducibility.

Common Pitfalls for Beginners

Let’s address mistakes that junior engineers commonly make with Docker.

Pitfall 1: Not Understanding Container Lifecycle

1
2
3
4
5
6
7
# This creates a NEW container each time
docker run myapp:1.0  # Creates container 1
docker run myapp:1.0  # Creates container 2
docker run myapp:1.0  # Creates container 3

# Instead, start existing containers
docker start container-name

Remember: docker run creates a new container. docker start starts an existing stopped container.

Pitfall 2: Losing Data When Container Stops

1
2
3
4
5
# ❌ Without volume: Data is lost when container is removed
docker run postgres:15

# ✅ With volume: Data persists
docker run -v postgres-data:/var/lib/postgresql/data postgres:15

Pitfall 3: Port Confusion

1
2
3
4
5
6
# Format: -p HOST_PORT:CONTAINER_PORT
docker run -p 8080:80 nginx

# This means:
# - nginx listens on port 80 INSIDE the container
# - You access it on port 8080 on your HOST machine

Pitfall 4: Not Cleaning Up

Docker can fill up your disk with unused images and containers:

1
2
3
4
5
6
7
8
9
10
11
# Remove stopped containers
docker container prune

# Remove unused images
docker image prune

# Remove unused volumes
docker volume prune

# Remove everything unused (nuclear option)
docker system prune -a

Pro Tip: Run docker system prune periodically to keep your system clean.

Pitfall 5: Hardcoding Configuration

1
2
3
4
5
# ❌ Bad: Hardcoded values
ENV DB_PASSWORD=mysecretpassword

# ✅ Good: Use at runtime
# docker run -e DB_PASSWORD=$SECURE_PASSWORD myapp

Pitfall 6: Ignoring Build Context Size

1
2
3
4
5
6
7
# If you run docker build from wrong directory
docker build -t myapp /

# This copies your ENTIRE filesystem as build context!
# Always build from project directory
cd /path/to/project
docker build -t myapp .

Practical Exercise: Complete Workflow

Let’s put everything together with a complete example. Here’s a step-by-step workflow for a Java Spring Boot application.

Project Structure

1
2
3
4
5
6
7
8
9
10
myapp/
├── src/
│   └── main/
│       ├── java/
│       └── resources/
│           └── application.yml
├── pom.xml
├── Dockerfile
├── docker-compose.yml
└── .dockerignore

Step 1: Update application.yml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
  datasource:
    url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/myappdb}
    username: ${SPRING_DATASOURCE_USERNAME:appuser}
    password: ${SPRING_DATASOURCE_PASSWORD:password}
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

server:
  port: ${APP_PORT:8080}

Step 2: Build and Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Build the image
docker build -t myapp:1.0 .

# Run with docker-compose (app + database)
docker-compose up -d

# Check if everything is running
docker-compose ps

# View logs
docker-compose logs -f app

# Test the application
curl http://localhost:8080/api/hello

# Stop everything
docker-compose down

Step 3: Push to Registry

1
2
3
4
5
6
7
8
# Login to Docker Hub
docker login

# Tag the image
docker tag myapp:1.0 yourusername/myapp:1.0

# Push to registry
docker push yourusername/myapp:1.0

When to Use Docker

Docker is not always the answer. Here’s when it shines:

Use Docker WhenConsider Alternatives When
You need consistent environmentsSimple single-file scripts
Multiple services need to work togetherDesktop GUI applications
You’re deploying to cloud/KubernetesVery low-level system programming
Team members use different OSLearning basic programming concepts
You need to scale horizontallySingle-developer simple projects

Advice: Start using Docker early in your career. It’s becoming a fundamental skill, and hands-on experience is the best teacher.

Docker Workflow Summary

flowchart TD
    START[Start Development]
    WRITE[Write Application Code]
    DOCKERFILE[Create Dockerfile]
    BUILD[docker build]
    TEST[Test Locally]
    COMPOSE[Set up docker-compose.yml]
    MULTITEST[Test Multi-Container Setup]
    TAG[Tag Image]
    PUSH[Push to Registry]
    DEPLOY[Deploy to Production]
    
    START --> WRITE
    WRITE --> DOCKERFILE
    DOCKERFILE --> BUILD
    BUILD --> TEST
    TEST --> |Issues| WRITE
    TEST --> |Works| COMPOSE
    COMPOSE --> MULTITEST
    MULTITEST --> |Issues| COMPOSE
    MULTITEST --> |Works| TAG
    TAG --> PUSH
    PUSH --> DEPLOY

Conclusion

Docker revolutionizes how we build, ship, and run applications. As a Junior engineer, mastering Docker gives you:

  • Consistency across all environments
  • Faster development with ready-to-use containers
  • Better collaboration with standardized setups
  • Production-ready skills for modern DevOps practices

Key takeaways:

  1. Containers are lightweight, portable, and isolated
  2. Images are blueprints; containers are running instances
  3. Dockerfiles define how to build images
  4. Docker Compose orchestrates multi-container applications
  5. Always use volumes for data persistence
  6. Follow best practices: small images, non-root users, specific tags
  7. Clean up regularly to avoid disk space issues

Next steps:

  • Build a Dockerfile for your current project
  • Set up Docker Compose for a full-stack application
  • Practice essential Docker commands daily
  • Explore Docker Hub for official images
  • Learn about Docker networking and volumes in depth

Remember: Docker has a learning curve, but the investment pays off quickly. Start with simple examples, experiment often, and don’t be afraid to break things in your local environment, that’s how you learn!

Happy containerizing! 🐳

This post is licensed under CC BY 4.0 by the author.