Welcome to Part 3! In Part 1 we learned about Traefik, and in Part 2 we got it running. Now comes the fun part: deploying actual applications.
By the end of this tutorial, you'll have deployed multiple applications, each with automatic HTTPS, and you'll understand exactly how Traefik's service discovery works.
What We'll Deploy Today
We'll build three progressively complex applications:
- Simple static website (HTML/CSS) - Learn the basics
- Next.js application - Handle Node.js-specific requirements
- Full-stack app (React frontend + Express API + PostgreSQL) - Multiple services
Time estimate: 2-3 hours
Understanding Docker Labels: The Key to Traefik
Before we start deploying, let's understand how Traefik discovers and routes to your applications.
How Traditional Reverse Proxies Work
With Nginx, you'd write a config file:
server {
server_name myapp.example.com;
location / {
proxy_pass http://localhost:3000;
}
}
Then reload Nginx. If the app moves to a different port, you update the config and reload again.
How Traefik Works
With Traefik, you add labels to your Docker container:
labels:
- "traefik.http.routers.myapp.rule=Host(`myapp.example.com`)"
- "traefik.http.services.myapp.loadbalancer.server.port=3000"
Traefik watches Docker and automatically creates routes when it sees these labels. No reload needed.
The Anatomy of Traefik Labels
Let's break down the label structure:
traefik.http.routers.{service-name}.rule=Host(`{domain}`)
│ │ │ │ │
│ │ │ │ └─ Routing rule (Host, Path, etc.)
│ │ │ └─ Unique name for this service (you choose)
│ │ └─ Type of router (http, tcp, udp)
│ └─ Router component
└─ Traefik label prefix
Example labels explained:
# Enable Traefik for this container
- "traefik.enable=true"
# Which network to use (if container is on multiple networks)
- "traefik.docker.network=traefik-network"
# Routing rule (when to send traffic here)
- "traefik.http.routers.blog.rule=Host(`blog.example.com`)"
# Which entrypoint (websecure = HTTPS port 443)
- "traefik.http.routers.blog.entrypoints=websecure"
# Enable TLS and specify certificate resolver
- "traefik.http.routers.blog.tls.certresolver=letsencrypt"
# Tell Traefik which port the app listens on INSIDE the container
- "traefik.http.services.blog.loadbalancer.server.port=3000"
Critical Concept: Internal vs External Ports
This is the #1 source of confusion for Traefik beginners.
Wrong way (won't work):
ports:
- "8080:3000"
labels:
- "traefik.http.services.myapp.loadbalancer.server.port=8080" # ❌
Right way:
# NO ports mapping needed!
labels:
- "traefik.http.services.myapp.loadbalancer.server.port=3000" # ✅
Why?
- Traefik talks to containers via Docker's internal network
- On that network, containers use their INTERNAL ports
- You're telling Traefik: "Connect to port 3000 inside the container"
- Traefik doesn't care about host ports (8080)
The ports: section is only needed when:
- You want direct access to the container (bypassing Traefik)
- You're debugging
- The service uses non-HTTP protocols
Project 1: Static Website
Let's deploy a simple HTML website to learn the basics.
Step 1: Create Project Directory
mkdir -p ~/projects/static-site
cd ~/projects/static-site
Step 2: Create a Simple Website
# Create HTML file
cat > index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My First Traefik Site</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
h1 { font-size: 3em; margin-bottom: 0; }
p { font-size: 1.2em; opacity: 0.9; }
.info {
background: rgba(255,255,255,0.1);
padding: 20px;
border-radius: 8px;
margin-top: 30px;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 3px;
}
</style>
</head>
<body>
<h1>🚀 It Works!</h1>
<p>Your first Traefik-powered website is live with automatic HTTPS!</p>
<div class="info">
<h3>What's happening here?</h3>
<ul>
<li>This HTML is served by an Nginx container</li>
<li>Traefik detected the container automatically</li>
<li>Let's Encrypt provided a free SSL certificate</li>
<li>HTTP requests redirect to HTTPS automatically</li>
<li>All from a few Docker labels!</li>
</ul>
<p><strong>Server time:</strong> <span id="time"></span></p>
</div>
<script>
setInterval(() => {
document.getElementById('time').textContent = new Date().toLocaleString();
}, 1000);
</script>
</body>
</html>
EOF
Step 3: Create Dockerfile
We'll use Nginx to serve our static files:
cat > Dockerfile << 'EOF'
FROM nginx:alpine
# Copy our HTML file
COPY index.html /usr/share/nginx/html/
# Nginx listens on port 80 by default
EXPOSE 80
# Use default nginx.conf
EOF
Step 4: Create docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
website:
container_name: static-site
build: .
restart: unless-stopped
networks:
- traefik-network
labels:
# Enable Traefik
- "traefik.enable=true"
# Specify the network (required if container is on multiple networks)
- "traefik.docker.network=traefik-network"
# Routing rule - CHANGE THIS to your domain!
- "traefik.http.routers.static-site.rule=Host(`site.lans.cloud`)"
# Use HTTPS entrypoint
- "traefik.http.routers.static-site.entrypoints=websecure"
# Enable automatic SSL
- "traefik.http.routers.static-site.tls.certresolver=letsencrypt"
# Tell Traefik the internal port (Nginx uses 80)
- "traefik.http.services.static-site.loadbalancer.server.port=80"
networks:
traefik-network:
external: true
EOF
Important: Change site.lans.cloud to your actual domain (e.g., site.yourdomain.com)
Step 5: Deploy!
docker compose up -d
What happens:
- Docker builds the image (first time only)
- Starts the container
- Container joins traefik-network
- Traefik sees the labels
- Traefik creates a route for
site.yourdomain.com - Traefik requests SSL certificate from Let's Encrypt
- Site is live with HTTPS!
All in about 30 seconds.
Step 6: Access Your Site
Open https://site.yourdomain.com in your browser.
You should see your website with:
- ✅ Valid SSL certificate (green padlock)
- ✅ Automatic HTTP → HTTPS redirect
- ✅ Your content displaying correctly
Step 7: Check the Traefik Dashboard
Visit https://traefik.yourdomain.com
You should now see:
- Routers:
static-site@dockerwith your domain rule - Services:
static-site@dockerpointing to your container - HTTP: Green status indicators
Understanding What Just Happened
Let's trace a request through the system:
User types: https://site.yourdomain.com
│
▼
1. DNS resolves to your server IP (203.0.113.45)
│
▼
2. Request hits port 443 (HTTPS)
│
▼
3. Traefik receives the request
│
▼
4. Traefik looks at the Host header: "site.yourdomain.com"
│
▼
5. Traefik matches this to router rule: Host(`site.yourdomain.com`)
│
▼
6. Router points to service: static-site
│
▼
7. Service configuration says: connect to port 80 inside container
│
▼
8. Traefik forwards request to static-site container:80
│
▼
9. Nginx inside container serves index.html
│
▼
10. Traefik returns response to user with SSL encryption
Project 2: Next.js Application
Now let's deploy a Next.js app. This is more complex because Node.js apps require special network configuration.
The Critical Next.js Requirement
Next.js apps must listen on 0.0.0.0 (all interfaces), not localhost or a specific IP.
Why?
In Docker, each container has its own network interface with its own IP. When Next.js binds to localhost, it's only accessible from inside the container itself. Traefik, connecting from the Docker network, can't reach it.
Solution: Set HOSTNAME=0.0.0.0 environment variable.
Step 1: Create Project
mkdir -p ~/projects/nextjs-blog
cd ~/projects/nextjs-blog
Step 2: Create Next.js App Files
Create package.json:
cat > package.json << 'EOF'
{
"name": "nextjs-traefik-demo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
EOF
Create app/page.tsx:
mkdir -p app
cat > app/page.tsx << 'EOF'
export default function Home() {
return (
<main style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontFamily: 'system-ui'
}}>
<div style={{ textAlign: 'center', maxWidth: '600px', padding: '20px' }}>
<h1 style={{ fontSize: '3em', marginBottom: '0.5em' }}>
⚡️ Next.js + Traefik
</h1>
<p style={{ fontSize: '1.2em', opacity: 0.9 }}>
This Next.js application was deployed with Traefik automatic service discovery
</p>
<div style={{
background: 'rgba(255,255,255,0.1)',
padding: '20px',
borderRadius: '8px',
marginTop: '30px'
}}>
<h3>Features Working:</h3>
<ul style={{ textAlign: 'left', lineHeight: '2' }}>
<li>✅ Server-side rendering</li>
<li>✅ Automatic HTTPS</li>
<li>✅ Fast Refresh (in dev mode)</li>
<li>✅ Zero config needed</li>
</ul>
</div>
</div>
</main>
);
}
EOF
Create app/layout.tsx:
cat > app/layout.tsx << 'EOF'
export const metadata = {
title: 'Next.js + Traefik Demo',
description: 'Next.js app deployed with Traefik',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
EOF
Create next.config.js:
cat > next.config.js << 'EOF'
/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable standalone output for smaller Docker images
output: 'standalone',
}
module.exports = nextConfig
EOF
Step 3: Create Optimized Dockerfile
cat > Dockerfile << 'EOF'
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Rebuild source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
# CRITICAL: Must listen on 0.0.0.0 for Docker networking
ENV HOSTNAME "0.0.0.0"
ENV PORT 3000
CMD ["node", "server.js"]
EOF
Step 4: Create docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
nextjs-app:
container_name: nextjs-blog
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
environment:
# CRITICAL: Next.js must listen on 0.0.0.0
- HOSTNAME=0.0.0.0
- PORT=3000
- NODE_ENV=production
networks:
- traefik-network
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-network"
# CHANGE THIS to your domain!
- "traefik.http.routers.nextjs-blog.rule=Host(`blog.lans.cloud`)"
- "traefik.http.routers.nextjs-blog.entrypoints=websecure"
- "traefik.http.routers.nextjs-blog.tls.certresolver=letsencrypt"
# Next.js default port is 3000
- "traefik.http.services.nextjs-blog.loadbalancer.server.port=3000"
networks:
traefik-network:
external: true
EOF
Change blog.lans.cloud to your domain!
Step 5: Deploy
docker compose up -d --build
This will take a few minutes because Next.js needs to build.
Watch the build:
docker logs -f nextjs-blog
Wait until you see:
ready - started server on 0.0.0.0:3000
Step 6: Access Your Next.js App
Visit https://blog.yourdomain.com
You should see your Next.js app with HTTPS!
Common Next.js Issues and Solutions
Issue: 502 Bad Gateway
Symptom: Traefik shows 502 error
Cause: Next.js not listening on 0.0.0.0
Solution:
environment:
- HOSTNAME=0.0.0.0 # Must be set!
Issue: "Cannot find module 'next'"
Cause: Dependencies not installed correctly
Solution:
docker compose down
docker compose build --no-cache
docker compose up -d
Issue: Slow Page Loads
Cause: Using dev mode in production
Solution: Make sure NODE_ENV=production is set
Project 3: Full-Stack Application
Now let's deploy a complete application with:
- React frontend
- Express API backend
- PostgreSQL database
- Internal communication between services
Architecture
Internet → Traefik → Frontend (React) → Port 3000 → HTTPS
↓
Backend (Express) → Port 8000 → HTTPS
↓
Database (PostgreSQL) → Port 5432 → Internal only
Only frontend and backend are exposed to the internet. Database is internal.
Step 1: Create Project Structure
mkdir -p ~/projects/fullstack-app/{frontend,backend}
cd ~/projects/fullstack-app
Step 2: Create Backend (Express API)
cd ~/projects/fullstack-app/backend
# Create package.json
cat > package.json << 'EOF'
{
"name": "backend-api",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.0",
"cors": "^2.8.5"
}
}
EOF
# Create index.js
cat > index.js << 'EOF'
const express = require('express');
const { Pool } = require('pg');
const cors = require('cors');
const app = express();
const port = 8000;
// Database connection
const pool = new Pool({
host: process.env.DB_HOST || 'db',
port: 5432,
database: process.env.DB_NAME || 'appdb',
user: process.env.DB_USER || 'appuser',
password: process.env.DB_PASSWORD || 'changeme',
});
// Middleware
app.use(cors());
app.use(express.json());
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// Get items
app.get('/api/items', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM items ORDER BY created_at DESC');
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// Create item
app.post('/api/items', async (req, res) => {
const { name, description } = req.body;
try {
const result = await pool.query(
'INSERT INTO items (name, description) VALUES ($1, $2) RETURNING *',
[name, description]
);
res.json(result.rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Database error' });
}
});
// Initialize database
async function initDB() {
try {
await pool.query(`
CREATE TABLE IF NOT EXISTS items (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('Database initialized');
} catch (err) {
console.error('Database initialization error:', err);
}
}
app.listen(port, '0.0.0.0', () => {
console.log(`API server listening on http://0.0.0.0:${port}`);
initDB();
});
EOF
# Create Dockerfile
cat > Dockerfile << 'EOF'
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8000
CMD ["npm", "start"]
EOF
Step 3: Create Frontend (React)
cd ~/projects/fullstack-app/frontend
# Create package.json
cat > package.json << 'EOF'
{
"name": "frontend",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.0",
"vite": "^5.0.0"
}
}
EOF
# Create vite.config.js
cat > vite.config.js << 'EOF'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 3000
},
preview: {
host: '0.0.0.0',
port: 3000
}
})
EOF
# Create index.html
cat > index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Full Stack Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
EOF
# Create src directory
mkdir -p src
# Create src/main.jsx
cat > src/main.jsx << 'EOF'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
EOF
# Create src/App.jsx
cat > src/App.jsx << 'EOF'
import { useState, useEffect } from 'react';
// Use environment variable or default to relative URL
const API_URL = import.meta.env.VITE_API_URL || 'https://api.lans.cloud';
function App() {
const [items, setItems] = useState([]);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
useEffect(() => {
fetchItems();
}, []);
const fetchItems = async () => {
try {
const res = await fetch(`${API_URL}/api/items`);
const data = await res.json();
setItems(data);
} catch (err) {
console.error('Error fetching items:', err);
}
};
const addItem = async (e) => {
e.preventDefault();
try {
await fetch(`${API_URL}/api/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description })
});
setName('');
setDescription('');
fetchItems();
} catch (err) {
console.error('Error adding item:', err);
}
};
return (
<div style={{ maxWidth: '800px', margin: '50px auto', padding: '20px', fontFamily: 'system-ui' }}>
<h1 style={{ fontSize: '2.5em', marginBottom: '20px' }}>📦 Full Stack Demo</h1>
<div style={{ background: '#f5f5f5', padding: '20px', borderRadius: '8px', marginBottom: '30px' }}>
<h2>Add New Item</h2>
<form onSubmit={addItem}>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ width: '100%', padding: '10px', marginBottom: '10px', fontSize: '1em' }}
required
/>
<textarea
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
style={{ width: '100%', padding: '10px', marginBottom: '10px', fontSize: '1em', minHeight: '80px' }}
/>
<button type="submit" style={{ padding: '10px 20px', fontSize: '1em', cursor: 'pointer' }}>
Add Item
</button>
</form>
</div>
<div>
<h2>Items ({items.length})</h2>
{items.map(item => (
<div key={item.id} style={{ background: 'white', padding: '15px', marginBottom: '10px', borderRadius: '8px', border: '1px solid #ddd' }}>
<h3 style={{ margin: '0 0 10px 0' }}>{item.name}</h3>
<p style={{ margin: 0, color: '#666' }}>{item.description || 'No description'}</p>
<small style={{ color: '#999' }}>{new Date(item.created_at).toLocaleString()}</small>
</div>
))}
</div>
</div>
);
}
export default App;
EOF
# Create Dockerfile
cat > Dockerfile << 'EOF'
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# Build argument for API URL
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build
# Production server
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
EOF
Important: In src/App.jsx, change https://api.lans.cloud to your actual API domain!
Step 4: Create docker-compose.yml
cd ~/projects/fullstack-app
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
# PostgreSQL Database (internal only, not exposed via Traefik)
db:
container_name: fullstack-db
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: changeme
POSTGRES_DB: appdb
volumes:
- db-data:/var/lib/postgresql/data
networks:
- internal
# Express API Backend
backend:
container_name: fullstack-api
build: ./backend
restart: unless-stopped
environment:
- DB_HOST=db
- DB_NAME=appdb
- DB_USER=appuser
- DB_PASSWORD=changeme
depends_on:
- db
networks:
- traefik-network
- internal
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-network"
# CHANGE THIS to your domain!
- "traefik.http.routers.fullstack-api.rule=Host(`api.lans.cloud`)"
- "traefik.http.routers.fullstack-api.entrypoints=websecure"
- "traefik.http.routers.fullstack-api.tls.certresolver=letsencrypt"
- "traefik.http.services.fullstack-api.loadbalancer.server.port=8000"
# CORS middleware
- "traefik.http.middlewares.api-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS"
- "traefik.http.middlewares.api-cors.headers.accesscontrolalloworiginlist=https://app.lans.cloud"
- "traefik.http.middlewares.api-cors.headers.accesscontrolallowheaders=Content-Type,Authorization"
- "traefik.http.middlewares.api-cors.headers.accesscontrolallowcredentials=true"
- "traefik.http.routers.fullstack-api.middlewares=api-cors"
# React Frontend
frontend:
container_name: fullstack-frontend
build:
context: ./frontend
args:
# CHANGE THIS to match your API domain!
VITE_API_URL: https://api.lans.cloud
restart: unless-stopped
networks:
- traefik-network
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-network"
# CHANGE THIS to your domain!
- "traefik.http.routers.fullstack-frontend.rule=Host(`app.lans.cloud`)"
- "traefik.http.routers.fullstack-frontend.entrypoints=websecure"
- "traefik.http.routers.fullstack-frontend.tls.certresolver=letsencrypt"
- "traefik.http.services.fullstack-frontend.loadbalancer.server.port=80"
networks:
traefik-network:
external: true
internal:
driver: bridge
volumes:
db-data:
EOF
Change these domains:
api.lans.cloud→ your API domainapp.lans.cloud→ your frontend domain- Also update in CORS and build args!
Step 5: Deploy the Full Stack
docker compose up -d --build
This will take several minutes. Three containers are building.
Monitor progress:
docker compose logs -f
Step 6: Test the Application
- Visit frontend:
https://app.yourdomain.com - Add an item using the form
- Verify it appears in the list
- Check API directly:
https://api.yourdomain.com/health
You now have a complete full-stack application running with:
- ✅ React frontend with HTTPS
- ✅ Express API with HTTPS
- ✅ PostgreSQL database (internal)
- ✅ CORS configured
- ✅ All services talking to each other
Understanding the Network Architecture
traefik-network
│
┌──────────────────┼──────────────────┐
│ │ │
Frontend Backend Traefik
│ │
│ │
│ internal network
│ │
│ ├─────── DB
│ │
Key points:
- Frontend and Backend are on
traefik-network(so Traefik can reach them) - Backend and DB are on
internalnetwork (so they can talk) - Frontend is NOT on internal (doesn't need direct DB access)
- DB is NOT on traefik-network (not exposed publicly)
Troubleshooting Common Issues
Frontend Can't Reach API
Symptoms: Network errors in browser console
Causes:
- Wrong API URL in frontend code
- CORS not configured
- API not running
Debug:
# Check API is accessible
curl https://api.yourdomain.com/health
# Check CORS headers
curl -I https://api.yourdomain.com/api/items
Fix CORS: The labels in docker-compose.yml handle this, but make sure the accesscontrolalloworiginlist matches your frontend domain.
Backend Can't Reach Database
Symptoms: "Database connection error" in logs
Causes:
- DB not on same network as backend
- Wrong DB credentials
- DB not ready when backend starts
Debug:
# Check backend logs
docker logs fullstack-api
# Check if DB is running
docker exec fullstack-db psql -U appuser -d appdb -c "\dt"
Fix: Add depends_on and health check:
backend:
depends_on:
db:
condition: service_healthy
Port Conflicts
Symptoms: "bind: address already in use"
Cause: Another service using the same port
Debug:
sudo lsof -i :80
sudo lsof -i :443
Fix: Make sure only Traefik uses these ports (no nginx, apache, etc.)
Best Practices for Production
1. Use Environment Variables
Don't hardcode secrets:
environment:
- DB_PASSWORD=${DB_PASSWORD} # Read from .env file
Create .env file:
DB_PASSWORD=your-secure-password-here
Add to .gitignore:
.env
2. Health Checks
Add health checks to ensure services are ready:
services:
backend:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
3. Resource Limits
Prevent containers from consuming all resources:
services:
backend:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
4. Logging
Centralize logs:
services:
backend:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
5. Database Backups
Create a backup script:
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
docker exec fullstack-db pg_dump -U appuser appdb > backup_$DATE.sql
# Upload to S3 or backup service
What's Next?
In Part 4: Advanced Features, we'll cover:
- Rate limiting to prevent abuse
- Custom authentication middleware
- WebSocket support (for real-time features)
- Advanced CORS configuration
- Serving multiple apps from one container
- Custom error pages
- Monitoring and metrics
Summary
You've learned how to:
- ✅ Deploy static websites with Traefik
- ✅ Deploy Next.js applications (with critical
HOSTNAMEconfig) - ✅ Deploy full-stack applications with multiple services
- ✅ Configure Docker networks for security
- ✅ Set up CORS for API access
- ✅ Understand Traefik labels in depth
- ✅ Debug common deployment issues
Key Takeaways:
- Traefik labels define routing, not config files
- Use INTERNAL container ports in labels
- Node.js apps must listen on
0.0.0.0 - Separate public and private networks for security
- CORS must match your actual domains
Continue to Part 4 to level up your Traefik skills with advanced features!