This lab demonstrates various Docker concepts including:
- Different types of Docker image builds
- Image loading and unloading
- Pushing images to Docker Hub
- Using Docker CLI and Docker Compose
- Application Overview
- Docker Image Types
- Architecture Overview
- Docker CLI Commands
- Docker Compose
- Complete Cleanup
- Use Cases and Scenarios
- Troubleshooting
The example includes a simple Node.js Express application that serves a "Hello from Docker!" message.
┌─────────────────────────┐
│ │
│ Express Web Server │
│ │
│ GET / → "Hello from │
│ Docker!" │
│ │
└─────────────────────────┘
This lab includes three different Dockerfile examples, each demonstrating important Docker concepts:
A standard single-stage build process that creates a simple container with all build dependencies included.
┌────────────────────────────────────┐
│ node:18-alpine │
│ ┌──────────────────────────────┐ │
│ │ Application Code │ │
│ │ │ │
│ │ ┌────────────────────────┐ │ │
│ │ │ npm dependencies │ │ │
│ │ │ (production + dev) │ │ │
│ │ └────────────────────────┘ │ │
│ └──────────────────────────────┘ │
└────────────────────────────────────┘
Key features:
- Simple to understand and build
- Contains all dependencies in a single layer
- Larger image size due to included dev dependencies and build tools
Optimizes the image by separating build and runtime environments to create smaller, more efficient containers.
┌────────────────────────┐ ┌────────────────────────┐
│ BUILD STAGE │ │ PRODUCTION STAGE │
│ │ │ │
│ node:18-alpine │ │ node:18-alpine │
│ ┌──────────────────┐ │ │ ┌──────────────────┐ │
│ │ App code │ │ │ │ App code │ │
│ │ │ │ │ │ (only .js files) │ │
│ │ npm install │ │ │ │ │ │
│ │ (all deps) │ │ │ │ node_modules │ │
│ └──────────────────┘ │ │ │ (prod only) │ │
│ │ │ └──────────────────┘ │
└───────────┬────────────┘ └────────────────────────┘
│ ▲
│ │
└──────────────────────────────┘
Copy artifacts
Key features:
- Smaller final image size
- Excludes build tools and development dependencies
- More secure with fewer unnecessary packages
- Best for production environments
Security-focused approach running the app as a non-root user to limit potential container escape vulnerabilities.
┌──────────────────────────────────────────┐
│ node:18-alpine │
│ ┌────────────────────────────────────┐ │
│ │ Custom non-root user: appuser │ │
│ │ │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ Application code │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────┐ │ │ │
│ │ │ │ npm dependencies │ │ │ │
│ │ │ └────────────────────────────┘ │ │ │
│ │ └────────────────────────────────┘ │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
Key features:
- Enhanced security through principle of least privilege
- Application runs as non-root user
- File permissions explicitly set
- Prevents privilege escalation in case of vulnerabilities
The following diagram illustrates how the different components interact in this lab:
┌─────────────────┐
│ Docker Hub │
│ │
│ Remote Registry│
└────────┬────────┘
│
│ Push/Pull
│
┌──────────────────────────┐ ┌────────▼────────┐
│ Local Development │ │ Docker Images │
│ Environment │ │ │
│ │ Build │ ┌──────────────┐ │
│ ┌─────────────────────┐ ├───────►│ │Basic Image │ │
│ │Application Source │ │ │ └──────────────┘ │ ┌──────────────────┐
│ │ │ │ │ ┌──────────────┐ │ │ Docker Containers│
│ │server.js │ │ │ │Multi-stage │ │ │ │
│ │package.json │ │ │ │Image │ │ │ ┌──────────────┐ │
│ └─────────────────────┘ │ │ └──────────────┘ │ │ │App Container │ │
│ │ │ ┌──────────────┐ │ │ │Port: 3000 │ │
│ │ │ │Non-root │ │ │ └──────────────┘ │
│ │ │ │Image │ │ │ ┌──────────────┐ │
└──────────────────────────┘ │ └──────────────┘ ├─────►│ │Multistage │ │
└──────────────────┘ │ │Port: 3001 │ │
│ │ └──────────────┘ │
│ Save/Load │ ┌──────────────┐ │
│ │ │Non-root │ │
┌────────▼────────┐ │ │Port: 3002 │ │
│ .tar Image File │ │ └──────────────┘ │
│ │ └──────────────────┘
└─────────────────┘
- Source code is built into Docker images using different Dockerfile strategies
- Images can be:
- Run locally as containers
- Saved to tar files for transfer
- Pushed to Docker Hub for distribution
- Each container exposes the application on different ports
# Build the basic image
docker build -t docker-example:basic -f Dockerfile.basic .
# Build the multi-stage image
docker build -t docker-example:multistage -f Dockerfile.multistage .
# Build the non-root user image
docker build -t docker-example:nonroot -f Dockerfile.nonroot .# Run the basic image
docker run -p 3000:3000 docker-example:basic
# Run the multi-stage image
docker run -p 3001:3000 docker-example:multistage
# Run the non-root image
docker run -p 3002:3000 docker-example:nonroot# List all images
docker images
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a# Stop a container
docker stop <container_id>
# Remove a container
docker rm <container_id>
# Stop and remove in one command
docker rm -f <container_id># Save an image to a tar file
docker save -o docker-example-basic.tar docker-example:basicImage saving process:
┌──────────────────┐ ┌───────────────────┐
│ Docker Image │ │ Filesystem │
│ │ │ │
│ docker-example: │ │ docker-example- │
│ basic │────►│ basic.tar │
│ │ │ │
└──────────────────┘ └───────────────────┘
docker save
# Load an image from a tar file
docker load -i docker-example-basic.tarImage loading process:
┌───────────────────┐ ┌──────────────────┐
│ Filesystem │ │ Docker Image │
│ │ │ │
│ docker-example- │ │ docker-example: │
│ basic.tar │────►│ basic │
│ │ │ │
└───────────────────┘ └──────────────────┘
docker load
# Tag with your Docker Hub username
docker tag docker-example:basic username/docker-example:latest# Log in to Docker Hub
docker login
# Push the image
docker push username/docker-example:latest
# Log out when done
docker logoutPush process visualization:
┌─────────────────┐ ┌───────────────────┐ ┌─────────────────────┐
│ Local Image │ │ │ │ Docker Hub Registry │
│ │ │ docker push │ │ │
│ username/docker-│────►│ │────►│ username/docker- │
│ example:latest │ │ │ │ example:latest │
│ │ │ │ │ │
└─────────────────┘ └───────────────────┘ └─────────────────────┘
# Inspect image details
docker inspect docker-example:basic
# Remove an image
docker rmi docker-example:basic
# Remove unused images
docker image prune
# Remove all unused images (not just dangling ones)
docker image prune -aDocker Compose allows you to define and run multi-container Docker applications.
# Start all services
docker compose up
# Start in detached mode (background)
docker compose up -d
# Stop all services
docker compose down
# Build and start
docker compose up --build
# View logs
docker compose logsThe compose file includes three services using the different Dockerfile types:
app- Uses the basic Dockerfile (port 3000)app-multistage- Uses the multi-stage Dockerfile (port 3001)app-nonroot- Uses the non-root Dockerfile (port 3002)
Visualizing the Docker Compose setup:
┌─────────────────────────────────────────────────────────────────┐
│ Docker Compose Environment │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│
│ │ app │ │ app-multistage │ │ app-nonroot ││
│ │ │ │ │ │ ││
│ │ Dockerfile.basic│ │ Dockerfile. │ │ Dockerfile. ││
│ │ │ │ multistage │ │ nonroot ││
│ │ │ │ │ │ ││
│ │ Port: 3000 │ │ Port: 3001 │ │ Port: 3002 ││
│ └─────────────────┘ └─────────────────┘ └─────────────────┘│
│ │
│ Shared Docker Network │
└─────────────────────────────────────────────────────────────────┘
│
│
▼
┌───────────────────────────────────────────────────────────────────┐
│ Host Machine │
│ │
│ localhost:3000 localhost:3001 localhost:3002 │
└───────────────────────────────────────────────────────────────────┘
To clean up all resources created by this lab, you can use the provided cleanup script:
# Make the script executable
chmod +x cleanup.sh
# Run the cleanup script
./cleanup.shThe cleanup script will:
- Stop and remove all running containers from docker-compose
- Remove any standalone containers from manual runs
- Remove all Docker images created in this lab
- Remove saved Docker image tar files
- Prune unused Docker resources
Cleanup process visualization:
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Running │ │ Docker │ │ Docker │
│ Containers │─────►│ Images │─────►│ Volumes/Networks │
│ │stop │ │remove│ │
└───────────────────┘ └───────────────────┘ └───────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌───────────────────────────────────────────────────────────────────────┐
│ │
│ ./cleanup.sh │
│ │
└───────────────────────────────────────────────────────────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Local .tar │ │ Tagged Images │ │ Unused Resources │
│ Files │ │ (Docker Hub) │ │ (cache, etc.) │
│ │ │ │ │ │
└───────────────────┘ └───────────────────┘ └───────────────────┘
For local development, you can use the basic Dockerfile with volumes for hot-reloading:
# Uncomment the volumes section in docker-compose.yml
# Then run:
docker compose up appChanges to your local code will be reflected in the container without rebuilding.
Local development workflow:
┌─────────────────────┐ ┌─────────────────────┐
│ Local Source Code │ │ Container │
│ │ │ │
│ Edit files │───────►│ App automatically │
│ │ Volume │ reloads │
│ │ Mount │ │
└─────────────────────┘ └─────────────────────┘
For production, the multi-stage build creates a smaller, optimized image:
docker build -t myapp:prod -f Dockerfile.multistage .
docker run -d -p 80:3000 myapp:prodBenefits of multi-stage build for production:
- Smaller image size (fewer layers)
- Only production dependencies included
- Faster deployment times
- Reduced attack surface
The non-root Dockerfile improves security by not running the application as root:
docker build -t myapp:secure -f Dockerfile.nonroot .
docker run -d -p 80:3000 myapp:secureSecurity benefits:
- Prevents privilege escalation
- Follows principle of least privilege
- Reduces impact of potential container breakout
- Complies with security best practices for containerized applications
Example GitHub Actions workflow to build and push your image:
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile.multistage
push: true
tags: username/docker-example:latestCI/CD workflow visualization:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Git Push │ │ GitHub │ │ Docker │ │ Docker Hub │
│ │────►│ Actions │────►│ Build │────►│ │
│ │ │ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
│
┌─────────────────────────────┐ │
│ Production Environment │ │
│ │ │
│ docker pull username/docker ◄──────────┘
│ -example:latest │
│ │
└─────────────────────────────┘
- Container exits immediately: Check your CMD instruction and ensure your application is properly configured
- Can't access the application: Verify port mappings and that the application is listening on the correct interface
- Permission denied errors: Use the non-root Dockerfile or check volume mount permissions
Common troubleshooting workflow:
┌───────────────────────────────────────────────────────────────────────┐
│ │
│ Issue Detection │
│ │
└───────────────┬───────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ │
│ Investigation │
│ • Check logs: docker logs <container_id> │
│ • Inspect container: docker inspect <container_id> │
│ • Access container shell: docker exec -it <container_id> /bin/sh │
│ │
└───────────────┬───────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ │
│ Resolution │
│ • Modify Dockerfile │
│ • Update application code │
│ • Adjust container configuration │
│ │
└───────────────────────────────────────────────────────────────────────┘