Building and Containerizing Applications
Learn how to create containerized applications using Docker best practices. Build a sample web application and create optimized, secure container images ready for production deployment.
Overview #
In this step, you’ll create a sample web application and learn how to containerize it using Docker best practices. You’ll build production-ready images that are secure, efficient, and optimized for cloud deployment.
Step 1: Create a Sample Web Application #
Let’s create a simple Node.js web application that demonstrates key containerization concepts.
Create the Application Structure #
mkdir cloud-native-app && cd cloud-native-app
Create package.json #
{
"name": "cloud-native-app",
"version": "1.0.0",
"description": "A sample cloud-native web application",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.2",
"helmet": "^7.0.0",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.5.0"
},
"keywords": ["docker", "kubernetes", "cloud-native"],
"author": "Your Name",
"license": "MIT"
}
Create the Server Application #
Create server.js
:
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3000;
// Security middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || '1.0.0'
});
});
// Main application endpoint
app.get('/', (req, res) => {
res.json({
message: 'Welcome to Cloud-Native Development!',
environment: process.env.NODE_ENV || 'development',
container: process.env.HOSTNAME || 'localhost'
});
});
// API endpoint
app.get('/api/status', (req, res) => {
res.json({
api: 'running',
uptime: process.uptime(),
memory: process.memoryUsage(),
cpu: process.cpuUsage()
});
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
});
Create Tests #
Create server.test.js
:
const request = require('supertest');
const app = require('./server');
describe('Cloud Native App', () => {
test('Health check endpoint', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.body.status).toBe('healthy');
});
test('Root endpoint', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
expect(response.body.message).toContain('Cloud-Native');
});
test('API status endpoint', async () => {
const response = await request(app).get('/api/status');
expect(response.status).toBe(200);
expect(response.body.api).toBe('running');
});
});
Step 2: Create an Optimized Dockerfile #
Create a production-ready Dockerfile
using multi-stage builds:
# Multi-stage build for optimized production image
FROM node:18-alpine AS builder
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Production stage
FROM node:18-alpine AS production
# Create non-root user for security
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# Set working directory
WORKDIR /app
# Copy built application from builder stage
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nextjs:nodejs . .
# Expose port
EXPOSE 3000
# Switch to non-root user
USER nextjs
# Set environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Start the application
CMD ["npm", "start"]
Create .dockerignore #
node_modules
npm-debug.log*
.git
.gitignore
README.md
.env
.nyc_output
coverage
.docker
Dockerfile*
.dockerignore
Step 3: Build and Test Your Container #
Build the Container Image #
# Build the image
docker build -t cloud-native-app:v1.0.0 .
# Build with build arguments for customization
docker build \
--build-arg NODE_ENV=production \
--build-arg APP_VERSION=1.0.0 \
-t cloud-native-app:v1.0.0 .
Test the Container Locally #
# Run the container
docker run -d \
--name cloud-app \
--publish 3000:3000 \
--env NODE_ENV=production \
cloud-native-app:v1.0.0
# Test the application
curl http://localhost:3000
curl http://localhost:3000/health
curl http://localhost:3000/api/status
# View logs
docker logs cloud-app
# Stop and remove the container
docker stop cloud-app && docker remove cloud-app
Step 4: Optimize Your Images #
Analyze Image Size #
# Check image size
docker images cloud-native-app:v1.0.0
# Analyze image layers
docker history cloud-native-app:v1.0.0
Use Docker Buildx for Advanced Builds #
# Create a new builder instance
docker buildx create --name cloud-builder --use
# Build multi-platform images
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag cloud-native-app:v1.0.0 \
--push .
Step 5: Container Security Best Practices #
Scan for Vulnerabilities #
# Scan the image for vulnerabilities
docker scout cves cloud-native-app:v1.0.0
# Generate a detailed security report
docker scout recommendations cloud-native-app:v1.0.0
Security Checklist #
- Non-root user: Container runs as non-root user
- Minimal base image: Using Alpine Linux for smaller attack surface
- Security headers: Application includes security middleware
- Health checks: Container includes health check endpoints
- Secrets management: No secrets embedded in image
- Resource limits: Container respects memory and CPU limits
Step 6: Create Docker Compose for Development #
Create docker-compose.yml
for local development:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DEBUG=app:*
volumes:
- .:/app
- /app/node_modules
command: npm run dev
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7-alpine
ports:
- "6379:6379"
restart: unless-stopped
networks:
default:
name: cloud-native-network
Test with Docker Compose #
# Start all services
docker-compose up -d
# View logs
docker-compose logs -f
# Scale the application
docker-compose up -d --scale app=3
# Stop all services
docker-compose down
Testing and Validation #
Performance Testing #
# Install Apache Bench for load testing
# macOS: brew install httpie
# Ubuntu: sudo apt install apache2-utils
# Run basic load test
ab -n 1000 -c 10 http://localhost:3000/
Container Resource Usage #
# Monitor container resource usage
docker stats cloud-app
# Check container processes
docker exec cloud-app ps aux
Best Practices Summary #
Dockerfile Optimization #
- Use multi-stage builds to reduce final image size
- Copy files in order of change frequency to leverage layer caching
- Run as non-root user for security
- Use specific image tags instead of ’latest'
- Include health checks for container orchestration
Security Best Practices #
- Scan images regularly for vulnerabilities
- Use minimal base images (Alpine, distroless)
- Don’t embed secrets in images
- Sign and verify images in production
- Implement proper logging and monitoring
What’s Next? #
Your application is now containerized and ready for deployment! In the next step, you’ll learn how to deploy your containerized application to Kubernetes using Helm charts, implement monitoring and logging, and set up a complete production deployment pipeline.
You’ll also explore advanced Kubernetes concepts like:
- Pod autoscaling
- Service mesh integration
- GitOps deployment workflows
- Production monitoring and alerting
Complete these tasks to finish this step:
- Multi-stage builds reduce final image size and improve security
- Running containers as non-root users enhances security
- Health checks enable better container orchestration
- Layer caching optimizes build performance
- Security scanning should be part of your build process
Docker build fails with permission errors
Ensure Docker daemon is running and your user is in the docker group (Linux) or Docker Desktop is properly installed (Windows/Mac).
Application not accessible in container
Check that the application binds to 0.0.0.0 rather than localhost, and ensure ports are properly exposed.
Large image sizes
Use multi-stage builds, choose minimal base images like Alpine, and review what files are being copied into the image.