Sebastian Gomez
Node.js, Express, Docker and Docker Swarm
This guide walks you step by step through building a motivational quotes API with Node.js and Express, packaging it in Docker and deploying it to a Docker Swarm cluster. It is designed to be worked through in class, but it also works as a self paced lab.
1. The big picture
Final product: an HTTP service that serves quotes as JSON, packaged in a production ready Docker image and deployed to a Swarm with multiple replicas.
Key technologies: Node.js 24, Express 5, npm, Docker, Docker Hub and Docker Swarm.
Estimated duration: 3 blocks of 60 minutes (adapt it to your group's pace).
Teaching approach: first we understand the concepts, then we build manually, and finally we automate with containers and orchestration.
A note on orchestration: Docker Swarm still ships with Docker and works perfectly for learning, but today it is in maintenance mode. In production, the orchestration standard is Kubernetes. We use Swarm here because it is the simplest way to understand the concepts of replicas, load balancing and services without the complexity of Kubernetes. Once you have these ideas down, the jump to Kubernetes will feel much more natural.
2. Learning objectives
- Understand why Express is a minimalist framework and how to structure a project from scratch.
- Practice creating HTTP endpoints that return JSON and handle simple errors.
- Tell production and development dependencies apart in
package.json. - Build a reproducible Docker image, applying good practices such as
.dockerignoreandnpm ci. - Publish images to Docker Hub and manage tags.
- Deploy and scale an application on Docker Swarm, verifying that the replicas serve requests.
3. What you will build
The API will have three main routes:
GET /quotesreturns all the quotes.GET /quotes/randomreturns a random quote.GET /quotes/:idreturns the quote with the given identifier.
The Docker image will expose the service on port 3000 and will be optimized for ARM/AMD execution thanks to the node:24-alpine base. Finally, the service will be orchestrated in Swarm with distributed replicas.
4. Prerequisites
Background knowledge
- Fundamentals of modern JavaScript.
- Basic terminal and Git usage.
- Basic container concepts (what an image is, what a container is, etc.).
Installed software
- Node.js 24.x or the current active LTS (download from https://nodejs.org).
- Docker Desktop (macOS/Windows) or Docker Engine 26+ (Linux).
- A code editor (VS Code, WebStorm, etc.).
Accounts and access
- A Docker Hub account (free) if you want to publish the image.
- Access to a cloud or virtual machines (Amazon EC2, Azure, etc.) for the Swarm section.
Quick check
node --version # should return v24.x.x (or whichever LTS you have installed)
npm --version # npm ships with Node, version >=10 recommended
docker --version # Docker Engine >= 26
docker compose version # optional, recommendedIf any command fails, stop and fix the installation before moving on.
5. Setting up the environment
- Create a working folder where you will keep all the tutorial material.
- Open your terminal and move into that folder.
- (Optional) Initialize a Git repository and commit by sections to record your progress.
6. Step 1, initialize the Node.js project
mkdir random-quotes
cd random-quotes
npm init -y
npm install express
npm install --save-dev nodemonWhat just happened?
npm init -ygenerates apackage.jsonwith default values.expressis installed as a production dependency.nodemonis added as a development dependency to automatically reload while you study.
Adjust `package.json`
Edit the package.json file so it contains useful metadata and scripts:
{
"name": "random-quotes",
"version": "1.0.0",
"description": "Educational Express service that serves random quotes and demonstrates Docker & Docker Swarm deployment.",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"lint": "echo \"No linter configured\" && exit 0",
"test": "echo \"No tests yet\" && exit 0"
},
"keywords": [
"express",
"docker",
"tutorial"
],
"author": "Sebastian Gomez <seagomezar@gmail.com> (http://www.sebasgo.dev/)",
"license": "ISC",
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
"express": "^5.1.0"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}Teaching note: defining the scripts from the start helps you run repetitive tasks without memorizing long commands.
7. Step 2, implement the API with Express
We create a simple structure inside src/ to separate responsibilities.
7.1 File `src/quotes.js`
It holds the data and a function that picks a random element. It is a simple way (with no database) to have content.
const quotes = [
{ id: 1, text: 'The best way to predict the future is to invent it.', author: 'Alan Kay' },
{ id: 2, text: 'Simplicity is the soul of efficiency.', author: 'Austin Freeman' },
{ id: 3, text: 'Continuous improvement is better than delayed perfection.', author: 'Mark Twain' },
{ id: 4, text: 'Programs must be written for people to read, and only incidentally for machines to execute.', author: 'Harold Abelson' },
{ id: 5, text: 'If you automate a mess, you get an automated mess.', author: 'Rod Michael' },
{ id: 6, text: 'First, solve the problem. Then, write the code.', author: 'John Johnson' },
{ id: 7, text: 'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.', author: 'Martin Fowler' },
{ id: 8, text: 'Talk is cheap. Show me the code.', author: 'Linus Torvalds' },
{ id: 9, text: 'Experience is the name everyone gives to their mistakes.', author: 'Oscar Wilde' },
{ id: 10, text: 'Success is the ability to go from one failure to another with no loss of enthusiasm.', author: 'Winston Churchill' }
]
function getRandomQuote () {
const index = Math.floor(Math.random() * quotes.length)
return quotes[index]
}
module.exports = {
quotes,
getRandomQuote
}7.2 File `src/app.js`
Here we configure Express, define middlewares and routes.
const express = require('express')
const { quotes, getRandomQuote } = require('./quotes')
function createApp () {
const app = express()
app.use(express.json())
app.get('/', (req, res) => {
res.json({
status: 'ok',
message: 'Welcome to the Random Quotes API',
endpoints: {
random: '/quotes/random',
all: '/quotes',
byId: '/quotes/:id'
}
})
})
app.get('/quotes', (req, res) => {
res.json({
count: quotes.length,
data: quotes
})
})
app.get('/quotes/random', (req, res) => {
res.json(getRandomQuote())
})
app.get('/quotes/:id', (req, res) => {
const id = Number(req.params.id)
const quote = quotes.find(entry => entry.id === id)
if (!quote) {
return res.status(404).json({
error: `Quote with id ${id} not found`
})
}
return res.json(quote)
})
return app
}
module.exports = { createApp }7.3 File `src/server.js`
The service entry point: it creates the app and exposes it on a configurable port.
const { createApp } = require('./app')
const PORT = Number(process.env.PORT) || 3000
const app = createApp()
app.listen(PORT, () => {
console.log(`Servicio Random Quotes escuchando en el puerto ${PORT}`)
})8. Step 3, local testing
Development mode (automatic reload):
npm run devProduction mode (no nodemon):
npm startCheck it with the browser or with curl:
curl http://localhost:3000/
curl http://localhost:3000/quotes/random
curl http://localhost:3000/quotes/3Teaching shortcuts:
- Stop the server with
Ctrl + C. - Use tools like Postman or Insomnia to reinforce API concepts.
- Try a request to a nonexistent ID (
/quotes/999) to observe the error handling.
9. Step 4, good practices before Docker
- Create a
.dockerignorefile so the image is lighter. - Add a
.gitignoreif you do not have one yet (avoid committingnode_modules). - Document the scripts you defined in the README so the team remembers them.
Recommended content for .dockerignore:
node_modules
npm-debug.log
.npmrc
.DS_Store10. Step 5, building the Docker image
10.1 Dockerfile explained
FROM node:24-alpine AS base
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "src/server.js"]Key points:
node:24-alpineis lightweight and Linux compatible, ideal for production. If you need a different LTS line, you can usenode:22-alpine.npm ciinstalls exactly the versions defined inpackage-lock.jsonand, with--omit=dev, skips the development dependencies (this flag replaces the old, now deprecated--only=production).EXPOSE 3000documents the port, although the actual publishing is done withdocker run -p.- We do not need to copy
node_modulesbecause they are installed inside the container.
10.2 Build and test the image
docker build -t random-quotes:local .
docker images | grep random-quotesRun a test container:
docker run --rm -d -p 3000:3000 --name random-quotes-test random-quotes:local
docker logs random-quotes-test # check that the server started
curl http://localhost:3000/quotes/random
docker stop random-quotes-test # clean up the containerImportant: if you are on macOS/Windows with Docker Desktop, the port is published on your host machine automatically. On Linux, make sure port 3000 is not already in use.
11. Step 6, publish to Docker Hub
- Create a public repository on Docker Hub (for example
youruser/random-quotes). - Log in from the terminal:
docker login- Tag the local image with your repository and publish it:
docker tag random-quotes:local youruser/random-quotes:1.0.0
docker push youruser/random-quotes:1.0.0
docker tag random-quotes:local youruser/random-quotes:latest
docker push youruser/random-quotes:latestConfirm from the Docker Hub web interface that the image was uploaded correctly.
12. Step 7, introduction to Docker Swarm
Docker Swarm is Docker's native orchestrator. It lets you:
- Create clusters with a
managernode (which orchestrates) and multipleworkernodes (which run containers). - Define services with multiple distributed replicas.
- Balance load and automatically restart containers if they fail.
Remember what we said at the start: Swarm is in maintenance mode and in real production the usual choice is Kubernetes. Even so, it is still the clearest way to learn these concepts.
12.1 Prepare the cluster
- Make sure you have at least one machine (manager). Ideally three nodes (1 manager, 2 workers) to experiment.
- Install Docker on each machine (check your distro's official documentation).
- Initialize Swarm on the manager:
docker swarm init --advertise-addr <manager-IP>Copy the command that gets printed to join workers and run it on each worker machine:
docker swarm join --token <token> <manager-IP>:2377Verify from the manager:
docker node ls12.2 Create an overlay network (optional but recommended)
docker network create --driver overlay random-quotes-net12.3 Deploy the service
docker service create \
--name random-quotes-service \
--replicas 3 \
--publish published=3001,target=3000 \
--network random-quotes-net \
youruser/random-quotes:latest12.4 Monitor the service
docker service ls
docker service ps random-quotes-service
docker service logs random-quotes-serviceTip: if you update the image, use docker service update --image youruser/random-quotes:1.0.1 random-quotes-service.
13. Step 8, validate the Swarm deployment
- Get the public IP of the manager node (or the load balancer if you use one).
- Open
http://<public-IP>:3001/quotes/randomin your browser or withcurl. - Refresh the page several times to confirm the responses vary (the replicas split the requests).
- If you have several machines, send requests from each one to simulate distributed traffic.
When you are done:
docker service rm random-quotes-service
docker network rm random-quotes-net # if you created it
docker swarm leave --force # run on each node, the manager last14. Common troubleshooting
- Port 3000 is in use: change the
PORTenvironment variable (PORT=4000 npm start) or stop the process using it. - `npm install` fails in Docker: make sure you copy
package-lock.jsonand do not edit it by hand. - `docker build` is very slow: check your connection; base images can weigh tens of MB.
- The Swarm service will not start: check
docker service logs. If the image does not exist, the worker may not have internet access or the name may be misspelled. - Workers do not join the cluster: open ports 2377 (control), 7946 TCP/UDP (node to node communication) and 4789 UDP (overlay network) in the firewall.
15. Extra challenges
- Add a POST route so the API accepts new quotes in memory.
- Persistence: store the quotes in a JSON file or a lightweight database (for example, SQLite).
- Testing: set up Jest or Vitest and write tests for the endpoints.
- CI/CD: create a GitHub Actions workflow that builds and publishes the image automatically.
- Observability: integrate a logging middleware like
morganor metrics with Prometheus.
16. Final checklist
npm run devworks and shows logs in the console.- The endpoints return JSON correctly and handle invalid IDs.
- The Docker image builds without errors and the container responds on port 3000.
- The image is published on Docker Hub with at least one semantic tag.
- The service is deployed on Swarm with several replicas and balances load.
- You documented all the important commands in your README or log.
17. Quick glossary
- Node.js: server side JavaScript runtime based on V8.
- Express: minimalist framework for building APIs and web applications in Node.
- npm: package manager that installs dependencies from the official registry.
- Docker Image: immutable template with everything needed to run an application.
- Docker Container: a running instance of an image.
- Docker Swarm: Docker's native clustering and orchestration mode.
- Replica: each copy of a container within a Swarm service.
- Endpoint: a URL exposed by a service that responds to HTTP requests.
18. Reference repository structure
GitHub repository:
random-quotes
├── README.md
├── Dockerfile
├── package.json
├── package-lock.json
├── .dockerignore
└── src
├── app.js
├── quotes.js
└── server.jsThat is all, I hope this guide is useful to you and that you can apply it to a project you have in mind, or that it simply helped you understand how Node.js, Express, Docker and Docker Swarm fit together. Leave me a comment if it helped, if you want to add an opinion, or if you have any questions. And remember, if you liked it, you can also share it using the social links below. Good luck.
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.