content/guides/nodejs/develop.md
Complete Containerize a Node.js application.
In this section, you'll learn how to set up a development environment for your containerized application. This includes:
The application uses PostgreSQL for data persistence. Add a database service to your Docker Compose configuration.
If you haven't already created a compose.yml file in the previous section, or if you need to add the database service, update your compose.yml file to include the PostgreSQL database service:
services:
# ... existing app services ...
# ========================================
# PostgreSQL Database Service
# ========================================
db:
image: postgres:18-alpine
container_name: todoapp-db
environment:
POSTGRES_DB: '${POSTGRES_DB:-todoapp}'
POSTGRES_USER: '${POSTGRES_USER:-todoapp}'
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}'
volumes:
- postgres_data:/var/lib/postgresql
ports:
- '${DB_PORT:-5432}:5432'
restart: unless-stopped
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-todoapp} -d ${POSTGRES_DB:-todoapp}']
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
networks:
- todoapp-network
# ========================================
# Volume Configuration
# ========================================
volumes:
postgres_data:
name: todoapp-postgres-data
driver: local
# ========================================
# Network Configuration
# ========================================
networks:
todoapp-network:
name: todoapp-network
driver: bridge
Make sure your application service in compose.yml is configured to connect to the database:
services:
app-dev:
build:
context: .
dockerfile: Dockerfile
target: development
container_name: todoapp-dev
ports:
- '${APP_PORT:-3000}:3000' # API server
- '${VITE_PORT:-5173}:5173' # Vite dev server
- '${DEBUG_PORT:-9229}:9229' # Node.js debugger
environment:
NODE_ENV: development
DOCKER_ENV: 'true'
POSTGRES_HOST: db
POSTGRES_PORT: 5432
POSTGRES_DB: todoapp
POSTGRES_USER: todoapp
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}'
ALLOWED_ORIGINS: '${ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:5173}'
volumes:
- ./src:/app/src:ro
- ./package.json:/app/package.json
- ./vite.config.ts:/app/vite.config.ts:ro
- ./tailwind.config.js:/app/tailwind.config.js:ro
- ./postcss.config.js:/app/postcss.config.js:ro
depends_on:
db:
condition: service_healthy
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- '**/*.test.*'
- '**/__tests__/**'
- action: rebuild
path: ./package.json
- action: sync
path: ./vite.config.ts
target: /app/vite.config.ts
- action: sync
path: ./tailwind.config.js
target: /app/tailwind.config.js
- action: sync
path: ./postcss.config.js
target: /app/postcss.config.js
restart: unless-stopped
networks:
- todoapp-network
db:
image: postgres:18-alpine
container_name: todoapp-db
environment:
POSTGRES_DB: '${POSTGRES_DB:-todoapp}'
POSTGRES_USER: '${POSTGRES_USER:-todoapp}'
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}'
volumes:
- postgres_data:/var/lib/postgresql
ports:
- '${DB_PORT:-5432}:5432'
restart: unless-stopped
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-todoapp} -d ${POSTGRES_DB:-todoapp}']
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
networks:
- todoapp-network
volumes:
postgres_data:
name: todoapp-postgres-data
driver: local
networks:
todoapp-network:
name: todoapp-network
driver: bridge
The PostgreSQL database configuration is handled automatically by the application. The database is created and initialized when the application starts, with data persisted using the postgres_data volume.
Configure your environment by copying the example file:
$ cp .env.example .env
Update the .env file with your preferred settings:
# Application Configuration
NODE_ENV=development
APP_PORT=3000
VITE_PORT=5173
DEBUG_PORT=9230
# Database Configuration
POSTGRES_HOST=db
POSTGRES_PORT=5432
POSTGRES_DB=todoapp
POSTGRES_USER=todoapp
POSTGRES_PASSWORD=todoapp_password
# Security Configuration
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
Run the following command to start your application in development mode:
$ docker compose up app-dev --build
Open a browser and verify that the application is running at http://localhost:5173 for the frontend or http://localhost:3000 for the API. The React frontend is served by Vite dev server on port 5173, with API calls proxied to the Express server on port 3000.
Add some items to the todo list to test data persistence.
After adding some items to the todo list, press CTRL + C in the terminal to stop your application.
Run the application again:
$ docker compose up app-dev
Refresh http://localhost:5173 in your browser and verify that the todo items persisted, even after the containers were removed and ran again.
You can use a bind mount to mount your source code into the container. The container can then see the changes you make to the code immediately, as soon as you save a file. This means that you can run processes, like nodemon, in the container that watch for filesystem changes and respond to them. To learn more about bind mounts, see Storage overview.
In addition to adding a bind mount, you can configure your Dockerfile and compose.yaml file to install development dependencies and run development tools.
Your Dockerfile should be configured as a multi-stage build with separate stages for development, production, and testing. If you followed the previous section, your Dockerfile already includes a development stage that has all development dependencies and runs the application with hot reload enabled.
Here's the development stage from your multi-stage Dockerfile:
# ========================================
# Development Stage
# ========================================
FROM build-deps AS development
# Set environment
ENV NODE_ENV=development \
NPM_CONFIG_LOGLEVEL=warn
# Copy source files
COPY . .
# Ensure all directories have proper permissions
RUN mkdir -p /app/node_modules/.vite && \
chown -R nodejs:nodejs /app && \
chmod -R 755 /app
# Switch to non-root user
USER nodejs
# Expose ports
EXPOSE 3000 5173 9229
# Start development server
CMD ["npm", "run", "dev:docker"]
The development stage:
npm run dev which starts both the Express server and Vite dev server concurrentlyNext, you'll need to update your Compose file to use the new stage.
Update your compose.yml file to run the development stage with bind mounts for hot reloading:
services:
app-dev:
build:
context: .
dockerfile: Dockerfile
target: development
container_name: todoapp-dev
ports:
- '${APP_PORT:-3000}:3000' # API server
- '${VITE_PORT:-5173}:5173' # Vite dev server
- '${DEBUG_PORT:-9229}:9229' # Node.js debugger
environment:
NODE_ENV: development
DOCKER_ENV: 'true'
POSTGRES_HOST: db
POSTGRES_PORT: 5432
POSTGRES_DB: todoapp
POSTGRES_USER: todoapp
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-todoapp_password}'
ALLOWED_ORIGINS: '${ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:5173}'
volumes:
- ./src:/app/src:ro
- ./package.json:/app/package.json
- ./vite.config.ts:/app/vite.config.ts:ro
- ./tailwind.config.js:/app/tailwind.config.js:ro
- ./postcss.config.js:/app/postcss.config.js:ro
depends_on:
db:
condition: service_healthy
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- '**/*.test.*'
- '**/__tests__/**'
- action: rebuild
path: ./package.json
- action: sync
path: ./vite.config.ts
target: /app/vite.config.ts
- action: sync
path: ./tailwind.config.js
target: /app/tailwind.config.js
- action: sync
path: ./postcss.config.js
target: /app/postcss.config.js
restart: unless-stopped
networks:
- todoapp-network
Key features of the development configuration:
.env file or defaultsRun the following command to run your application with the development configuration:
$ docker compose up app-dev --build
Or with file watching for automatic updates:
$ docker compose up app-dev --watch
For local development without Docker:
$ npm run dev:with-db
Or start services separately:
$ npm run db:start # Start PostgreSQL container
$ npm run dev # Start both server and client
The project includes a Taskfile.yml for advanced workflows:
# Development
$ task dev # Start development environment
$ task dev:build # Build development image
$ task dev:run # Run development container
# Production
$ task build # Build production image
$ task run # Run production container
$ task build-run # Build and run in one step
# Testing
$ task test # Run all tests
$ task test:unit # Run unit tests with coverage
$ task test:lint # Run linting
# Kubernetes
$ task k8s:deploy # Deploy to Kubernetes
$ task k8s:status # Check deployment status
$ task k8s:logs # View pod logs
# Utilities
$ task clean # Clean up containers and images
$ task health # Check application health
$ task logs # View container logs
The application will start with both the Express API server and Vite development server:
Any changes to the application's source files on your local machine will now be immediately reflected in the running container thanks to the bind mounts.
Try making a change to test hot reloading:
Open src/client/components/TodoApp.tsx in an IDE or text editor.
Update the main heading text:
- <h1 className="text-3xl font-bold text-gray-900 mb-8">
- Modern Todo App
- </h1>
+ <h1 className="text-3xl font-bold text-gray-900 mb-8">
+ My Todo App
+ </h1>
Save the file and the Vite dev server will automatically reload the page with your changes.
Debugging support:
You can connect a debugger to your application on port 9229. The Node.js inspector is enabled with --inspect=0.0.0.0:9230 in the development script (dev:server).
Create a launch configuration in .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Docker Container",
"type": "node",
"request": "attach",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app",
"protocol": "inspector",
"restart": true,
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"]
}
]
}
Start your development container:
docker compose up app-dev --build
Attach the debugger:
You can also use Chrome DevTools for debugging:
Start your container (if not already running):
docker compose up app-dev --build
Open Chrome and go to chrome://inspect.
From the Configure option, add:
localhost:9229
When your Node.js target appears, select inspect.
The debugger configuration:
tsx watch --inspect=0.0.0.0:9230 src/server/index.tsThe debugger listens on all interfaces (0.0.0.0) inside the container on port 9230 and is accessible on port 9229 from your host machine.
If the debugger doesn't connect:
Check if the container is running:
docker ps
Check if the port is exposed:
docker port todoapp-dev
Check container logs:
docker compose logs app-dev
You should see a message like:
Debugger listening on ws://0.0.0.0:9230/...
Now you can set breakpoints in your TypeScript source files and debug your containerized Node.js application.
For more details about Node.js debugging, see the Node.js documentation.
You've set up your Compose file with a PostgreSQL database and data persistence. You also created a multi-stage Dockerfile and configured bind mounts for development.
Related information:
In the next section, you'll learn how to run unit tests using Docker.