CodeKits.devNew developer offer

Dockerise React, Node.js and MySQL

Package React, Node.js and MySQL with Docker Compose, then check networking, persistent storage, health checks and recovery.

By Jonathan · 9 min read

Published

React, Node.js and MySQL Docker Compose tutorial
In this article

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.

Prepare
docker version
docker compose version

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

Architecture
Browser :8080 -> web :80
web -> api :3000
api -> db :3306

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

Project
compose.yaml
api/Dockerfile
web/Dockerfile
web/nginx.conf
database/schema.sql

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

Configuration
cp .env.example .env
openssl rand -hex 24
# Repeat for a different root password.
chmod 600 .env

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

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

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

Networking
DB_HOST: db
DB_NAME: catalogue
DB_USER: catalogue

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

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

Frontend
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/html

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

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

Readiness
depends_on:
  db:
    condition: service_healthy

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

Run
docker compose config --quiet
docker compose up --build -d
docker compose ps

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

Verify
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:8080

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

Failures
docker compose stop db
curl -i http://127.0.0.1:8080/api/kits
docker compose start db

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

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

Debugging
docker compose logs --tail=80 api db web
docker compose ps
node --test api/app.test.mjs

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

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

Hosting
HTTPS reverse proxy
Restricted administration
Monitoring and alerting
Off-server backups + restore tests

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