Manual Steps

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

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

sudo service docker start
docker --version

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


Pick one stack

StackWorking folder you will createBrowserMySQL port
PHP~/ba-docker-php (or any empty folder)http://localhost:80803306
Node~/ba-docker-nodehttp://localhost:30003307

Reference copies of the finished code already live under:

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

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

Final layout you will build:

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

nano application/db.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

nano application/index.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

nano application/create.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

nano application/edit.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

nano application/delete.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

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

A3. Create Dockerfile piece by piece

Link (reference): php-app/Dockerfile

cd ~/ba-docker-php
nano Dockerfile

Piece 1

FROM php:8.2-apache

Piece 2

RUN docker-php-ext-install pdo pdo_mysql

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

COPY application/ /var/www/html/

Piece 4

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

Full 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

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

A5. Create network by command

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)

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
VolumeUsed for
ba-docker-mysqlMySQL data files
ba-docker-uploadsUser uploaded files

A7. Build your image (copies code in)

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

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.html

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

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:

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

A11. Persistence proof

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

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

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

Layout:

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

nano package.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

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:

// 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);
  });
ls -la
# package.json  server.js

B3. Create Dockerfile piece by piece

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

nano Dockerfile

Pieces

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"]

Full 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

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:

docker rm -f ba-docker-app ba-docker-db

B5. Build

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

B6. Run MySQL (host port 3307)

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.html.


B7. Run Node app

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

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.

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 createOpen / copy from
application/db.phpphp-app/application/db.php
application/index.phpphp-app/application/index.php
application/create.phpphp-app/application/create.php
application/edit.phpphp-app/application/edit.php
application/delete.phpphp-app/application/delete.php
Dockerfilephp-app/Dockerfile

Node — create these yourself

You createOpen / copy from
package.jsonnodejs-app/package.json
server.jsnodejs-app/server.js
Dockerfilenodejs-app/Dockerfile (manual version above uses npm install)

Database SQL

TaskFile
Create DB / user / tablesetup-database.html · setup.sql

What you prove for the assignment

ConceptHow you prove it manually
Imagesdocker pull + docker images + your docker build image
DockerfileYou wrote it; COPY puts code in the image
Containersdocker run / docker ps / docker rm
Volumesdocker volume create + -v name:/path (not host bind mount)
Networkingdocker network create + --network-alias db + DB_HOST=db
PersistenceDelete containers, re-run with same volumes

Rules for this project (summary)

DoDon’t
Create every .php / .js file yourselfRely on compose as the main demo
COPY code in Dockerfile + docker buildBind-mount source code with -v ./app:...
docker volume create for MySQL + uploadsOnly use a host ./uploads folder as the “volume story”
docker pull then docker runHide everything behind one compose up

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