Day 12: Exposing Ports and Accessing Services

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand the difference between EXPOSE and port publishing
  • Master various port mapping techniques and patterns
  • Configure load balancing and reverse proxy setups
  • Implement security best practices for exposed services

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Imagine your containerized application is like a house with many rooms, each serving a different purpose. How do visitors (users) know which door to knock on to get the service they need? And how do you ensure they can only access the rooms they’re supposed to? Today we’ll learn Docker’s port management system!

Lesson Overview

  • Duration: 50-60 minutes
  • Prerequisites: Completion of Day 11 (Docker Networking), understanding of basic networking concepts
  • Materials Needed: Docker installed, web browser, multiple terminal windows

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Port Concepts - EXPOSE vs publish, and when to use each
  2. Port Mapping Patterns - Different ways to expose container services
  3. Service Discovery - Making services accessible and discoverable
  4. Load Balancing - Distributing traffic across multiple containers
  5. Security Considerations - Safe port exposure practices

Success Criteria:

By the end of this session, you should be able to design and implement proper port exposure strategies for any containerized application, from simple web apps to complex microservices architectures.

3. Foundation Knowledge (15-20 minutes)

Understanding Port Exposure

Docker containers run in isolated network namespaces, which means their ports are not accessible from the host by default. To make services running in containers accessible, we need to explicitly expose and publish ports.

Real-World Analogy: Think of a container as an apartment building. Each apartment (service) has its own internal phone number (port). To call from outside, you need the building’s main number (host port) and know which apartment extension (container port) to dial.

Key Concepts:

  • EXPOSE instruction: Documents which ports a container listens on (metadata only)
  • Port Publishing: Makes container ports accessible from the host
  • Port Binding: Maps host ports to container ports
  • Service Port: The port a service actually listens on inside the container

EXPOSE vs Port Publishing

EXPOSE Instruction (Dockerfile)

# This is documentation only - doesn't actually expose ports
FROM nginx:alpine
EXPOSE 80
EXPOSE 443

The EXPOSE instruction:

  • Documents intended port usage
  • Provides metadata for orchestration tools
  • Does NOT publish ports to the host
  • Is used by docker run -P (publish all exposed ports)

Port Publishing (Runtime)

# Actually makes ports accessible from host
docker run -p 8080:80 nginx        # Host port 8080 → Container port 80
docker run -p 80:80 nginx          # Host port 80 → Container port 80
docker run -P nginx                # Publish all EXPOSED ports to random host ports

Port Mapping Types

1. Specific Port Mapping

# Map specific host port to container port
docker run -p 8080:80 nginx

2. Random Port Assignment

# Let Docker choose random host port
docker run -p 80 nginx              # Maps to random host port
docker run -P nginx                 # Maps all EXPOSED ports

3. IP-Specific Binding

# Bind to specific host interface
docker run -p 127.0.0.1:8080:80 nginx    # Only localhost
docker run -p 192.168.1.100:8080:80 nginx # Specific IP

4. Protocol-Specific Binding

# Specify protocol (default is TCP)
docker run -p 8080:80/tcp nginx     # TCP (default)
docker run -p 53:53/udp bind9       # UDP

Key Terminology

  • Host Port: Port on the Docker host machine
  • Container Port: Port inside the container where service listens
  • Port Binding: The mapping between host and container ports
  • Published Port: A container port that’s accessible from outside
  • Service Discovery: Method for finding and connecting to services

4. Detailed Explanation and Examples (25-30 minutes)

Basic Port Mapping Patterns

Single Service Exposure

# Simple web server
docker run -d --name web-server -p 8080:80 nginx

# Check port mapping
docker port web-server
# Output: 80/tcp -> 0.0.0.0:8080

# Access service
curl http://localhost:8080

Multiple Port Exposure

# Application with multiple services
docker run -d \
  --name full-stack-app \
  -p 3000:3000 \
  -p 5432:5432 \
  -p 6379:6379 \
  myapp:latest

# Check all port mappings
docker port full-stack-app

Dynamic Port Assignment

# Let Docker choose ports
docker run -d --name dynamic-app -P nginx

# Find assigned ports
docker port dynamic-app
# Output: 80/tcp -> 0.0.0.0:32768

# Get port programmatically
HOST_PORT=$(docker port dynamic-app 80/tcp | cut -d: -f2)
echo "Service available on port: $HOST_PORT"

Advanced Port Management

IP Address Binding

# Bind to localhost only (more secure)
docker run -d --name local-only -p 127.0.0.1:8080:80 nginx

# Bind to specific interface
docker run -d --name external-facing -p 0.0.0.0:8080:80 nginx

# Bind to multiple interfaces
docker run -d --name multi-interface \
  -p 127.0.0.1:8080:80 \
  -p 192.168.1.100:8081:80 \
  nginx

Port Range Mapping

# Map range of ports
docker run -d --name port-range -p 8000-8010:8000-8010 myapp

# Useful for applications that use multiple ports

Service Discovery Patterns

Environment-Based Discovery

# Run database with known port
docker run -d --name postgres-db \
  -p 5432:5432 \
  -e POSTGRES_PASSWORD=secret \
  postgres:13

# Run application with database connection
docker run -d --name web-app \
  -p 3000:3000 \
  -e DATABASE_URL=postgresql://postgres:secret@host.docker.internal:5432/mydb \
  myapp:latest

Network-Based Discovery (Preferred)

# Create custom network
docker network create app-network

# Run database (no port mapping needed for internal communication)
docker run -d --name postgres-db \
  --network app-network \
  -e POSTGRES_PASSWORD=secret \
  postgres:13

# Run application (only expose web port)
docker run -d --name web-app \
  --network app-network \
  -p 3000:3000 \
  -e DATABASE_URL=postgresql://postgres:secret@postgres-db:5432/mydb \
  myapp:latest

Load Balancing with Port Mapping

Manual Load Balancing

# Run multiple instances of the same service
docker run -d --name web1 -p 8081:80 nginx
docker run -d --name web2 -p 8082:80 nginx
docker run -d --name web3 -p 8083:80 nginx

# Use external load balancer (nginx, HAProxy, etc.)

Simple Round-Robin with nginx

# Create nginx config for load balancing
cat > nginx.conf << EOF
upstream backend {
    server host.docker.internal:8081;
    server host.docker.internal:8082;
    server host.docker.internal:8083;
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
    }
}
EOF

# Run load balancer
docker run -d --name load-balancer \
  -p 80:80 \
  -v $(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf \
  nginx

Common Misconceptions

  1. Misconception: “EXPOSE in Dockerfile automatically makes ports accessible” Reality: EXPOSE is documentation only; you must use -p or -P to actually publish ports

  2. Misconception: “Containers on the same network need port mapping to communicate” Reality: Containers on the same custom network can communicate directly using container names

  3. Misconception: “Published ports are always accessible from anywhere” Reality: You can bind ports to specific interfaces for security

5. Live Coding Demonstration (20-25 minutes)

Example 1: Progressive Service Exposure

# Start with a simple internal service
docker network create demo-network
docker run -d --name internal-api \
  --network demo-network \
  node:18-alpine \
  sh -c "
    echo 'const express = require(\"express\");
    const app = express();
    app.get(\"/\", (req, res) => res.json({service: \"internal\", port: 3000}));
    app.get(\"/health\", (req, res) => res.json({status: \"healthy\"}));
    app.listen(3000, () => console.log(\"Internal API running\"));' > app.js
    && npm init -y
    && npm install express
    && node app.js
  "

# Test internal connectivity
docker run --rm --network demo-network alpine \
  sh -c "apk add curl && curl http://internal-api:3000"

# Now expose the service to host
docker run -d --name external-api \
  --network demo-network \
  -p 8080:3000 \
  node:18-alpine \
  sh -c "
    echo 'const express = require(\"express\");
    const app = express();
    app.get(\"/\", (req, res) => res.json({service: \"external\", port: 3000, host_port: 8080}));
    app.listen(3000, () => console.log(\"External API running\"));' > app.js
    && npm init -y
    && npm install express
    && node app.js
  "

# Test external access
curl http://localhost:8080

Explanation:

  • First container is only accessible within the Docker network
  • Second container is accessible both internally and from the host
  • This pattern allows for secure, layered service architectures

Example 2: Multi-Service Application with Smart Port Management

# Create application network
docker network create microservices

# Database (internal only - no port exposure)
docker run -d --name database \
  --network microservices \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=myapp \
  postgres:13

# API Service (internal + external access)
docker run -d --name api-service \
  --network microservices \
  -p 3000:3000 \
  node:18-alpine \
  sh -c "
    echo 'const express = require(\"express\");
    const { Pool } = require(\"pg\");
    const app = express();

    const pool = new Pool({
      host: \"database\",
      database: \"myapp\",
      user: \"postgres\",
      password: \"secret\",
      port: 5432,
    });

    app.get(\"/\", (req, res) => res.json({service: \"API\", version: \"1.0\"}));
    
    app.get(\"/users\", async (req, res) => {
      try {
        const result = await pool.query(\"SELECT NOW() as current_time\");
        res.json({users: [], db_time: result.rows[0].current_time});
      } catch (err) {
        res.status(500).json({error: err.message});
      }
    });

    app.listen(3000, () => console.log(\"API service running on port 3000\"));' > app.js
    && npm init -y
    && npm install express pg
    && node app.js
  "

# Frontend (external access only)
docker run -d --name frontend \
  --network microservices \
  -p 80:80 \
  nginx:alpine

# Create simple frontend page
docker exec frontend sh -c "
  echo '<!DOCTYPE html>
<html>
<head><title>Microservices Demo</title></head>
<body>
  <h1>Microservices Application</h1>
  <button onclick=\"fetchData()\">Get API Data</button>
  <pre id=\"result\"></pre>
  <script>
    async function fetchData() {
      try {
        const response = await fetch(\"http://localhost:3000/users\");
        const data = await response.json();
        document.getElementById(\"result\").textContent = JSON.stringify(data, null, 2);
      } catch (err) {
        document.getElementById(\"result\").textContent = \"Error: \" + err.message;
      }
    }
  </script>
</body>
</html>' > /usr/share/nginx/html/index.html
"

# Test the complete setup
echo "Frontend: http://localhost:80"
echo "API: http://localhost:3000"
echo "Database: Only accessible internally"

Key Points:

  • Database has no exposed ports (maximum security)
  • API is accessible both internally and externally
  • Frontend only exposes HTTP port
  • Each service can only access what it needs

Example 3: Dynamic Service Discovery and Health Checks

# Create monitoring network
docker network create monitoring

# Run service with health check endpoint
docker run -d --name app-with-health \
  --network monitoring \
  -p 8080:3000 \
  --health-cmd="curl -f http://localhost:3000/health || exit 1" \
  --health-interval=30s \
  --health-timeout=10s \
  --health-retries=3 \
  node:18-alpine \
  sh -c "
    echo 'const express = require(\"express\");
    const app = express();
    
    let healthy = true;
    
    app.get(\"/\", (req, res) => res.json({service: \"health-demo\"}));
    
    app.get(\"/health\", (req, res) => {
      if (healthy) {
        res.json({status: \"healthy\", timestamp: new Date().toISOString()});
      } else {
        res.status(500).json({status: \"unhealthy\"});
      }
    });
    
    app.post(\"/toggle-health\", (req, res) => {
      healthy = !healthy;
      res.json({status: healthy ? \"healthy\" : \"unhealthy\"});
    });
    
    app.listen(3000, () => console.log(\"Service with health checks running\"));' > app.js
    && npm init -y
    && npm install express
    && node app.js
  "

# Monitor health status
docker inspect app-with-health | grep -A 5 "Health"

# Test health endpoint
curl http://localhost:8080/health

# Simulate health failure
curl -X POST http://localhost:8080/toggle-health

Port Management Debugging

# Check what ports are in use
docker ps --format "table {{.Names}}\t{{.Ports}}"

# Find process using specific port
lsof -i :8080  # macOS/Linux
netstat -ano | findstr :8080  # Windows

# Check Docker port mappings
docker port <container-name>

# Inspect container network configuration
docker inspect <container> | grep -A 20 "NetworkSettings"

6. Hands-On Practice (30-40 minutes)

Exercise 1: E-commerce Microservices (20 minutes)

Objective: Design and implement port exposure for a multi-service e-commerce application

Requirements: Create the following services with appropriate port exposure:

  • Product API (port 3001) - Exposed to host
  • User API (port 3002) - Exposed to host
  • Order API (port 3003) - Exposed to host
  • Database (port 5432) - Internal only
  • Redis Cache (port 6379) - Internal only
  • API Gateway (port 80) - Main entry point, routes to other APIs

Instructions:

  1. Create a custom network for the services
  2. Implement each service with proper port configuration
  3. Set up API Gateway to route requests to appropriate services
  4. Ensure database and cache are not directly accessible from host
  5. Test inter-service communication and external access

Starter Code for API Gateway:

// gateway.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// Route to different services
app.use('/api/products', createProxyMiddleware({
  target: 'http://product-api:3001',
  changeOrigin: true
}));

app.use('/api/users', createProxyMiddleware({
  target: 'http://user-api:3002', 
  changeOrigin: true
}));

app.use('/api/orders', createProxyMiddleware({
  target: 'http://order-api:3003',
  changeOrigin: true
}));

app.get('/', (req, res) => {
  res.json({
    message: 'API Gateway',
    endpoints: ['/api/products', '/api/users', '/api/orders']
  });
});

app.listen(80, () => console.log('API Gateway running on port 80'));

Exercise 2: Security-Focused Port Configuration (10 minutes)

Objective: Implement secure port exposure patterns

Challenge: Set up a web application with these security requirements:

  • Admin interface only accessible from localhost
  • Public API accessible from any IP
  • Database connections only from application containers
  • Monitoring endpoint on separate port with restricted access

Security Patterns to Implement:

# Admin interface - localhost only
docker run -d --name admin-panel \
  -p 127.0.0.1:8443:443 \
  admin-app

# Public API - all interfaces  
docker run -d --name public-api \
  -p 443:443 \
  public-app

# Monitoring - specific IP only
docker run -d --name monitoring \
  -p 192.168.1.10:9090:9090 \
  monitoring-app

Exercise 3: Load Balancer Configuration (10 minutes)

Objective: Create a load-balanced service setup

Requirements:

  1. Run 3 instances of a web service on different ports
  2. Configure an nginx load balancer to distribute traffic
  3. Implement health checks to remove unhealthy instances
  4. Test failover behavior

nginx Configuration Template:

upstream backend_pool {
    server host.docker.internal:8081 max_fails=2 fail_timeout=30s;
    server host.docker.internal:8082 max_fails=2 fail_timeout=30s;
    server host.docker.internal:8083 max_fails=2 fail_timeout=30s;
}

server {
    listen 80;
    
    location / {
        proxy_pass http://backend_pool;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # Health check
        proxy_connect_timeout 5s;
        proxy_send_timeout 5s;
        proxy_read_timeout 5s;
    }
    
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Port already in use error Cause: Another process or container is using the host port Solution: Find and stop the conflicting process or use a different port
# Find what's using the port
lsof -i :8080
netstat -tlnp | grep 8080

# Kill the process
kill -9 <PID>

# Or use a different port
docker run -p 8081:80 nginx
  1. Problem: Cannot access service from outside host Cause: Binding to localhost only when external access is needed Solution: Bind to all interfaces (0.0.0.0)
# ❌ Only accessible from localhost
docker run -p 127.0.0.1:8080:80 nginx

# ✅ Accessible from any IP
docker run -p 8080:80 nginx
# or explicitly
docker run -p 0.0.0.0:8080:80 nginx
  1. Problem: Service responds but with wrong content Cause: Port mapping to wrong container port Solution: Verify the actual port your service listens on
# Check what port the service actually uses
docker exec <container> netstat -tlnp
docker exec <container> ss -tlnp

# Correct port mapping
docker run -p 8080:3000 myapp  # If app listens on 3000, not 80
  1. Problem: SSL/TLS certificate errors with proxies Cause: Incorrect proxy headers or certificate configuration Solution: Configure proper proxy headers and SSL termination
# Proper SSL proxy configuration
server {
    listen 443 ssl;
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Debugging Port Issues

# Check container port mappings
docker port <container-name>

# Verify service is listening inside container
docker exec <container> netstat -tlnp

# Test connectivity from inside container
docker exec <container> curl localhost:<port>

# Test connectivity from host
curl localhost:<host-port>

# Check Docker daemon logs
docker logs <container-name>

# Inspect container network configuration
docker inspect <container> | grep -A 20 "NetworkSettings"

Security Troubleshooting

# Check firewall rules (Linux)
iptables -L

# Verify Docker's iptables rules
iptables -t nat -L

# Test specific IP binding
telnet <ip> <port>

# Check SELinux context (if applicable)
getenforce
sestatus

8. Best Practices and Professional Tips (10 minutes)

Industry Standards

Port Management Strategy

# Use consistent port patterns
# Development: 3000-3099
# Staging: 4000-4099  
# Production: 80, 443, specific ranges

# Document port usage
docker run -d --name web-app \
  -p 3000:3000 \
  --label service.port.web=3000 \
  --label service.port.admin=3001 \
  myapp

Security Best Practices

# Principle of least exposure
# Only expose ports that need external access
docker run -d --name secure-app \
  --network internal-net \
  -p 127.0.0.1:8080:80 \
  myapp

# Use reverse proxy for SSL termination
docker run -d --name ssl-proxy \
  -p 443:443 \
  -v ssl-certs:/etc/nginx/ssl \
  nginx-ssl-proxy

Production Configurations

# Health check integration
docker run -d --name production-app \
  -p 80:3000 \
  --health-cmd="curl -f http://localhost:3000/health" \
  --health-interval=30s \
  --restart=unless-stopped \
  myapp

# Resource limits with port exposure
docker run -d --name limited-app \
  -p 8080:80 \
  --memory=512m \
  --cpus=1.0 \
  myapp

Monitoring and Observability

# Expose metrics endpoints
docker run -d --name monitored-app \
  -p 3000:3000 \
  -p 9090:9090 \
  --label prometheus.scrape=true \
  --label prometheus.port=9090 \
  myapp

Performance Considerations

  • Minimize port mappings for internal services
  • Use host networking only when necessary for performance
  • Implement proper load balancing for high-traffic services
  • Monitor connection pooling and keep-alive settings

Next Steps in Your Learning Journey

  • Learn about Docker Compose service definitions
  • Explore Kubernetes service and ingress concepts
  • Study service mesh architectures (Istio, Linkerd)
  • Practice with API gateways and reverse proxies

9. Summary and Review (5 minutes)

Key Takeaways

  1. EXPOSE documents, -p publishes - Remember the difference between documentation and actual port binding
  2. Security through selective exposure - Only expose ports that need external access
  3. Use custom networks for internal communication - Avoid unnecessary port mappings between related services

Quick Review Questions

  1. What’s the difference between EXPOSE 80 in a Dockerfile and docker run -p 8080:80?
  2. How would you make a service accessible only from localhost?
  3. What’s the advantage of using custom networks over port mapping for service communication?

Answers:

  1. EXPOSE is documentation only; -p actually publishes the port to the host
  2. Use IP-specific binding: docker run -p 127.0.0.1:8080:80 myapp
  3. Custom networks enable name-based service discovery and don’t require port conflicts resolution

For Instructors

Teaching Tips

  • Use real-world scenarios to demonstrate port mapping needs
  • Show both working and failing configurations to highlight common mistakes
  • Demonstrate security implications with practical examples
  • Use network monitoring tools to visualize traffic flow

Assessment Ideas

  • Port mapping design challenges for different application architectures
  • Security audit exercises for exposed services
  • Load balancing configuration projects
  • Troubleshooting scenarios with port conflicts

Adaptation Notes

  • For beginners: Focus on basic port mapping concepts and common patterns
  • For advanced students: Introduce complex proxy configurations and security hardening
  • Online delivery: Use port scanning tools to demonstrate concepts
  • Time management: Hands-on exercises with multiple services can take longer than expected