Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Setting Up Docker and MySQL for React Projects

Setting Up Docker and MySQL

~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Our reviews so far live only in React state – gone after every reload. In this and the next chapter, we'll build our own small server plus a database, so reviews are actually saved permanently. The first step: Docker and a MySQL database.

What exactly is Docker?

A database like MySQL is really a standalone program you'd normally have to laboriously install, configure, and maintain on your computer – with version conflicts, OS differences, and a lot of manual setup. Docker solves this by packaging programs into "containers": ready-configured, isolated mini-environments that run identically on any computer, whether Windows, Mac, or Linux.

Docker termComparison
Image – the "blueprint"/"recipe" for a container, e.g. "MySQL version 8"comparable to a .dmg/.exe installer file, just reproducible and identical for everyone
Container – a RUNNING instance of an imagecomparable to a running program/process – you can start multiple containers from the same image
docker-compose.yml – a config file describing several related containers togethercomparable to a package.json, just for whole programs/services instead of npm packages

Creating docker-compose.yml

In the root folder (at the same level as package.json, NOT inside src/), create the file docker-compose.yml:

docker-compose.yml
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secretpassword
      MYSQL_DATABASE: produktkatalog
    ports:
      - '3306:3306'
    volumes:
      - mysql-data:/var/lib/mysql

volumes:
  mysql-data:

Line by line: image: mysql:8.0 automatically downloads the official, ready-configured MySQL image from Docker Hub (the "app store" for container images). environment sets environment variables that the MySQL image itself reads to automatically create a root password and an empty database called produktkatalog on its very first start. ports: '3306:3306' connects port 3306 INSIDE the container to port 3306 ON your computer, so programs outside the container (our upcoming Node server) can connect. volumes ensures the database files survive a docker compose down, instead of being deleted along with the container.

Starting MySQL

docker compose up -d

-d ("detached") starts the containers in the background instead of blocking your terminal. Verify it's running:

docker compose ps

You should see an entry with status "running" or "Up". To stop it later: docker compose down (the database DATA survives thanks to volumes, only the running container gets stopped).

server/: laying the foundation for our Node server

Our own server lives in its own folder, server/, completely separate from src/ (that's React code that runs in the BROWSER) – the server code, by contrast, runs directly with Node.js, no browser involved. Create server/package.json:

server/package.json
{
  "name": "produktkatalog-server",
  "type": "module",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.19.0",
    "mysql2": "^3.9.0",
    "cors": "^2.8.5"
  }
}

"type": "module" enables modern import/export syntax instead of the older require() – the same syntax you've already been using throughout src/. Install the three packages:

cd server
npm install
cd ..

express is the most common web server framework for Node.js (comparable to Symfony/Laravel routing in the PHP world, just noticeably more minimal). mysql2 is the database driver we use to connect from Node.js to our freshly started MySQL container. cors allows our React app (running on localhost:5173) to make requests to our server (running on a different port) – without cors, the browser blocks that by default for security reasons.

server/db.js: the database connection

server/db.js
import mysql from 'mysql2/promise';

export const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'secretpassword',
  database: 'produktkatalog',
});

mysql2/promise instead of mysql2 matters – the "promise" variant supports async/await from chapter 22, instead of only callback functions. A "pool" automatically manages several reusable database connections in the background, instead of opening and closing a new connection for every request – noticeably faster.

server/server.js: the skeleton

server/server.js
import express from 'express';
import cors from 'cors';
import { pool } from './db.js';

const app = express();
app.use(cors());
app.use(express.json());

app.get('/api/health', async (req, res) => {
  const [rows] = await pool.query('SELECT 1 AS ok');
  res.json({ status: 'ok', databaseConnected: rows[0].ok === 1 });
});

app.listen(4000, () => {
  console.log('Server running at http://localhost:4000');
});

app.use(express.json()) makes Express automatically turn incoming JSON requests into req.body (we'll need that in the next chapter for new reviews). Start the server and test the health check:

cd server
node server.js

In a second terminal (or browser tab), open http://localhost:4000/api/health – you should see {{"status":"ok","databaseConnected":true}}. That confirms: both the Node server AND the MySQL connection are working.

Target project structure (reminder from chapter 3) – server/ is now taking shape

produktkatalog-web/
├── index.html
├── package.json
├── vite.config.js
├── docker-compose.yml            (MySQL container for reviews)
├── server/                       (our own Node/Express server)
│   ├── package.json
│   ├── server.js
│   └── db.js
└── src/
    ├── main.jsx
    ├── App.jsx                   (routing)
    ├── index.css
    ├── api/
    │   ├── magentoApi.js         (Magento REST API: products)
    │   └── reviewsApi.js         (our own server: reviews)
    ├── context/
    │   └── AuthContext.jsx       (simple login state)
    ├── components/
    │   ├── ProductCard.jsx
    │   └── ProtectedRoute.jsx
    ├── hooks/
    │   └── useDocumentTitle.js   (custom hook)
    └── pages/
        ├── ProductListPage.jsx
        ├── ProductDetailPage.jsx
        ├── ReviewsPage.jsx       (nested route)
        ├── LoginPage.jsx
        └── AccountPage.jsx       (protected route)

Achtung: A real password should NEVER go directly into code, as we do here for simplicity – in a real project you'd store credentials in a .env file (NOT checked into your Git repository) and load them via the dotenv package. For this local learning setup, the hard-coded password is a deliberate simplification.