Build a local React catalogue backed by Node.js and MySQL. This walkthrough uses the supplied complete project and separates packaging, networking, persistence and verification. Docker Engine with Compose v2 is required.
Check the tools first
Start with a working Docker installation and the Compose plugin. Check both commands shown here, and make sure the engine is running. Extract the supplied starter into its own directory. Use this as a new local project, not as a replacement for an existing production database. The examples are instructions you can execute; this video isn't a recording of a live container deployment.
docker version
docker compose versionFollow a catalogue request
A browser request reaches Nginx in the web container. Nginx serves the built frontend and forwards catalogue requests to the backend service. Node queries My SQL and returns a list of records. These are separate containers connected through the Compose network. The browser doesn't need to know the internal database hostname, and it never receives the database password. Keep those responsibilities distinct when debugging.
Browser :8080 -> web :80
web -> api :3000
api -> db :3306Read the project structure
The Compose file describes how the services fit together. Each application directory has a Dockerfile defining its image. The frontend directory also contains Nginx configuration, while the database directory contains initial schema and sample data. Read the README before starting. Full source files are included in the download; the slide excerpts explain the important parts rather than replacing the complete configuration.
compose.yaml
api/Dockerfile
web/Dockerfile
web/nginx.conf
database/schema.sqlCreate local secrets
Copy the environment example and replace both password placeholders. Generate different random values for the application account and the database administrator. Restrict the local file's permissions and keep it out of source control. Compose passes the settings into the services that need them. People with access to the Docker engine can inspect container configuration, so this file isn't a dedicated production secrets management system.
cp .env.example .env
openssl rand -hex 24
# Repeat for a different root password.
chmod 600 .envStore data outside the container
The database mounts a named volume at its data directory. This separates persistent records from the disposable container. Replacing the container can retain the volume, which matters when you update an image. A volume isn't a backup: accidental deletion or damaged data can still affect it. Use logical database backups and test restoration before relying on a deployment for important records.
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:Initialise the catalogue
The initial schema creates a small catalogue table and adds three example rows. It also reduces the application account to read access, matching the read-only interface. This initialization belongs to the first creation of a fresh database volume. Editing the schema file later doesn't automatically migrate an existing database. Introduce reviewed migrations for subsequent changes, and don't delete a volume merely to force initialization to run again.
CREATE TABLE kits (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(120) NOT NULL
);
INSERT INTO kits (title) VALUES ('React catalogue'), ('Node service'), ('Persistent MySQL');
REVOKE ALL PRIVILEGES ON catalogue.* FROM 'catalogue'@'%';
GRANT SELECT ON catalogue.* TO 'catalogue'@'%';Use the service hostname
Inside the backend container, the database hostname is db, matching the Compose service name. Localhost would refer to the backend container itself. Service discovery lets you avoid hardcoding changing container addresses. Likewise, Nginx reaches Node by the backend's service name. Distinguish internal service addresses from the address your browser opens on the host machine when tracing a failed request.
DB_HOST: db
DB_NAME: catalogue
DB_USER: catalogueBuild a small Node image
The backend image starts from a Node base image. It copies the dependency manifests first and installs production dependencies using the lockfile. Application source is copied afterwards, and the process runs as the Node user rather than root. Exposing a port in a Dockerfile documents the application port; it doesn't publish that port to your host. Compose controls the external port mapping.
FROM node:24-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node app.mjs server.mjs ./
USER node
EXPOSE 3000
CMD ["node","server.mjs"]Build React before serving it
The frontend uses a multi-stage build. The first stage installs the locked dependencies and compiles React into static files. The second stage contains Nginx, its configuration, and the build output. The development server isn't part of the runtime image. This separates the tools needed to build the interface from the software needed to serve it after deployment.
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY index.html ./
COPY src ./src
RUN npm run build
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/htmlKeep browser requests relative
The frontend sends requests to the A P I path on the same origin that served the page. Nginx forwards that path to the Node container. The browser therefore uses one address while Compose handles internal service names. Read the proxy address carefully because changing slash handling can change which path reaches Node. Test the health and catalogue routes through Nginx, not only inside the backend.
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
location /api/ {
proxy_pass http://api:3000;
proxy_set_header Host $host;
}
location / { try_files $uri $uri/ /index.html; }
}Wait for a healthy dependency
Container startup and application readiness aren't the same event. The database may need time to initialize before it accepts queries. This project uses a database health check and a healthy dependency condition before starting the backend. The frontend also waits for backend health. These checks help initial startup, but they don't replace error handling for a database that becomes unavailable later.
depends_on:
db:
condition: service_healthyValidate and start
Validate the Compose configuration before starting. The quiet option checks it without printing resolved settings that may contain secrets. Then build and start the services in detached mode. Inspect their status rather than assuming that a successful command means every service is ready. The initial database setup can take time. If a health check keeps failing, investigate the relevant service logs before repeatedly rebuilding everything.
docker compose config --quiet
docker compose up --build -d
docker compose psCheck the browser and API
Open the local address in your browser and confirm that the catalogue cards appear. Request the health endpoint and the catalogue endpoint directly as well. The health response should indicate success, and the catalogue response should include the seeded rows. These are expected results to verify on your machine. This local stack publishes only the web service, bound to host loopback for the exercise.
curl -f http://127.0.0.1:8080/api/health
curl -f http://127.0.0.1:8080/api/kits
# Browser: http://127.0.0.1:8080Observe a database outage
Now practise a failure deliberately in this disposable learning environment. Stop the database and request the catalogue. The backend should return a general service unavailable response, rather than database credentials or a raw stack trace. Start the database again, allow it to recover, and retry. Use the interface's retry behaviour too. A useful test includes the recovery path as well as the failure message.
docker compose stop db
curl -i http://127.0.0.1:8080/api/kits
docker compose start dbRecreate without deleting records
Stop the stack without deleting its named volume, then start it again and verify that the records remain. Avoid the volume deletion flag during routine shutdown because it destroys the stored database. Password settings also need care: changing an environment variable doesn't automatically rotate an account already stored in an existing database. Plan account rotation and schema migration as deliberate database operations.
docker compose down
docker compose up -d
# Do not add -v to routine shutdown.Read the failing service logs
Investigate the service responsible for the failing layer. Database logs help with initialization and permissions. Backend logs and health status help with connectivity. Web configuration matters when direct backend requests work but public paths fail. The supplied Node tests use an injected database double to check response behaviour. They passed during preparation, but they don't prove that Docker networking or a real My SQL instance works.
docker compose logs --tail=80 api db web
docker compose ps
node --test api/app.test.mjsRebuild deliberately
For reviewed application changes, rebuild the affected images and recreate the services as needed. Keep the named data volume and verify the public path afterwards. Build-time changes and database migrations have different recovery requirements. Back up before schema changes, and record the application and image versions needed for rollback. For reproducible releases, review and pin suitable image digests rather than relying indefinitely on moving tags.
docker compose up --build -d
# Back up data before schema changes.
# Recheck /api/health and /api/kits.Add the production requirements
This learning configuration isn't a complete public hosting deployment. Add a correctly configured secure reverse proxy, protect administration, and keep the database and backend from unnecessary external exposure. Monitor health, manage dependency updates, and practise restoring backups. Containerization makes packaging consistent, but it doesn't automatically provide access controls, operational ownership, or recovery. Design those around the actual application and its users.
HTTPS reverse proxy
Restricted administration
Monitoring and alerting
Off-server backups + restore testsValidation and references
The two supplied Node tests passed with an injected database double. Docker Engine was unavailable here, so container builds, real MySQL and the live browser workflow still require verification using the README.
Explore CodeKits services.

