# Manual Docker only — create code yourself, then build & run

This is the **only** way documented for this project.

You will:

1. Create folders and **every source file yourself** (PHP or Node)
2. Paste code from the **linked files** (or from the blocks below)
3. Write a **Dockerfile** piece by piece
4. `docker pull` images  
5. `docker network create`  
6. `docker volume create` (named volumes only — **no host folder bind mounts**)  
7. `docker build` (code goes into the image with `COPY`)  
8. `docker run` MySQL + app  

There is **no** `docker compose up` in this path.  
There is **no** “mount `./application` from the host”. Code is **copied into the image** at build time.  
Named volumes only hold **MySQL data** and **uploaded files**.

---

## How code gets into the container

```text
You create files on your PC
        ↓
Dockerfile: COPY application/ ...
        ↓
docker build   →  files baked into YOUR image
        ↓
docker run     →  container runs that image
```

Uploads / MySQL data use **named volumes** (created with `docker volume create`), not Windows folder mounts.

---

## One-time: Docker in WSL

```bash
sudo service docker start
docker --version
```

If Docker is not installed yet, follow the install section in [GUIDE.md](GUIDE.md) (WSL + Docker Engine only).

---

## Pick one stack

| Stack | Working folder you will create | Browser | MySQL port |
|-------|--------------------------------|---------|------------|
| **PHP** | `~/ba-docker-php` (or any empty folder) | http://localhost:8080 | 3306 |
| **Node** | `~/ba-docker-node` | http://localhost:3000 | 3307 |

Reference copies of the finished code already live under:

- PHP: [`php-app/application/`](php-app/application/)
- Node: [`nodejs-app/server.js`](nodejs-app/server.js), [`nodejs-app/package.json`](nodejs-app/package.json)

Open those links in your editor if you prefer copy-from-file instead of paste-from-guide.

---

# PATH A — PHP (create everything manually)

## A1. Create project folders

```bash
mkdir -p ~/ba-docker-php/application
cd ~/ba-docker-php
```

Final layout you will build:

```text
ba-docker-php/
├── Dockerfile                 ← you create
└── application/
    ├── db.php                 ← you create
    ├── index.php              ← you create
    ├── create.php             ← you create
    ├── edit.php               ← you create
    └── delete.php             ← you create
```

---

## A2. Create PHP files one by one

For each file: `nano application/FILENAME` → paste → save.

### File 1 — `application/db.php`

**Link (reference):** [php-app/application/db.php](php-app/application/db.php)

```bash
nano application/db.php
```

```php
<?php
// Simple database connection using PDO
// Values come from Docker environment variables
// Database and tables must already exist (see SETUP_DATABASE.md)

$host = getenv('DB_HOST') ?: 'db';
$name = getenv('DB_NAME') ?: 'userdb';
$user = getenv('DB_USER') ?: 'appuser';
$pass = getenv('DB_PASS') ?: 'apppass';

try {
    $pdo = new PDO(
        "mysql:host=$host;dbname=$name;charset=utf8mb4",
        $user,
        $pass,
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
} catch (PDOException $e) {
    die("Database connection failed: " . $e->getMessage()
        . " — Create the database and tables first (see SETUP_DATABASE.md).");
}
```

---

### File 2 — `application/index.php` (READ / list users)

**Link:** [php-app/application/index.php](php-app/application/index.php)

```bash
nano application/index.php
```

```php
<?php
// Home page - list all users (READ)
require 'db.php';

$users = $pdo->query("SELECT * FROM users ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head>
    <title>User Management (PHP)</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
        th { background: #f0f0f0; }
        a { margin-right: 10px; }
        .btn { display: inline-block; padding: 8px 16px; background: #007bff; color: #fff; text-decoration: none; border-radius: 4px; }
    </style>
</head>
<body>
    <h1>User Management (PHP + Docker)</h1>
    <p><a class="btn" href="create.php">+ Register New User</a></p>

    <table>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Email</th>
            <th>File</th>
            <th>Actions</th>
        </tr>
        <?php if (count($users) === 0): ?>
            <tr><td colspan="5">No users yet. Register one!</td></tr>
        <?php endif; ?>
        <?php foreach ($users as $u): ?>
        <tr>
            <td><?= htmlspecialchars($u['id']) ?></td>
            <td><?= htmlspecialchars($u['name']) ?></td>
            <td><?= htmlspecialchars($u['email']) ?></td>
            <td>
                <?php if ($u['file_name']): ?>
                    <a href="uploads/<?= htmlspecialchars($u['file_name']) ?>" target="_blank">View</a>
                <?php else: ?>
                    —
                <?php endif; ?>
            </td>
            <td>
                <a href="edit.php?id=<?= $u['id'] ?>">Edit</a>
                <a href="delete.php?id=<?= $u['id'] ?>" onclick="return confirm('Delete this user?')">Delete</a>
            </td>
        </tr>
        <?php endforeach; ?>
    </table>
</body>
</html>
```

---

### File 3 — `application/create.php` (CREATE + upload)

**Link:** [php-app/application/create.php](php-app/application/create.php)

```bash
nano application/create.php
```

```php
<?php
// CREATE - register a new user + optional file upload
require 'db.php';

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name     = trim($_POST['name'] ?? '');
    $email    = trim($_POST['email'] ?? '');
    $password = trim($_POST['password'] ?? '');
    $fileName = null;

    if ($name === '' || $email === '' || $password === '') {
        $error = 'Name, email and password are required.';
    } else {
        if (!empty($_FILES['file']['name']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
            $original = basename($_FILES['file']['name']);
            $fileName = time() . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '', $original);
            $dest = __DIR__ . '/uploads/' . $fileName;
            if (!move_uploaded_file($_FILES['file']['tmp_name'], $dest)) {
                $error = 'File upload failed.';
            }
        }

        if ($error === '') {
            $hashed = password_hash($password, PASSWORD_DEFAULT);
            $stmt = $pdo->prepare(
                "INSERT INTO users (name, email, password, file_name) VALUES (?, ?, ?, ?)"
            );
            try {
                $stmt->execute([$name, $email, $hashed, $fileName]);
                header('Location: index.php');
                exit;
            } catch (PDOException $e) {
                $error = 'Could not save user. Email may already exist.';
            }
        }
    }
}
?>
<!DOCTYPE html>
<html>
<head>
    <title>Register User</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 500px; margin: 40px auto; padding: 0 20px; }
        label { display: block; margin-top: 12px; }
        input { width: 100%; padding: 8px; box-sizing: border-box; }
        button { margin-top: 16px; padding: 10px 20px; background: #28a745; color: #fff; border: none; cursor: pointer; }
        .error { color: red; }
    </style>
</head>
<body>
    <h1>Register New User</h1>
    <p><a href="index.php">← Back to list</a></p>
    <?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>

    <form method="POST" enctype="multipart/form-data">
        <label>Name</label>
        <input type="text" name="name" required>

        <label>Email</label>
        <input type="email" name="email" required>

        <label>Password</label>
        <input type="password" name="password" required>

        <label>Upload File (optional)</label>
        <input type="file" name="file">

        <button type="submit">Register</button>
    </form>
</body>
</html>
```

---

### File 4 — `application/edit.php` (UPDATE)

**Link:** [php-app/application/edit.php](php-app/application/edit.php)

```bash
nano application/edit.php
```

```php
<?php
// UPDATE - edit an existing user
require 'db.php';

$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$user) {
    die('User not found. <a href="index.php">Go back</a>');
}

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name     = trim($_POST['name'] ?? '');
    $email    = trim($_POST['email'] ?? '');
    $password = trim($_POST['password'] ?? '');
    $fileName = $user['file_name'];

    if ($name === '' || $email === '') {
        $error = 'Name and email are required.';
    } else {
        if (!empty($_FILES['file']['name']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
            $original = basename($_FILES['file']['name']);
            $fileName = time() . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '', $original);
            $dest = __DIR__ . '/uploads/' . $fileName;
            move_uploaded_file($_FILES['file']['tmp_name'], $dest);
        }

        if ($password !== '') {
            $hashed = password_hash($password, PASSWORD_DEFAULT);
        } else {
            $hashed = $user['password'];
        }

        $stmt = $pdo->prepare(
            "UPDATE users SET name = ?, email = ?, password = ?, file_name = ? WHERE id = ?"
        );
        try {
            $stmt->execute([$name, $email, $hashed, $fileName, $id]);
            header('Location: index.php');
            exit;
        } catch (PDOException $e) {
            $error = 'Update failed. Email may already be used.';
        }
    }
}
?>
<!DOCTYPE html>
<html>
<head>
    <title>Edit User</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 500px; margin: 40px auto; padding: 0 20px; }
        label { display: block; margin-top: 12px; }
        input { width: 100%; padding: 8px; box-sizing: border-box; }
        button { margin-top: 16px; padding: 10px 20px; background: #007bff; color: #fff; border: none; cursor: pointer; }
        .error { color: red; }
    </style>
</head>
<body>
    <h1>Edit User #<?= $user['id'] ?></h1>
    <p><a href="index.php">← Back to list</a></p>
    <?php if ($error): ?><p class="error"><?= htmlspecialchars($error) ?></p><?php endif; ?>

    <form method="POST" enctype="multipart/form-data">
        <label>Name</label>
        <input type="text" name="name" value="<?= htmlspecialchars($user['name']) ?>" required>

        <label>Email</label>
        <input type="email" name="email" value="<?= htmlspecialchars($user['email']) ?>" required>

        <label>New Password (leave blank to keep current)</label>
        <input type="password" name="password" placeholder="Leave blank to keep current">

        <label>Upload New File (optional)</label>
        <input type="file" name="file">
        <?php if ($user['file_name']): ?>
            <p>Current: <a href="uploads/<?= htmlspecialchars($user['file_name']) ?>" target="_blank"><?= htmlspecialchars($user['file_name']) ?></a></p>
        <?php endif; ?>

        <button type="submit">Save Changes</button>
    </form>
</body>
</html>
```

---

### File 5 — `application/delete.php` (DELETE)

**Link:** [php-app/application/delete.php](php-app/application/delete.php)

```bash
nano application/delete.php
```

```php
<?php
// DELETE - remove a user
require 'db.php';

$id = (int)($_GET['id'] ?? 0);

$stmt = $pdo->prepare("SELECT file_name FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

if ($user) {
    if ($user['file_name']) {
        $path = __DIR__ . '/uploads/' . $user['file_name'];
        if (file_exists($path)) {
            unlink($path);
        }
    }
    $pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$id]);
}

header('Location: index.php');
exit;
```

### Check files exist

```bash
ls -la application/
# db.php  index.php  create.php  edit.php  delete.php
```

---

## A3. Create Dockerfile piece by piece

**Link (reference):** [php-app/Dockerfile](php-app/Dockerfile)

```bash
cd ~/ba-docker-php
nano Dockerfile
```

### Piece 1

```dockerfile
FROM php:8.2-apache
```

### Piece 2

```dockerfile
RUN docker-php-ext-install pdo pdo_mysql
```

### Piece 3 — **COPY** your code into the image (this replaces “mounting”)

```dockerfile
COPY application/ /var/www/html/
```

### Piece 4

```dockerfile
RUN mkdir -p /var/www/html/uploads && chown -R www-data:www-data /var/www/html/uploads
```

### Full Dockerfile

```dockerfile
FROM php:8.2-apache
RUN docker-php-ext-install pdo pdo_mysql
COPY application/ /var/www/html/
RUN mkdir -p /var/www/html/uploads && chown -R www-data:www-data /var/www/html/uploads
```

---

## A4. Pull images by command

```bash
docker pull mysql:8.0
docker pull php:8.2-apache
docker images
```

---

## A5. Create network by command

```bash
docker network create ba-docker-net
docker network ls
docker network inspect ba-docker-net
```

---

## A6. Create named volumes by command (no host bind mounts)

```bash
docker volume create ba-docker-mysql
docker volume create ba-docker-uploads
docker volume ls
docker volume inspect ba-docker-mysql
docker volume inspect ba-docker-uploads
```

| Volume | Used for |
|--------|----------|
| `ba-docker-mysql` | MySQL data files |
| `ba-docker-uploads` | User uploaded files |

---

## A7. Build your image (copies code in)

```bash
cd ~/ba-docker-php
docker build -t ba-docker-php .
docker images
```

If you change any PHP file later: edit the file → run `docker build -t ba-docker-php .` again → recreate the app container.

---

## A8. Run MySQL

```bash
docker run -d \
  --name ba-docker-db \
  --network ba-docker-net \
  --network-alias db \
  -e MYSQL_ROOT_PASSWORD=rootpass \
  -v ba-docker-mysql:/var/lib/mysql \
  -p 3306:3306 \
  mysql:8.0

docker logs -f ba-docker-db
# wait for "ready for connections", then Ctrl+C
```

---

## A9. Create database (SQLyog)

Connect: host `127.0.0.1`, user `root`, password `rootpass`, port **3306**.

Full guide: [SETUP_DATABASE.md](SETUP_DATABASE.md)

```sql
CREATE DATABASE IF NOT EXISTS userdb
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED BY 'apppass';
GRANT ALL PRIVILEGES ON userdb.* TO 'appuser'@'%';
FLUSH PRIVILEGES;

USE userdb;
CREATE TABLE IF NOT EXISTS users (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  name       VARCHAR(100)  NOT NULL,
  email      VARCHAR(100)  NOT NULL UNIQUE,
  password   VARCHAR(255)  NOT NULL,
  file_name  VARCHAR(255)  DEFAULT NULL,
  created_at TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## A10. Run PHP app

```bash
docker run -d \
  --name ba-docker-app \
  --network ba-docker-net \
  -e DB_HOST=db \
  -e DB_NAME=userdb \
  -e DB_USER=appuser \
  -e DB_PASS=apppass \
  -v ba-docker-uploads:/var/www/html/uploads \
  -p 8080:80 \
  ba-docker-php

docker ps
docker network inspect ba-docker-net
```

Browser: **http://localhost:8080**

List uploads inside the **named volume**:

```bash
docker run --rm -v ba-docker-uploads:/data alpine ls -la /data
```

---

## A11. Persistence proof

```bash
docker rm -f ba-docker-app ba-docker-db

docker run -d --name ba-docker-db --network ba-docker-net --network-alias db \
  -e MYSQL_ROOT_PASSWORD=rootpass \
  -v ba-docker-mysql:/var/lib/mysql -p 3306:3306 mysql:8.0

# wait for MySQL ready, then:
docker run -d --name ba-docker-app --network ba-docker-net \
  -e DB_HOST=db -e DB_NAME=userdb -e DB_USER=appuser -e DB_PASS=apppass \
  -v ba-docker-uploads:/var/www/html/uploads -p 8080:80 ba-docker-php
```

Refresh browser → same data (volumes kept the data; you did not re-mount a host folder).

---

## A12. Cleanup

```bash
docker rm -f ba-docker-app ba-docker-db
docker volume rm ba-docker-mysql ba-docker-uploads
docker network rm ba-docker-net
docker rmi ba-docker-php
```

---

# PATH B — Node.js (create everything manually)

## B1. Create project folder

```bash
mkdir -p ~/ba-docker-node
cd ~/ba-docker-node
```

Layout:

```text
ba-docker-node/
├── Dockerfile          ← you create
├── package.json        ← you create
└── server.js           ← you create
```

---

## B2. Create Node files one by one

### File 1 — `package.json`

**Link:** [nodejs-app/package.json](nodejs-app/package.json)

```bash
nano package.json
```

```json
{
  "name": "user-management-nodejs",
  "version": "1.0.0",
  "description": "Beginner Dockerized user management app",
  "main": "server.js",
  "dependencies": {
    "express": "^5.2.1",
    "multer": "^2.2.0",
    "mysql2": "^3.23.2"
  }
}
```

---

### File 2 — `server.js` (entire app)

**Link:** [nodejs-app/server.js](nodejs-app/server.js)

```bash
nano server.js
```

Paste the full contents of the linked file (CRUD + upload + MySQL).  
Open the link in your editor and copy the whole file if paste from here is hard.

Full code:

```javascript
// Beginner Node.js + Express + MySQL user management app
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const mysql = require('mysql2/promise');

function hashPassword(password) {
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = crypto.scryptSync(password, salt, 64).toString('hex');
  return salt + ':' + hash;
}

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.urlencoded({ extended: true }));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, path.join(__dirname, 'uploads')),
  filename: (req, file, cb) => {
    const safe = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '');
    cb(null, Date.now() + '_' + safe);
  }
});
const upload = multer({ storage });

const dbConfig = {
  host: process.env.DB_HOST || 'db',
  user: process.env.DB_USER || 'appuser',
  password: process.env.DB_PASS || 'apppass',
  database: process.env.DB_NAME || 'userdb'
};

let pool;

async function initDb() {
  for (let i = 0; i < 30; i++) {
    try {
      pool = mysql.createPool(dbConfig);
      await pool.query('SELECT 1');
      console.log('Connected to MySQL (database must already exist — see SETUP_DATABASE.md)');
      return;
    } catch (err) {
      console.log('Waiting for MySQL... (' + (i + 1) + '/30) — ' + err.message);
      await new Promise(r => setTimeout(r, 2000));
    }
  }
  throw new Error('Could not connect to MySQL. Start MySQL and create userdb + tables (SETUP_DATABASE.md).');
}

function esc(s) {
  return String(s ?? '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

function page(title, body) {
  return `<!DOCTYPE html>
<html>
<head>
  <title>${esc(title)}</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
    table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
    th { background: #f0f0f0; }
    a { margin-right: 10px; }
    .btn { display: inline-block; padding: 8px 16px; background: #007bff; color: #fff; text-decoration: none; border-radius: 4px; }
    label { display: block; margin-top: 12px; }
    input { width: 100%; max-width: 400px; padding: 8px; box-sizing: border-box; }
    button { margin-top: 16px; padding: 10px 20px; background: #28a745; color: #fff; border: none; cursor: pointer; }
    .error { color: red; }
  </style>
</head>
<body>
${body}
</body>
</html>`;
}

app.get('/', async (req, res) => {
  try {
    const [users] = await pool.query('SELECT * FROM users ORDER BY id DESC');
    let rows = '';
    if (users.length === 0) {
      rows = '<tr><td colspan="5">No users yet. Register one!</td></tr>';
    } else {
      for (const u of users) {
        const fileLink = u.file_name
          ? `<a href="/uploads/${esc(u.file_name)}" target="_blank">View</a>`
          : '—';
        rows += `<tr>
          <td>${u.id}</td>
          <td>${esc(u.name)}</td>
          <td>${esc(u.email)}</td>
          <td>${fileLink}</td>
          <td>
            <a href="/edit/${u.id}">Edit</a>
            <a href="/delete/${u.id}" onclick="return confirm('Delete this user?')">Delete</a>
          </td>
        </tr>`;
      }
    }
    res.send(page('User Management (Node.js)', `
      <h1>User Management (Node.js + Docker)</h1>
      <p><a class="btn" href="/create">+ Register New User</a></p>
      <table>
        <tr><th>ID</th><th>Name</th><th>Email</th><th>File</th><th>Actions</th></tr>
        ${rows}
      </table>
    `));
  } catch (err) {
    res.status(500).send('Error: ' + err.message);
  }
});

app.get('/create', (req, res) => {
  res.send(page('Register User', `
    <h1>Register New User</h1>
    <p><a href="/">← Back to list</a></p>
    <form method="POST" action="/create" enctype="multipart/form-data">
      <label>Name</label>
      <input type="text" name="name" required>
      <label>Email</label>
      <input type="email" name="email" required>
      <label>Password</label>
      <input type="password" name="password" required>
      <label>Upload File (optional)</label>
      <input type="file" name="file">
      <button type="submit">Register</button>
    </form>
  `));
});

app.post('/create', upload.single('file'), async (req, res) => {
  const { name, email, password } = req.body;
  const fileName = req.file ? req.file.filename : null;

  if (!name || !email || !password) {
    return res.send(page('Error', '<p class="error">All fields required.</p><a href="/create">Try again</a>'));
  }

  try {
    const hashed = hashPassword(password);
    await pool.query(
      'INSERT INTO users (name, email, password, file_name) VALUES (?, ?, ?, ?)',
      [name, email, hashed, fileName]
    );
    res.redirect('/');
  } catch (err) {
    res.send(page('Error', '<p class="error">Could not save. Email may already exist.</p><a href="/create">Try again</a>'));
  }
});

app.get('/edit/:id', async (req, res) => {
  const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
  const u = rows[0];
  if (!u) return res.send('User not found. <a href="/">Go back</a>');

  const currentFile = u.file_name
    ? `<p>Current: <a href="/uploads/${esc(u.file_name)}" target="_blank">${esc(u.file_name)}</a></p>`
    : '';

  res.send(page('Edit User', `
    <h1>Edit User #${u.id}</h1>
    <p><a href="/">← Back to list</a></p>
    <form method="POST" action="/edit/${u.id}" enctype="multipart/form-data">
      <label>Name</label>
      <input type="text" name="name" value="${esc(u.name)}" required>
      <label>Email</label>
      <input type="email" name="email" value="${esc(u.email)}" required>
      <label>New Password (leave blank to keep current)</label>
      <input type="password" name="password" placeholder="Leave blank to keep current">
      <label>Upload New File (optional)</label>
      <input type="file" name="file">
      ${currentFile}
      <button type="submit">Save Changes</button>
    </form>
  `));
});

app.post('/edit/:id', upload.single('file'), async (req, res) => {
  const { name, email, password } = req.body;
  const id = req.params.id;

  if (!name || !email) {
    return res.send(page('Error', '<p class="error">Name and email are required.</p><a href="/">Back</a>'));
  }

  const [rows] = await pool.query('SELECT file_name, password FROM users WHERE id = ?', [id]);
  if (!rows[0]) return res.send('User not found');

  let fileName = rows[0].file_name;
  if (req.file) fileName = req.file.filename;

  let hashed = rows[0].password;
  if (password && password.trim() !== '') {
    hashed = hashPassword(password);
  }

  try {
    await pool.query(
      'UPDATE users SET name = ?, email = ?, password = ?, file_name = ? WHERE id = ?',
      [name, email, hashed, fileName, id]
    );
    res.redirect('/');
  } catch (err) {
    res.send(page('Error', '<p class="error">Update failed.</p><a href="/">Back</a>'));
  }
});

app.get('/delete/:id', async (req, res) => {
  const [rows] = await pool.query('SELECT file_name FROM users WHERE id = ?', [req.params.id]);
  if (rows[0] && rows[0].file_name) {
    const filePath = path.join(__dirname, 'uploads', rows[0].file_name);
    if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
  }
  await pool.query('DELETE FROM users WHERE id = ?', [req.params.id]);
  res.redirect('/');
});

initDb()
  .then(() => {
    app.listen(PORT, () => console.log('App running on port ' + PORT));
  })
  .catch(err => {
    console.error(err);
    process.exit(1);
  });
```

```bash
ls -la
# package.json  server.js
```

---

## B3. Create Dockerfile piece by piece

**Link (reference):** [nodejs-app/Dockerfile](nodejs-app/Dockerfile)  
(This manual path uses `npm install` so you do not need `package-lock.json`.)

```bash
nano Dockerfile
```

### Pieces

```dockerfile
FROM node:22-alpine
```

```dockerfile
WORKDIR /app
```

```dockerfile
COPY package.json ./
```

```dockerfile
RUN npm install --omit=dev
```

```dockerfile
COPY server.js ./
```

```dockerfile
RUN mkdir -p /app/uploads
EXPOSE 3000
CMD ["node", "server.js"]
```

### Full Dockerfile

```dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY server.js ./
RUN mkdir -p /app/uploads
EXPOSE 3000
CMD ["node", "server.js"]
```

---

## B4. Pull → network → volumes

```bash
docker pull mysql:8.0
docker pull node:22-alpine
docker images

docker network create ba-docker-net
# skip create if it already exists

docker volume create ba-docker-mysql-node
docker volume create ba-docker-uploads-node
docker volume ls
```

If old PHP containers use the same names/ports:

```bash
docker rm -f ba-docker-app ba-docker-db
```

---

## B5. Build

```bash
cd ~/ba-docker-node
docker build -t ba-docker-node .
docker images
```

---

## B6. Run MySQL (host port 3307)

```bash
docker run -d \
  --name ba-docker-db \
  --network ba-docker-net \
  --network-alias db \
  -e MYSQL_ROOT_PASSWORD=rootpass \
  -v ba-docker-mysql-node:/var/lib/mysql \
  -p 3307:3306 \
  mysql:8.0

docker logs -f ba-docker-db
```

SQLyog: port **3307**, same SQL as Path A / [SETUP_DATABASE.md](SETUP_DATABASE.md).

---

## B7. Run Node app

```bash
docker run -d \
  --name ba-docker-app \
  --network ba-docker-net \
  -e DB_HOST=db \
  -e DB_NAME=userdb \
  -e DB_USER=appuser \
  -e DB_PASS=apppass \
  -e PORT=3000 \
  -v ba-docker-uploads-node:/app/uploads \
  -p 3000:3000 \
  ba-docker-node

docker logs -f ba-docker-app
```

Browser: **http://localhost:3000**

```bash
docker run --rm -v ba-docker-uploads-node:/data alpine ls -la /data
```

---

## B8. Persistence + cleanup

Same idea as PHP: `docker rm -f` containers, re-`docker run` with the **same** volume names → data remains.

```bash
docker rm -f ba-docker-app ba-docker-db
docker volume rm ba-docker-mysql-node ba-docker-uploads-node
docker network rm ba-docker-net
docker rmi ba-docker-node
```

---

# Linked source files (quick index)

### PHP — create these yourself

| You create | Open / copy from |
|------------|------------------|
| `application/db.php` | [php-app/application/db.php](php-app/application/db.php) |
| `application/index.php` | [php-app/application/index.php](php-app/application/index.php) |
| `application/create.php` | [php-app/application/create.php](php-app/application/create.php) |
| `application/edit.php` | [php-app/application/edit.php](php-app/application/edit.php) |
| `application/delete.php` | [php-app/application/delete.php](php-app/application/delete.php) |
| `Dockerfile` | [php-app/Dockerfile](php-app/Dockerfile) |

### Node — create these yourself

| You create | Open / copy from |
|------------|------------------|
| `package.json` | [nodejs-app/package.json](nodejs-app/package.json) |
| `server.js` | [nodejs-app/server.js](nodejs-app/server.js) |
| `Dockerfile` | [nodejs-app/Dockerfile](nodejs-app/Dockerfile) (manual version above uses `npm install`) |

### Database SQL

| Task | File |
|------|------|
| Create DB / user / table | [SETUP_DATABASE.md](SETUP_DATABASE.md) · [setup.sql](setup.sql) |

---

# What you prove for the assignment

| Concept | How you prove it manually |
|---------|---------------------------|
| Images | `docker pull` + `docker images` + your `docker build` image |
| Dockerfile | You wrote it; `COPY` puts code in the image |
| Containers | `docker run` / `docker ps` / `docker rm` |
| Volumes | `docker volume create` + `-v name:/path` (not host bind mount) |
| Networking | `docker network create` + `--network-alias db` + `DB_HOST=db` |
| Persistence | Delete containers, re-run with same volumes |

---

# Rules for this project (summary)

| Do | Don’t |
|----|--------|
| Create every `.php` / `.js` file yourself | Rely on compose as the main demo |
| `COPY` code in Dockerfile + `docker build` | Bind-mount source code with `-v ./app:...` |
| `docker volume create` for MySQL + uploads | Only use a host `./uploads` folder as the “volume story” |
| `docker pull` then `docker run` | Hide everything behind one `compose up` |

You are building, pulling, networking, and volumizing **by hand**. That is the whole point.
