Lesson 1: Deploy a Simple Web App 🚀¶
Deploy your first containerized web application (Node.js, Python, or Go) with Docker and Traefik.
Progress Indicator¶
Difficulty: Beginner | Time: 30-40 minutes
Learning Objectives¶
By the end of this lesson, you'll be able to:
- Create a Dockerfile for a web application
- Configure docker-compose.yml for deployment
- Deploy an application with Traefik routing
- Verify your deployment with health checks
- Troubleshoot common deployment issues
Prerequisites Checklist¶
- Completed Module 1: Getting Started
- Basic understanding of Docker (what containers are)
- Access to a web application (Node.js, Python, or Go)
- SSH access to your EgyGeeks server
- Docker installed locally (for testing)
- A text editor for modifying files
- Familiarity with basic command-line commands
Using our example?
If you don't have a web app ready, we'll use a simple example throughout this lesson.
Part 1: Understanding the Deployment Process¶
What Happens When You Deploy¶
Step 1: Dockerfile Your application code is packaged into a Docker image (blueprint)
Step 2: docker-compose.yml Defines how to run the container and connect to Traefik
Step 3: Server Container starts on the server and becomes accessible
Step 4: Traefik Reverse proxy routes incoming requests to your container
Why Docker?¶
- Reproducibility: Same setup everywhere (laptop, staging, production)
- Isolation: App doesn't interfere with others on server
- Simplicity: Package everything your app needs
- Scaling: Easy to run multiple instances
Why Traefik?¶
- SSL/TLS: Automatic Let's Encrypt certificates
- Routing: Multiple apps on one server
- Health Checks: Only routes to healthy containers
- Simple: Just add labels to docker-compose.yml
Part 2: Choose Your Framework¶
Select the tab for your application type:
Example Node.js Application¶
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({ message: 'Hello from Node.js!', timestamp: new Date() });
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
package.json¶
{
"name": "node-app",
"version": "1.0.0",
"description": "Simple Node.js app",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
Dockerfile¶
# Use official Node.js runtime as base image
FROM node:18-alpine
# Set working directory in container
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy application code
COPY . .
# Expose port (documentation only, doesn't actually open it)
EXPOSE 3000
# Health check - tells Traefik if container is healthy
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget --quiet --tries=1 --spider http://127.0.0.1:3000/health || exit 1
# Start application
CMD ["npm", "start"]
Key Points: - node:18-alpine is a lightweight base image (~170MB) - Install dependencies in Docker, not before - HEALTHCHECK uses /health endpoint to verify app is running - Use 127.0.0.1 not localhost in Alpine images
Example Python Flask Application¶
from flask import Flask, jsonify
import os
from datetime import datetime
app = Flask(__name__)
PORT = int(os.environ.get('PORT', 5000))
@app.route('/')
def hello():
return jsonify({
'message': 'Hello from Flask!',
'timestamp': datetime.now().isoformat()
})
@app.route('/health')
def health():
return jsonify({'status': 'healthy'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=PORT, debug=False)
requirements.txt¶
Dockerfile¶
# Use official Python runtime as base image
FROM python:3.11-alpine
# Set working directory in container
WORKDIR /app
# Copy requirements
COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose port (documentation only)
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget --quiet --tries=1 --spider http://127.0.0.1:5000/health || exit 1
# Start application
CMD ["python", "app.py"]
Key Points: - python:3.11-alpine is lightweight (~50MB) - Use --no-cache-dir to keep layer small - Flask app must bind to 0.0.0.0 to be accessible in container - HEALTHCHECK endpoint returns 200 OK
Example Go Application¶
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"time"
)
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
})
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Hello from Go!",
"timestamp": time.Now(),
})
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/", helloHandler)
http.HandleFunc("/health", healthHandler)
log.Printf("Server running on port %s\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
go.mod¶
Dockerfile¶
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
# Runtime stage - even smaller
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/app .
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget --quiet --tries=1 --spider http://127.0.0.1:8080/health || exit 1
CMD ["./app"]
Key Points: - Multi-stage build keeps final image tiny (~10MB) - First stage builds the binary - Second stage only includes the binary (no compiler) - Go compiles to a single binary, very efficient
Part 3: Create docker-compose.yml¶
Use the template from our templates directory. Replace my-app with your app name, 3000 with your port.
version: '3.8'
services:
app:
build: .
container_name: my-app
restart: unless-stopped
environment:
- PORT=3000
labels:
# Enable Traefik for this container
- traefik.enable=true
# Router configuration - requests matching this Host go to this service
- traefik.http.routers.my-app.rule=Host(`app.egygeeks.com`)
# Use HTTPS entry point
- traefik.http.routers.my-app.entrypoints=websecure
# Use Let's Encrypt for SSL certificates
- traefik.http.routers.my-app.tls.certresolver=letsencrypt
# Service configuration - the actual port your app listens on
- traefik.http.services.my-app.loadbalancer.server.port=3000
networks:
- traefik_public
networks:
traefik_public:
external: true
Understanding the Configuration¶
| Configuration | Explanation |
|---|---|
build: . | Build image from Dockerfile in current directory |
container_name: my-app | Name of the running container |
restart: unless-stopped | Automatically restart if it crashes (except manual stop) |
environment | Pass env variables to container |
traefik.enable=true | Tell Traefik to manage this container |
Host(app.egygeeks.com) | Route requests for this domain to this container |
websecure | Use HTTPS (port 443) |
letsencrypt | Use Let's Encrypt for free SSL certificates |
loadbalancer.server.port=3000 | Port your app actually listens on inside container |
traefik_public | Network that Traefik manages |
Customization Steps¶
Step 1: Replace App Name
Find and replace my-app in 3 places:
container_name: my-app # ← Here
traefik.http.routers.my-app.rule=... # ← Here
traefik.http.services.my-app.loadbalancer... # ← Here
Step 2: Set Your Domain
Domain Planning
You can use: - Your actual domain: myapp.com - Subdomain: api.myapp.com - EgyGeeks subdomain: myapp.egygeeks.com
You'll configure DNS later in Module 3.
Step 3: Update Port
Replace 3000 with your application's port:
Part 4: Deployment Steps¶
Step 1: Prepare Your Repository¶
Your project should look like:
my-app/
├── Dockerfile ← Created in Part 2
├── docker-compose.yml ← Created in Part 3
├── app.js ← Your application code
├── package.json ← Dependencies (Node.js example)
└── .gitignore ← Don't commit node_modules
Step 2: Test Locally (Optional but Recommended)¶
Before deploying, test on your computer:
# Build the image
docker compose build
# Start the container
docker compose up -d
# Check if container is running
docker compose ps
# View logs
docker compose logs -f app
# Test the application
curl http://localhost:3000
# Test health check
curl http://localhost:3000/health
# Stop when done
docker compose down
If build fails
Common issues: - Dependencies not installed (check Dockerfile COPY/RUN order) - Wrong working directory - Missing environment variables
Check logs with docker compose logs -f app
Step 3: Push to GitHub¶
Create a new repository and push your code:
git init
git add .
git commit -m "Initial commit: Deploy simple web app"
git branch -M main
git remote add origin https://github.com/yourusername/my-app.git
git push -u origin main
Step 4: Deploy to Server¶
SSH into your EgyGeeks server:
Clone your repository:
cd /opt/apps # Standard app directory
git clone https://github.com/yourusername/my-app.git
cd my-app
Start the container:
Verify it's running:
Part 5: Verification Commands¶
Check Container Status¶
# List running containers
docker compose ps
# Expected output shows:
# - Container running
# - Status "healthy"
View Application Logs¶
# Real-time logs
docker compose logs -f app
# Last 50 lines
docker compose logs --tail 50 app
# Logs from specific time
docker compose logs --since 5m app
Verify Health Check¶
# Check if health check passes
docker compose ps
# Status should show "healthy" or "Up (healthy)"
# Run health check manually inside container
docker exec my-app wget -O- http://127.0.0.1:3000/health
Test Application Access¶
# From server, test application
curl http://127.0.0.1:3000
curl http://127.0.0.1:3000/health
# Check that Traefik sees the container as healthy
docker exec traefik curl http://my-app:3000/health
Monitor Resource Usage¶
# Check CPU and memory usage
docker stats my-app
# View disk usage
docker compose exec app du -sh /app
Part 6: Common Mistakes & Solutions¶
⚠️ Mistake 1: Using localhost in Health Check¶
Wrong:
Why it fails: In Alpine containers, localhost may not resolve.
Correct:
⚠️ Mistake 2: No HEALTHCHECK in Dockerfile¶
Wrong:
Why it fails: Traefik will route to unhealthy containers, causing 502 errors.
Correct:
FROM node:18-alpine
COPY . .
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl http://127.0.0.1:3000/health || exit 1
CMD ["npm", "start"]
⚠️ Mistake 3: Port Mismatch¶
Wrong:
labels:
- traefik.http.services.my-app.loadbalancer.server.port=3000
# But app actually listens on port 5000
Correct:
Check: Run docker logs my-app to see what port your app reports.
⚠️ Mistake 4: Binding to localhost Only¶
Wrong:
Why it fails: Application only accepts connections from inside container, Traefik can't reach it.
Correct:
⚠️ Mistake 5: Domain Not Configured¶
Wrong:
Then accessing https://example.com (without subdomain).
Solution: Either: 1. Use correct domain in browser that matches docker-compose.yml 2. Configure DNS to point domain to server 3. Use /etc/hosts for local testing: 127.0.0.1 app.egygeeks.com
Part 7: AI Prompts for Help¶
Creating a Custom Dockerfile¶
Ask AI to Generate Dockerfile
I'm deploying a [Node.js/Python/Go] application with these requirements:
Tech stack:
- Framework: [Express/FastAPI/Gin/etc]
- Port: [your port number]
- Environment variables needed: DATABASE_URL, API_KEY, PORT
- Has a /health endpoint
Generate a production-ready Dockerfile that:
1. Uses multi-stage build for smaller image size
2. Runs as non-root user for security
3. Includes health check
4. Has proper .dockerignore
My package.json/requirements.txt/go.mod:
[paste your dependency file here]
Debugging Container Issues¶
Container Won't Start?
My Docker container exits immediately after starting.
Container name: [your-container-name]
Output from `docker ps -a`:
[paste output showing container status]
Output from `docker logs [container-name]`:
[paste full error logs]
My Dockerfile:
[paste Dockerfile content]
My docker-compose.yml:
[paste docker-compose.yml content]
What's causing the crash and how do I fix it?
Optimizing Docker Images¶
Get Optimization Tips
Real Examples¶
Check out complete working examples in the egygeeks-docs repository:
- Node.js/Express:
/examples/nodejs-app/ - Python/FastAPI:
/examples/python-app/ - Go:
/examples/go-app/
Clone the repo and explore:
Troubleshooting¶
Container won't start¶
Diagnosis:
Common Causes: - Missing dependencies (check Dockerfile RUN commands) - Wrong working directory (WORKDIR) - Port already in use - Permission issues with code files
Application starts but Traefik shows 502 error¶
Diagnosis:
Common Causes: - Health check failing (app actually not running correctly) - Port mismatch between docker-compose.yml and app - App binding to localhost instead of 0.0.0.0
Health check fails¶
Diagnosis:
Common Causes: - App doesn't have /health endpoint - Using localhost instead of 127.0.0.1 (Alpine issue) - App hasn't started yet (try increasing --start-period)
Domain shows certificate error¶
Solution: Wait 30-60 seconds for Let's Encrypt to provision the certificate. Check Traefik logs:
What's Next¶
After completing this lesson:
- ✅ You've deployed a simple containerized web application
- ✅ You understand Docker and Traefik basics
- ✅ You can verify deployments with health checks
Next Step: Lesson 2: App with Database →
Learn how to deploy applications that need persistent data storage with PostgreSQL or MongoDB.
Need Help?¶
Within This Module¶
- Review the "Common Mistakes" section above
- Check your docker-compose.yml against the template
- Compare your Dockerfile to examples in your framework
External Resources¶
Quick Reference¶
Key Commands:
docker compose build # Build image
docker compose up -d # Start containers
docker compose logs -f app # View logs
docker compose ps # Check status
docker compose down # Stop containers
File Checklist: - [ ] Dockerfile (created in Part 2) - [ ] docker-compose.yml (created in Part 3) - [ ] Application code (your code) - [ ] requirements.txt / package.json / go.mod (dependencies) - [ ] .gitignore (to exclude node_modules, pycache, etc.)
← Back to Module Overview Continue to Lesson 2: App with Database →