Skip to content

Lesson 1: Deploy a Simple Web App 🚀

Deploy your first containerized web application (Node.js, Python, or Go) with Docker and Traefik.

Progress Indicator

Module 2 of 4: Deploying Apps
└─ Lesson 1 of 3: Simple Web App [Current]

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

Your Code → Docker Image → Container → Traefik → Internet

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

app.js
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

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

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

app.py
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

requirements.txt
Flask==2.3.3

Dockerfile

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

main.go
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

go.mod
module myapp

go 1.21

Dockerfile

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.

docker-compose.yml
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

- traefik.http.routers.my-app.rule=Host(`yourdomain.com`)

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:

- traefik.http.services.my-app.loadbalancer.server.port=3000

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

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:

ssh user@your-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:

docker compose up -d

Verify it's running:

docker compose ps
docker compose logs -f app

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:

HEALTHCHECK CMD curl http://localhost:3000/health || exit 1

Why it fails: In Alpine containers, localhost may not resolve.

Correct:

HEALTHCHECK CMD curl http://127.0.0.1:3000/health || exit 1

⚠️ Mistake 2: No HEALTHCHECK in Dockerfile

Wrong:

FROM node:18-alpine
COPY . .
CMD ["npm", "start"]

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:

labels:
  - traefik.http.services.my-app.loadbalancer.server.port=5000

Check: Run docker logs my-app to see what port your app reports.

⚠️ Mistake 4: Binding to localhost Only

Wrong:

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000)

Why it fails: Application only accepts connections from inside container, Traefik can't reach it.

Correct:

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

⚠️ Mistake 5: Domain Not Configured

Wrong:

- traefik.http.routers.my-app.rule=Host(`app.example.com`)

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

My Docker image is [X] MB and takes too long to build.

Current Dockerfile:
[paste your Dockerfile]

Application type: [Node.js/Python/Go]
Dependencies: [list main packages]

Help me:
1. Reduce image size
2. Speed up build time
3. Use multi-stage builds effectively
4. Remove unnecessary files

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:

git clone https://github.com/egygeeks/egygeeks-docs.git
cd egygeeks-docs/examples

Troubleshooting

Container won't start

Diagnosis:

docker compose logs -f app

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:

docker compose ps  # Check if status is "healthy"
docker exec traefik curl http://my-app:3000/health

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:

docker compose ps
docker exec my-app wget -O- http://127.0.0.1:3000/health

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:

docker logs traefik | grep "certificate"

What's Next

After completing this lesson:

  1. ✅ You've deployed a simple containerized web application
  2. ✅ You understand Docker and Traefik basics
  3. ✅ 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 →