diff --git a/01-basics/Makefile b/01-basics/Makefile new file mode 100644 index 0000000..081bfeb --- /dev/null +++ b/01-basics/Makefile @@ -0,0 +1,11 @@ +# Makefile for the 'basic' example + +include ../include/make/init.mk + +start: ## Start the example + @printf "\n\033[1;33m%s\033[0m\n\n" "Starting the example..." + @docker compose up + +cleanup: ## Cleanup any stopped containers + @printf "\n\033[1;33m%s\033[0m\n\n" "Cleaning up stopped containers..." + @docker compose down diff --git a/01-basics/README.md b/01-basics/README.md new file mode 100644 index 0000000..58d2f6a --- /dev/null +++ b/01-basics/README.md @@ -0,0 +1,5 @@ +# 01 Basic + +1. Run `make start` or `docker compose up`. +2. See the Docker "Hello World" message printed to the console. +3. Run `make cleanup` or `docker compose down`. diff --git a/01-basics/docker-compose.yml b/01-basics/docker-compose.yml index 4682964..b45a06c 100644 --- a/01-basics/docker-compose.yml +++ b/01-basics/docker-compose.yml @@ -1,9 +1,11 @@ ## This is a basic Docker Compose file -## Version of the Docker Compose file +## Version of the Compose specification you're using in this file (optional) version: '3.7' ## Service definitions services: + ## Service name hello-world: + ## Image to use image: hello-world diff --git a/02-service-config/Makefile b/02-service-config/Makefile new file mode 100644 index 0000000..55c4d66 --- /dev/null +++ b/02-service-config/Makefile @@ -0,0 +1,15 @@ +# Makefile for the 'service-config' example + +include ../include/make/init.mk + +start: ## Start services + @printf "\n\033[1;33m%s\033[0m\n\n" "Starting services..." + @docker compose up -d + +restart: ## Restart Docker services + @printf "\n\033[1;33m%s\033[0m\n\n" "Restarting services..." + @docker compose restart + +stop: ## Stop services + @printf "\n\033[1;33m%s\033[0m\n\n" "Stopping services..." + @docker compose down diff --git a/02-service-config/README.md b/02-service-config/README.md new file mode 100644 index 0000000..c520bef --- /dev/null +++ b/02-service-config/README.md @@ -0,0 +1,9 @@ +# 02 Service Configuration + +1. Run `make start` or `docker compose up -d`. +2. Go to [http://localhost:4000](http://localhost:4000) in your web browser. +3. See the "Hello world!" message printed in your browser. +4. Run `make stop` or `docker compose down`. + +- You shouldn't be able to get to [http://localhost:3000](http://localhost:3000) in your web browser. + - Even though our Node.js server is listening on port 3000, our Docker service publishes to port 4000 on the host. diff --git a/02-service-config/docker-compose.yml b/02-service-config/docker-compose.yml new file mode 100644 index 0000000..a0e5dfc --- /dev/null +++ b/02-service-config/docker-compose.yml @@ -0,0 +1,28 @@ +## This is a Compose file with a service configuration + +services: + node: + ## Image to use, with a specific tag + image: node:20 + + ## User to run the container as + ## This is a user that exists in the image + user: node + + ## Volumes to mount to the container + ## The first path is the host path, the second is the container path + ## The dot (.) represents the current directory + volumes: + - .:/home/node/app + + ## Set the working directory inside the container + working_dir: /home/node/app + + ## Publish ports from the container to the host + ## This lets you access the service from your own machine, e.g. through a browser + ## The first port is the host port, the second is the container port + ports: + - 4000:3000 + + ## Command to run when the container starts + command: npm start diff --git a/02-service-config/package.json b/02-service-config/package.json new file mode 100644 index 0000000..ee3a894 --- /dev/null +++ b/02-service-config/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "name": "example-service-config", + "scripts": { + "start": "node server.js" + } +} diff --git a/02-service-config/server.js b/02-service-config/server.js new file mode 100644 index 0000000..cd0bb33 --- /dev/null +++ b/02-service-config/server.js @@ -0,0 +1,14 @@ +const { createServer } = require('node:http'); + +const hostname = '0.0.0.0'; +const port = 3000; + +const server = createServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('Hello World!'); +}); + +server.listen(port, hostname, () => { + console.log(`Server running at http://${hostname}:${port}/`); +}); diff --git a/03-networking/Makefile b/03-networking/Makefile new file mode 100644 index 0000000..1f3e987 --- /dev/null +++ b/03-networking/Makefile @@ -0,0 +1,15 @@ +# Makefile for the 'networking' example + +include ../include/make/init.mk + +start: ## Start services + @printf "\n\033[1;33m%s\033[0m\n\n" "Starting services..." + @docker compose up -d + +restart: ## Restart Docker services + @printf "\n\033[1;33m%s\033[0m\n\n" "Restarting services..." + @docker compose restart + +stop: ## Stop services + @printf "\n\033[1;33m%s\033[0m\n\n" "Stopping services..." + @docker compose down diff --git a/03-networking/README.md b/03-networking/README.md new file mode 100644 index 0000000..6d25b24 --- /dev/null +++ b/03-networking/README.md @@ -0,0 +1,12 @@ +# 03 Networking + +1. Run `make start` or `docker compose up -d`. +2. Go to [http://localhost:4000](http://localhost:4000) in your web browser. +3. See the backend's message printed in your browser. +4. Run `make stop` or `docker compose down`. + +- You shouldn't be able to get to [http://localhost:3000](http://localhost:3000) in your web browser. + - Even though our Node.js server is listening on port 3000, our Docker service publishes to port 4000 on the host. +- You shouldn't be able to get to [http://localhost:5000](http://localhost:5000) either. + - Our backend service doesn't publish **any** ports to the host - but it does expose it to the frontend service. + - This means the frontend service can talk to our backend, but no one else can. diff --git a/03-networking/backend/package.json b/03-networking/backend/package.json new file mode 100644 index 0000000..a22a2a6 --- /dev/null +++ b/03-networking/backend/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "name": "example-networking-backend", + "scripts": { + "start": "node server.js" + } +} diff --git a/03-networking/backend/server.js b/03-networking/backend/server.js new file mode 100644 index 0000000..876dad9 --- /dev/null +++ b/03-networking/backend/server.js @@ -0,0 +1,14 @@ +const { createServer } = require('node:http'); + +const hostname = '0.0.0.0'; +const port = 5000; + +const server = createServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('This is a message from the backend!'); +}); + +server.listen(port, hostname, () => { + console.log(`Server running at http://${hostname}:${port}/`); +}); diff --git a/03-networking/docker-compose.yml b/03-networking/docker-compose.yml new file mode 100644 index 0000000..5ef0b86 --- /dev/null +++ b/03-networking/docker-compose.yml @@ -0,0 +1,43 @@ +## This is a Compose file with multiple services and networking + +services: + frontend: + image: node:20 + user: node + volumes: + - ./frontend:/home/node/app + working_dir: /home/node/app + ports: + - 4000:3000 + command: npm start + + ## Attach the frontend service to our network + networks: + - my-network + + ## Let's add another service for our backend + backend: + image: node:20 + user: node + volumes: + - ./backend:/home/node/app + working_dir: /home/node/app + command: npm start + + ## Expose the port to the frontend service + ## This is different from `ports`, which publishes ports to the host + ## Instead, `expose` makes the port available to other services in the same network - but not to the host + expose: + - 5000 + + ## Attach the backend service to our network + networks: + - my-network + +## Define the networks for our services +networks: + ## Create a network called my-network + my-network: + ## Use the bridge driver + ## This creates a private internal network that only our services can access + driver: bridge diff --git a/03-networking/frontend/package.json b/03-networking/frontend/package.json new file mode 100644 index 0000000..275eceb --- /dev/null +++ b/03-networking/frontend/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "name": "example-networking-frontend", + "scripts": { + "start": "node server.js" + } +} diff --git a/03-networking/frontend/server.js b/03-networking/frontend/server.js new file mode 100644 index 0000000..be3e483 --- /dev/null +++ b/03-networking/frontend/server.js @@ -0,0 +1,32 @@ +const { createServer } = require('node:http'); +const http = require('http'); + +const hostname = '0.0.0.0'; +const port = 3000; + +const server = createServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + + const backendUrl = 'http://backend:5000'; + + http.get(backendUrl, (backendRes) => { + let data = ''; + backendRes.on('data', (chunk) => { + data += chunk; + }); + backendRes.on('end', () => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end(data); + }); + }).on('error', (err) => { + console.error(`Error accessing backend: ${err}`); + res.statusCode = 500; + res.end('Internal Server Error'); + }); +}); + +server.listen(port, hostname, () => { + console.log(`Server running at http://${hostname}:${port}/`); +}); diff --git a/04-volumes/Makefile b/04-volumes/Makefile new file mode 100644 index 0000000..5112855 --- /dev/null +++ b/04-volumes/Makefile @@ -0,0 +1,20 @@ +# Makefile for the 'volumes' example + +include ../include/make/init.mk + +start: ## Start services + @printf "\n\033[1;33m%s\033[0m\n\n" "Starting services..." + @docker compose up -d + +restart: ## Restart Docker services + @printf "\n\033[1;33m%s\033[0m\n\n" "Restarting services..." + @docker compose restart + +stop: ## Stop services + @printf "\n\033[1;33m%s\033[0m\n\n" "Stopping services..." + @docker compose down + +inspect-volume: ## Inspect the backend-logs named volume + @printf "\n\033[1;33m%s\033[0m\n\n" "Inspecting the backend-logs named volume..." + @docker volume inspect 04-volumes_backend-logs + @printf "\n\033[1;35m%s\033[0m\n\n" "💡 You can view the contents of the volume by opening the mountpoint in your file explorer." diff --git a/04-volumes/README.md b/04-volumes/README.md new file mode 100644 index 0000000..258dcde --- /dev/null +++ b/04-volumes/README.md @@ -0,0 +1,14 @@ +# 04 Volumes + +## Running the application + +1. Run `make start` or `docker compose up -d`. +2. Go to [http://localhost:4000](http://localhost:4000) in your web browser. +3. See the backend's message (number of requests) printed in your browser. +4. Run `make stop` or `docker compose down`. + +## Viewing the files in the named volume + +1. Start the services. +2. Run `make inspect-volume` or `docker volume inspect 04-volumes_backend-logs`. +3. Open the `mountpoint` path in your file explorer program. diff --git a/04-volumes/backend/package.json b/04-volumes/backend/package.json new file mode 100644 index 0000000..1373f1f --- /dev/null +++ b/04-volumes/backend/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "name": "example-volumes-backend", + "scripts": { + "start": "node server.js" + } +} diff --git a/04-volumes/backend/server.js b/04-volumes/backend/server.js new file mode 100644 index 0000000..846c8a2 --- /dev/null +++ b/04-volumes/backend/server.js @@ -0,0 +1,54 @@ +const { createServer } = require('node:http'); +const fs = require('fs'); +const readline = require('readline'); + +const hostname = '0.0.0.0'; +const port = 5000; + +const server = createServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + + const logFilePath = '/home/node/app/logs/access.log'; + + const clientIP = req.socket.remoteAddress; + const timestamp = new Date().toISOString(); + + fs.access(logFilePath, fs.constants.F_OK, (err) => { + if (err) { + if (err.code === 'ENOENT') { + fs.writeFile(logFilePath, '', (err) => { + if (err) { + console.error('Error creating access log file:', err); + } + }); + } else { + console.error('Error checking access log file:', err); + } + } + }); + + fs.appendFile(logFilePath, `[${timestamp}] Connection from ${clientIP}\n`, (err) => { + if (err) { + console.error('Error writing to access log:', err); + } + }); + + const lineReader = readline.createInterface({ + input: fs.createReadStream(logFilePath), + }); + + let lineCount = 0; + + lineReader.on('line', () => { + lineCount++; + }); + + lineReader.on('close', () => { + res.end('The backend server has received ' + lineCount + ' requests.'); + }); +}); + +server.listen(port, hostname, () => { + console.log(`Server running at http://${hostname}:${port}/`); +}); diff --git a/04-volumes/docker-compose.yml b/04-volumes/docker-compose.yml new file mode 100644 index 0000000..7d7db6b --- /dev/null +++ b/04-volumes/docker-compose.yml @@ -0,0 +1,45 @@ +## This is a Compose file with a named volume + +services: + frontend: + image: node:20 + user: node + volumes: + - ./frontend:/home/node/app + working_dir: /home/node/app + ports: + - 4000:3000 + command: npm start + networks: + - my-network + + backend: + image: node:20 + user: node + volumes: + ## Note that we're mounting the backend folder to /home/node/app/src + ## This is so we can separate the source code from the logs + - ./backend:/home/node/app/src + + ## Add a named volume for the backend logs + ## This will store the logs in a volume that can be shared between containers + - backend-logs:/home/node/app/logs + + working_dir: /home/node/app/src + command: npm start + expose: + - 5000 + networks: + - my-network + +networks: + my-network: + driver: bridge + +## Define our volumes +volumes: + ## Create a named volume called backend-logs + backend-logs: + ## Use the local driver + ## This stores the volume on the host machine in a folder managed by Docker + driver: local diff --git a/04-volumes/frontend/package.json b/04-volumes/frontend/package.json new file mode 100644 index 0000000..1443231 --- /dev/null +++ b/04-volumes/frontend/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "name": "example-volumes-frontend", + "scripts": { + "start": "node server.js" + } +} diff --git a/04-volumes/frontend/server.js b/04-volumes/frontend/server.js new file mode 100644 index 0000000..be3e483 --- /dev/null +++ b/04-volumes/frontend/server.js @@ -0,0 +1,32 @@ +const { createServer } = require('node:http'); +const http = require('http'); + +const hostname = '0.0.0.0'; +const port = 3000; + +const server = createServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + + const backendUrl = 'http://backend:5000'; + + http.get(backendUrl, (backendRes) => { + let data = ''; + backendRes.on('data', (chunk) => { + data += chunk; + }); + backendRes.on('end', () => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end(data); + }); + }).on('error', (err) => { + console.error(`Error accessing backend: ${err}`); + res.statusCode = 500; + res.end('Internal Server Error'); + }); +}); + +server.listen(port, hostname, () => { + console.log(`Server running at http://${hostname}:${port}/`); +}); diff --git a/include/make/init.mk b/include/make/init.mk new file mode 100644 index 0000000..4ddb8c3 --- /dev/null +++ b/include/make/init.mk @@ -0,0 +1,20 @@ +# Sets up your Makefile with some quality-of-life configurations. + +# Change the shell that Make uses to bash. +SHELL := bash + +# Force Make to run each recipe in one single shell session instead of a new +# session per command. +.ONESHELL: + +# Set bash unofficial "strict mode". +.SHELLFLAGS := -eu -o pipefail -c + +# Have Make warn us if we try to use undefined variables. +MAKEFLAGS += --warn-undefined-variables + +# Stop Make applying loads of built-in magic rubbish. We don't need it! +MAKEFLAGS += --no-builtin-rules + +# Silence recursive Make directory notices. +MAKEFLAGS += --no-print-directory