Manual Docker only — create code yourself, then build & run
This is the only way documented for this project.
You will:
- Create folders and every source file yourself (PHP or Node)
- Paste code from the linked files (or from the blocks below)
- Write a Dockerfile piece by piece
docker pullimagesdocker network createdocker volume create(named volumes only — no host folder bind mounts)docker build(code goes into the image withCOPY)docker runMySQL + 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
| 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:
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
| Volume | Used for |
|---|---|
ba-docker-mysql | MySQL data files |
ba-docker-uploads | User 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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
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 create | Open / copy from |
|---|---|
application/db.php | php-app/application/db.php |
application/index.php | php-app/application/index.php |
application/create.php | php-app/application/create.php |
application/edit.php | php-app/application/edit.php |
application/delete.php | php-app/application/delete.php |
Dockerfile | php-app/Dockerfile |
Node — create these yourself
| You create | Open / copy from |
|---|---|
package.json | nodejs-app/package.json |
server.js | nodejs-app/server.js |
Dockerfile | nodejs-app/Dockerfile (manual version above uses npm install) |
Database SQL
| Task | File |
|---|---|
| Create DB / user / table | setup-database.html · 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.