Skip to main content

Overview

The Docker deployment provides a complete production-ready stack with:
  • MySQL 8 - Persistent database with full ACID compliance
  • Node.js + Express API - RESTful backend with JWT authentication
  • Nginx - High-performance web server for frontend
  • Adminer - Web-based database management tool
All services are orchestrated with Docker Compose for easy management and automatic health checks.
This deployment is recommended for production environments with multiple users, centralized data, and network access requirements.

Prerequisites

1

Install Docker and Docker Compose

# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Add user to docker group
sudo usermod -aG docker $USER

# Verify installation
docker --version
docker-compose --version
2

System Requirements

  • CPU: 2+ cores recommended
  • RAM: 2 GB minimum, 4 GB recommended
  • Disk: 10 GB free space
  • OS: Linux, Windows 10+, macOS 10.15+
3

Network Requirements

Ensure the following ports are available:
  • 3306 - MySQL database
  • 3001 - Node.js API
  • 8080 - Nginx web server
  • 8081 - Adminer (optional)

Quick Start

1

Build the Frontend

Before starting Docker, compile the React frontend:
npm install
npm run build
This creates the dist/ directory that Nginx will serve.
2

Start All Services

Launch the entire stack with a single command:
docker-compose up -d
The -d flag runs containers in detached mode (background).
3

Verify Services

Check that all containers are running:
docker-compose ps
Expected output:
NAME                  STATUS         PORTS
aptiv-scrap-db        Up (healthy)   0.0.0.0:3306->3306/tcp
aptiv-scrap-api       Up             0.0.0.0:3001->3001/tcp
aptiv-scrap-web       Up             0.0.0.0:8080->80/tcp
aptiv-adminer         Up             0.0.0.0:8081->8080/tcp
4

Access the Application

Open in your browser:
  • Frontend: http://localhost:8080
  • API Health: http://localhost:3001/api/health
  • Adminer: http://localhost:8081
Replace localhost with your server’s IP address for network access.

Docker Compose Configuration

Services Architecture

docker-compose.yml
version: '3.8'

services:
  mysql:
    image: mysql:8
    container_name: aptiv-scrap-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: scrap2024
      MYSQL_DATABASE: scrap_control
      MYSQL_CHARSET: utf8mb4
      MYSQL_COLLATION: utf8mb4_unicode_ci
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docs/server/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pscrap2024"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    build: ./docs/server
    container_name: aptiv-scrap-api
    restart: unless-stopped
    environment:
      DB_HOST: mysql
      DB_USER: root
      DB_PASSWORD: scrap2024
      DB_NAME: scrap_control
      JWT_SECRET: aptiv-scrap-secret-2024
      TZ: America/Mexico_City
      PORT: 3001
    ports:
      - "3001:3001"
    depends_on:
      mysql:
        condition: service_healthy
    command: >
      sh -c "
        echo 'Esperando 5s para que MySQL esté listo...' &&
        sleep 5 &&
        node seed.js || true &&
        node server.js
      "

  adminer:
    image: adminer
    container_name: aptiv-adminer
    restart: unless-stopped
    ports:
      - "8081:8080"
    depends_on:
      mysql:
        condition: service_healthy

  nginx:
    image: nginx:alpine
    container_name: aptiv-scrap-web
    restart: unless-stopped
    volumes:
      - ./dist:/usr/share/nginx/html
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "8080:80"
    depends_on:
      - api

volumes:
  mysql_data:
    driver: local

Service Details

MySQL Database Service

MySQL 8 Configuration

Image: mysql:8 (official MySQL 8 image)Container Name: aptiv-scrap-dbRestart Policy: unless-stopped (survives reboots)Exposed Port: 3306 (accessible from host and other containers)

Environment Variables

VariableValueDescription
MYSQL_ROOT_PASSWORDscrap2024Root user password
MYSQL_DATABASEscrap_controlDatabase created on initialization
MYSQL_CHARSETutf8mb4Character set for Unicode support
MYSQL_COLLATIONutf8mb4_unicode_ciCollation for proper sorting

Volumes

  • Persistent Data: mysql_data:/var/lib/mysql - Database files persisted across container restarts
  • Init Script: ./docs/server/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql - Automatic schema creation on first run

Health Check

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pscrap2024"]
  interval: 10s      # Check every 10 seconds
  timeout: 5s        # Fail if no response in 5s
  retries: 5         # 5 failed checks = unhealthy
Other services wait for MySQL to be healthy before starting.

Node.js API Service

Express API Configuration

Build Context: ./docs/server (uses Dockerfile)Container Name: aptiv-scrap-apiBase Image: node:18-alpine (lightweight Node.js)Exposed Port: 3001

Environment Variables

VariableValueDescription
DB_HOSTmysqlMySQL container hostname (Docker network)
DB_USERrootDatabase username
DB_PASSWORDscrap2024Database password
DB_NAMEscrap_controlDatabase name
JWT_SECRETaptiv-scrap-secret-2024Secret key for JWT token signing
TZAmerica/Mexico_CityTimezone for timestamps
PORT3001API listen port
Security: Change JWT_SECRET and DB_PASSWORD in production to secure values.

Dockerfile

FROM node:18-alpine

ENV TZ=America/Mexico_City

# Install timezone data
RUN apk add --no-cache tzdata \
  && cp /usr/share/zoneinfo/$TZ /etc/localtime \
  && echo $TZ > /etc/timezone

WORKDIR /app

COPY package.json ./
RUN npm install --production

COPY . .

EXPOSE 3001

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:3001/api/health || exit 1

CMD ["node", "server.js"]

Startup Command

The API container runs a multi-step startup:
sh -c "
  echo 'Esperando 5s para que MySQL esté listo...' &&
  sleep 5 &&
  node seed.js || true &&  # Seed demo data (ignore errors)
  node server.js            # Start API server
"
Startup Process:
  1. Wait 5 seconds for MySQL to fully initialize
  2. Run seed.js to populate demo data (first run only)
  3. Start the Express server on port 3001

Dependencies

The API uses these npm packages:
{
  "express": "^4.18.2",      // Web framework
  "mysql2": "^3.6.5",        // MySQL driver with Promise support
  "bcryptjs": "^2.4.3",      // Password hashing
  "jsonwebtoken": "^9.0.2",  // JWT authentication
  "cors": "^2.8.5",          // CORS middleware
  "multer": "^1.4.5-lts.1",  // File upload handling
  "uuid": "^9.0.0",          // UUID generation
  "dotenv": "^16.3.1"        // Environment variables
}

Nginx Web Server

Nginx Configuration

Image: nginx:alpine (lightweight Nginx)Container Name: aptiv-scrap-webExposed Port: 8080 (maps to container port 80)

Nginx Configuration File

nginx.conf
server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    # Frontend SPA - serve index.html for all routes
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Proxy API requests to Node.js backend
    location /api/ {
        proxy_pass http://api:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
    }

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_min_length 256;

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}
Key Features:
  • SPA Routing: All routes serve index.html for client-side routing
  • API Proxy: /api/* requests forwarded to api:3001 container
  • Compression: Gzip enabled for text assets
  • Caching: Static assets cached for 1 year

Adminer Database Tool

Adminer Configuration

Image: adminer (official database management UI)Container Name: aptiv-adminerExposed Port: 8081 (maps to container port 8080)
Access Adminer:
  1. Navigate to http://localhost:8081
  2. Login with:
    • System: MySQL
    • Server: mysql
    • Username: root
    • Password: scrap2024
    • Database: scrap_control

Database Initialization

Automatic Seed Data

On first startup, the API automatically populates the database with:
  • 4 demo users (admin, calidad, supervisor, operator)
  • 5 production areas (Molding, Assembly, Painting, Testing, Packaging)
  • 20 part numbers with materials and costs
  • 15 failure modes categorized by type
  • 3 shifts with time ranges
  • 4 user roles with permission matrices
  • Sample scrap records for testing reports
Seed data is idempotent - it checks for existing data before inserting, so it’s safe to run multiple times.

Manual Seed Execution

To re-run the seed script:
docker-compose exec api node seed.js

Running Migrations

For existing databases that need schema updates:
# Copy migration file into MySQL container
docker cp migration.sql aptiv-scrap-db:/tmp/migration.sql

# Execute migration
docker-compose exec mysql mysql -u root -pscrap2024 scrap_control < /tmp/migration.sql

Container Management

Common Commands

docker-compose up -d

Updating Services

1

Update Application Code

# Pull latest code
git pull origin main

# Rebuild frontend
npm install
npm run build
2

Rebuild Containers

# Rebuild and restart
docker-compose down
docker-compose up -d --build
3

Verify Update

# Check logs for errors
docker-compose logs -f

# Test health endpoint
curl http://localhost:3001/api/health

Data Backup and Restore

Database Backup

1

Export MySQL Database

# Full database dump
docker-compose exec mysql mysqldump -u root -pscrap2024 scrap_control > backup_$(date +%Y%m%d_%H%M%S).sql

# Compressed backup
docker-compose exec mysql mysqldump -u root -pscrap2024 scrap_control | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
2

Backup Specific Tables

# Only scrap records
docker-compose exec mysql mysqldump -u root -pscrap2024 scrap_control pesaje > pesaje_backup.sql

# Only catalogs
docker-compose exec mysql mysqldump -u root -pscrap2024 scrap_control areas catnp fallas turnos > catalogs_backup.sql
3

Automated Backup Script

Create a daily backup cron job:
backup.sh
#!/bin/bash
BACKUP_DIR="/home/user/backups"
DATE=$(date +%Y%m%d_%H%M%S)

docker-compose exec mysql mysqldump -u root -pscrap2024 scrap_control | gzip > "$BACKUP_DIR/aptiv_scrap_$DATE.sql.gz"

# Keep only last 30 days
find $BACKUP_DIR -name "aptiv_scrap_*.sql.gz" -mtime +30 -delete
Add to crontab:
0 2 * * * /path/to/backup.sh

Database Restore

1

Stop API Service

docker-compose stop api
2

Restore from Backup

# From SQL file
docker-compose exec -T mysql mysql -u root -pscrap2024 scrap_control < backup.sql

# From compressed backup
gunzip < backup.sql.gz | docker-compose exec -T mysql mysql -u root -pscrap2024 scrap_control
3

Restart Services

docker-compose start api
docker-compose logs -f api

Volume Backup

Backup the entire MySQL data volume:
# Stop MySQL to ensure consistency
docker-compose stop mysql

# Create volume backup
docker run --rm -v aptiv-scrap-control_mysql_data:/data -v $(pwd):/backup alpine tar czf /backup/mysql_volume_$(date +%Y%m%d).tar.gz /data

# Restart MySQL
docker-compose start mysql

Health Checks and Monitoring

API Health Endpoint

curl http://localhost:3001/api/health
Response:
{
  "status": "ok",
  "timestamp": "2024-03-15T10:30:45.123Z",
  "uptime": 3600,
  "database": "connected"
}

Container Health Status

# View health status
docker-compose ps

# Detailed inspection
docker inspect --format='{{.State.Health.Status}}' aptiv-scrap-api
docker inspect --format='{{.State.Health.Status}}' aptiv-scrap-db

Resource Usage

# Container stats (CPU, memory, network)
docker stats

# Disk usage
docker system df

# Volume size
docker volume inspect aptiv-scrap-control_mysql_data

Troubleshooting

MySQL Container Won’t Start

1

Check Logs

docker-compose logs mysql
Common issues:
  • Port 3306 already in use
  • Insufficient disk space
  • Corrupted data volume
2

Reset Database

This will delete all data. Backup first!
docker-compose down -v
docker volume rm aptiv-scrap-control_mysql_data
docker-compose up -d

API Returns 502 Bad Gateway

# Check API container status
docker-compose ps api

# View API logs
docker-compose logs -f api

# Restart API
docker-compose restart api

# Verify MySQL connectivity
docker-compose exec api ping mysql

Permission Denied Errors

# Fix file ownership
sudo chown -R $USER:$USER .

# Rebuild containers
docker-compose down
docker-compose up -d --build

Port Already in Use

Change ports in docker-compose.yml:
services:
  mysql:
    ports:
      - "13306:3306"  # Changed from 3306
  api:
    ports:
      - "13001:3001"  # Changed from 3001
  nginx:
    ports:
      - "18080:80"    # Changed from 8080
Update API environment:
environment:
  DB_HOST: mysql
  # Note: Use container port (3306), not host port

Complete Reset

Nuclear option - start completely fresh:
# Stop and remove everything
docker-compose down -v

# Remove all images
docker rmi $(docker images -q aptiv-scrap-*)

# Clean Docker system
docker system prune -a --volumes

# Rebuild from scratch
npm run build
docker-compose up -d --build

Production Hardening

Security Checklist

1

Change Default Passwords

Update in docker-compose.yml:
environment:
  MYSQL_ROOT_PASSWORD: <strong-random-password>
  DB_PASSWORD: <strong-random-password>
  JWT_SECRET: <random-256-bit-string>
2

Restrict Port Exposure

Remove external port bindings for internal services:
mysql:
  # ports:
  #   - "3306:3306"  # Commented - only accessible from containers

api:
  # ports:
  #   - "3001:3001"  # Commented - access via Nginx proxy only
3

Enable SSL/TLS

Configure Nginx with SSL certificates:
server {
    listen 443 ssl;
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
}
4

Implement Rate Limiting

Add to nginx.conf:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

location /api/ {
    limit_req zone=api_limit burst=20;
    proxy_pass http://api:3001;
}

Next Steps

Network Setup

Configure LAN access and firewall rules

API Reference

Explore available API endpoints

Database Schema

Understand the database structure

Monitoring

Set up logging and monitoring

Build docs developers (and LLMs) love