📝 Josh's Notes

Docker Compose Setup for a Dev Environment

An example of how to set up a development environment in Docker using docker compose. These are NOT builds for production, and therefore use a Docker build stage identified as DEV. (You can call this whatever you want really). Using Docker build stages allows you to cleanly add on to the Dockerfile later for a production build.

The example is for a React Vite app with an Express backend.

Vite App

package.json

Ensure you add --host to your dev script. This will allow you to access the app from outside of the container.

1  "scripts": {
2    "dev": "vite --host", // <-- Ensure you add --host
3    "build": "vite build",
4    "lint": "eslint .",
5    "preview": "vite preview"
6  },

Dockerfile

The dockerfile is straight forward. We don’t need to copy any files because we will pass everything in with a bind mount.

1FROM node:24-alpine as DEV
2WORKDIR /src/app
3CMD ["npm", "run", "dev"]

Express App (API)

package.json

For the API, we’ll need to create a dev script that will run the app via nodemon. We also need a build-db script that will run database migrations and seeds.

1  "scripts": {
2    "start": "node server.js",
3    "dev": "nodemon server.js",
4    "build-db": "npx knex migrate:latest && npx knex seed:run"
5  },

Dockerfile

Then in the Dockerfile, we run both of those scripts with the && shell operator. Using JSON notation like the Vite app’s Dockerfile is recommended and your editor may yell at you for not using it (as I don’t in this example). But the shell operator won’t work in JSON notation, so I do it this way.

1FROM node:24-alpine as DEV
2WORKDIR /src/app
3CMD npm run build-db && npm run dev

.env file

Ensure you have a .env file for any environment variables that your code may be using. Here’s my example:

VITE_API_PROTO=http
VITE_API_HOST=localhost
VITE_API_PORT=3001

NODE_ENV=development
CORS_ORIGINS=http://localhost:5173
JWT_SECRET=supersecretjwtsaucethatnobodycanguess
COOKIE_SECRET=supersecretcookiesaucethatnobodycanguess

DB_NAME=clearminder
DB_HOST=db
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=docker

Docker Compose

Finally, here’s the docker compose that runs everything, referencing environment variables from the .env file. Notice that API and UI (vite) containers have volumes (bind mounts) to the code in the repo. This is why we don’t need to run a COPY or npm install operation within the Dockerfile.

 1---
 2services:
 3  db:
 4    image: postgres
 5    hostname: clearminder-db
 6    container_name: clearminder-db
 7    restart: unless-stopped
 8    # set shared memory limit when using docker compose
 9    shm_size: 128mb
10    environment:
11      POSTGRES_USER: ${DB_USER}
12      POSTGRES_PASSWORD: ${DB_PASSWORD}
13      POSTGRES_DB: ${DB_NAME}
14    ports:
15      - "5432:5432"
16    volumes:
17      - "./dev-pg-data:/var/lib/postgresql"
18  api:
19    build:
20      context: api/
21      target: DEV
22    hostname: clearminder-api
23    container_name: clearminder-api
24    restart: unless-stopped
25    ports:
26      - "3001:3001"
27    volumes:
28      - "./api:/src/app"
29    environment:
30      - CORS_ORIGINS=${CORS_ORIGINS}
31      - JWT_SECRET=${JWT_SECRET}
32      - COOKIE_SECRET=${COOKIE_SECRET}
33      - DB_USER=${DB_USER}
34      - DB_PASSWORD=${DB_PASSWORD}
35      - DB_NAME=${DB_NAME}
36      - DB_HOST=${DB_HOST}
37      - DB_PORT=${DB_PORT}
38    depends_on:
39      - db
40  ui:
41    build:
42      context: ui/
43      target: DEV
44    hostname: clearminder
45    container_name: clearminder
46    restart: unless-stopped
47    ports:
48      - "5173:5173"
49    environment:
50      - VITE_API_PROTO=${VITE_API_PROTO}
51      - VITE_API_HOST=${VITE_API_HOST}
52      - VITE_API_PORT=${VITE_API_PORT}
53    volumes:
54      - "./ui:/src/app"
55    depends_on:
56      - db
57      - api

#docker #programming