🔥 0
0
Lesson 5 of 10 20 min +125 XP

Building the Product API Image

Let's build a production-quality Docker image for an e-commerce Product API. We'll cover image optimization, proper tagging, and security best practices.

Project Setup

Create the complete product API structure:

shopflow-product-api/
├── Dockerfile
├── .dockerignore
├── package.json
├── src/
│   ├── server.js
│   ├── routes/
│   │   └── products.js
│   └── data/
│       └── products.json
package.json:
{
  "name": "shopflow-product-api",
  "version": "1.0.0",
  "description": "Product catalog API for ShopFlow e-commerce",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "dev": "node --watch src/server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "cors": "^2.8.5",
    "helmet": "^7.1.0"
  }
}
src/data/products.json:
[
  {
    "id": "SKU001",
    "name": "Wireless Bluetooth Headphones",
    "description": "Premium noise-cancelling headphones with 30hr battery",
    "price": 79.99,
    "category": "electronics",
    "stock": 45,
    "images": ["headphones-1.jpg", "headphones-2.jpg"]
  },
  {
    "id": "SKU002",
    "name": "USB-C Fast Charging Cable",
    "description": "2m braided cable with 100W power delivery",
    "price": 12.99,
    "category": "accessories",
    "stock": 200,
    "images": ["cable-1.jpg"]
  },
  {
    "id": "SKU003",
    "name": "Ergonomic Laptop Stand",
    "description": "Adjustable aluminum stand with cooling vents",
    "price": 49.99,
    "category": "accessories",
    "stock": 30,
    "images": ["stand-1.jpg", "stand-2.jpg"]
  },
  {
    "id": "SKU004",
    "name": "Mechanical Keyboard",
    "description": "RGB backlit with Cherry MX switches",
    "price": 129.99,
    "category": "electronics",
    "stock": 25,
    "images": ["keyboard-1.jpg"]
  },
  {
    "id": "SKU005",
    "name": "Wireless Mouse",
    "description": "Ergonomic design with 6 programmable buttons",
    "price": 34.99,
    "category": "electronics",
    "stock": 80,
    "images": ["mouse-1.jpg"]
  }
]
src/routes/products.js:
const express = require('express');
const router = express.Router();
const products = require('../data/products.json');

// GET all products
router.get('/', (req, res) => {
  const { category, minPrice, maxPrice } = req.query;

  let filtered = [...products];

  if (category) {
    filtered = filtered.filter(p => p.category === category);
  }
  if (minPrice) {
    filtered = filtered.filter(p => p.price >= parseFloat(minPrice));
  }
  if (maxPrice) {
    filtered = filtered.filter(p => p.price <= parseFloat(maxPrice));
  }

  res.json({
    count: filtered.length,
    products: filtered
  });
});

// GET single product
router.get('/:id', (req, res) => {
  const product = products.find(p => p.id === req.params.id);

  if (!product) {
    return res.status(404).json({ error: 'Product not found' });
  }

  res.json(product);
});

// GET product stock
router.get('/:id/stock', (req, res) => {
  const product = products.find(p => p.id === req.params.id);

  if (!product) {
    return res.status(404).json({ error: 'Product not found' });
  }

  res.json({
    id: product.id,
    name: product.name,
    stock: product.stock,
    available: product.stock > 0
  });
});

module.exports = router;
src/server.js:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const productRoutes = require('./routes/products');

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.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

// API routes
app.use('/api/products', productRoutes);

// 404 handler
app.use((req, res) => {
  res.status(404).json({ error: 'Endpoint not found' });
});

app.listen(PORT, () => {
  console.log(`Product API running on port ${PORT}`);
  console.log(`Health check: http://localhost:${PORT}/health`);
});

Production Dockerfile

.dockerignore:
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
*.md
.DS_Store
coverage
.nyc_output
Dockerfile:
# Use specific version for reproducibility
FROM node:20.10-alpine AS base

# Create non-root user for security
RUN addgroup -S shopflow && adduser -S shopflow -G shopflow

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && npm cache clean --force

# Copy application code
COPY src/ ./src/

# Change ownership to non-root user
RUN chown -R shopflow:shopflow /app

# Switch to non-root user
USER shopflow

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"

# Start the application
CMD ["node", "src/server.js"]

Build the Image

docker build -t shopflow/product-api:1.0 .
Output:
[+] Building 12.5s (12/12) FINISHED
 => [base 1/7] FROM node:20.10-alpine
 => [base 2/7] RUN addgroup -S shopflow && adduser -S shopflow -G shopflow
 => [base 3/7] WORKDIR /app
 => [base 4/7] COPY package*.json ./
 => [base 5/7] RUN npm ci --only=production && npm cache clean --force
 => [base 6/7] COPY src/ ./src/
 => [base 7/7] RUN chown -R shopflow:shopflow /app
 => exporting to image
 => => naming to docker.io/shopflow/product-api:1.0

Tagging Strategy

Use semantic versioning for your images:

# Build with version tag
docker build -t shopflow/product-api:1.0.0 .

# Also tag as latest
docker tag shopflow/product-api:1.0.0 shopflow/product-api:latest

# Tag with git commit for traceability
docker tag shopflow/product-api:1.0.0 shopflow/product-api:abc123f

List your images:

docker images shopflow/product-api
Output:
REPOSITORY              TAG       IMAGE ID       SIZE
shopflow/product-api    1.0.0     a1b2c3d4e5f6   125MB
shopflow/product-api    latest    a1b2c3d4e5f6   125MB
shopflow/product-api    abc123f   a1b2c3d4e5f6   125MB

Run and Test

# Run the container
docker run -d \
  --name product-api \
  -p 3000:3000 \
  shopflow/product-api:1.0.0

# Check it's running
docker ps

# View logs
docker logs product-api
Test the API:
# Health check
curl http://localhost:3000/health

# Get all products
curl http://localhost:3000/api/products

# Filter by category
curl "http://localhost:3000/api/products?category=electronics"

# Get specific product
curl http://localhost:3000/api/products/SKU001

# Check stock
curl http://localhost:3000/api/products/SKU001/stock

Checking Image Size

docker images shopflow/product-api:1.0.0

Compare image sizes:

Base ImageApproximate Size
node:20~350MB
node:20-slim~200MB
node:20-alpine~125MB

Alpine saves significant space - faster to push, pull, and deploy.

Security Best Practices Applied

Our Dockerfile includes several security measures:

1. Non-root user:
RUN addgroup -S shopflow && adduser -S shopflow -G shopflow
USER shopflow
2. Minimal base image:
FROM node:20.10-alpine
3. Production dependencies only:
RUN npm ci --only=production
4. Health check:
HEALTHCHECK --interval=30s --timeout=3s \
  CMD node -e "require('http').get('http://localhost:3000/health', ...)"
5. Clean npm cache:
RUN npm ci --only=production && npm cache clean --force

Inspect the Image

See what's in your image:

# View image layers
docker history shopflow/product-api:1.0.0

# Inspect image metadata
docker inspect shopflow/product-api:1.0.0

# Check the user it runs as
docker inspect --format='{{.Config.User}}' shopflow/product-api:1.0.0

Clean Up

# Stop and remove container
docker rm -f product-api

# Remove image
docker rmi shopflow/product-api:1.0.0

# Remove all unused images
docker image prune

Key Takeaways

  • Specific versions in FROM for reproducibility
  • Non-root user for security
  • Alpine images for smaller size
  • npm ci instead of npm install for consistency
  • HEALTHCHECK for container monitoring
  • Semantic versioning for image tags
  • .dockerignore to exclude unnecessary files

Next, let's connect multiple containers together with Docker networking!

🧠 Quick Quiz

Test your understanding of this lesson.

1

What does 'docker build -t shopflow/product-api:1.0 .' do?

2

Why use alpine-based images in production?

3

What does the USER instruction do?

Writing a Dockerfile