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 Image | Approximate 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!