--- title: Use containers for Node.js development linkTitle: Develop your app weight: 30 keywords: node, node.js, development description: Learn how to develop your Node.js application locally using containers. aliases: - /get-started/nodejs/develop/ - /language/nodejs/develop/ - /guides/language/nodejs/develop/ --- ## Prerequisites Complete [Containerize a Node.js application](containerize.md). ## Overview In this section, you'll learn how to set up a development environment for your containerized application. This includes: - Adding a local database and persisting data - Configuring your container to run a development environment - Debugging your containerized application ## Add a local database and persist data The application uses PostgreSQL for data persistence. Add a database service to your Docker Compose configuration. ### Add database service to Docker Compose 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: ```yaml 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 ``` ### Update your application service Make sure your application service in `compose.yml` is configured to connect to the database: ```yaml {hl_lines="18-20,42-44",collapse=true,title=compose.yml} 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 ``` 1. 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. 1. Configure your environment by copying the example file: ```console $ cp .env.example .env ``` Update the `.env` file with your preferred settings: ```env # 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 ``` 1. Run the following command to start your application in development mode: ```console $ docker compose up app-dev --build ``` 1. Open a browser and verify that the application is running at [http://localhost:5173](http://localhost:5173) for the frontend or [http://localhost:3000](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. 1. Add some items to the todo list to test data persistence. 1. After adding some items to the todo list, press `CTRL + C` in the terminal to stop your application. 1. Run the application again: ```console $ docker compose up app-dev ``` 1. Refresh [http://localhost:5173](http://localhost:5173) in your browser and verify that the todo items persisted, even after the containers were removed and ran again. ## Configure and run a development container 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](/manuals/engine/storage/_index.md). In addition to adding a bind mount, you can configure your Dockerfile and `compose.yaml` file to install development dependencies and run development tools. ### Update your Dockerfile for development 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: ```dockerfile {hl_lines="5-26",collapse=true,title=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: - Installs all dependencies including dev dependencies - Exposes ports for the API server (3000), Vite dev server (5173), and Node.js debugger (9229) - Runs `npm run dev` which starts both the Express server and Vite dev server concurrently - Includes health checks for monitoring container status Next, you'll need to update your Compose file to use the new stage. ### Update your Compose file for development Update your `compose.yml` file to run the development stage with bind mounts for hot reloading: ```yaml {hl_lines=[5,8-10,20-27],collapse=true,title=compose.yml} 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: - **Multi-port exposure**: API server (3000), Vite dev server (5173), and debugger (9229) - **Comprehensive bind mounts**: Source code, configuration files, and package files for hot reloading - **Environment variables**: Configurable through `.env` file or defaults - **PostgreSQL database**: Production-ready database with persistent storage - **Docker Compose watch**: Automatic file synchronization and container rebuilds - **Health checks**: Database health monitoring with automatic dependency management ### Run your development container and debug your application Run the following command to run your application with the development configuration: ```console $ docker compose up app-dev --build ``` Or with file watching for automatic updates: ```console $ docker compose up app-dev --watch ``` For local development without Docker: ```console $ npm run dev:with-db ``` Or start services separately: ```console $ npm run db:start # Start PostgreSQL container $ npm run dev # Start both server and client ``` ### Using Task Runner (alternative) The project includes a Taskfile.yml for advanced workflows: ```console # 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: - **API Server**: [http://localhost:3000](http://localhost:3000) - Express.js backend with REST API - **Frontend**: [http://localhost:5173](http://localhost:5173) - Vite dev server with hot module replacement - **Health Check**: [http://localhost:3000/health](http://localhost:3000/health) - Application health status 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: 1. Open `src/client/components/TodoApp.tsx` in an IDE or text editor. 1. Update the main heading text: ```diff -

- Modern Todo App -

+

+ My Todo App +

``` 1. 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`). ### VS Code debugger setup 1. Create a launch configuration in `.vscode/launch.json`: ```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": ["/**"] } ] } ``` 1. Start your development container: ```console docker compose up app-dev --build ``` 1. Attach the debugger: - Open VS Code - From the Debug panel (Ctrl/Cmd + Shift + D), select **Attach to Docker Container** from the drop-down - Select the green play button or press F5 ### Chrome DevTools (alternative) You can also use Chrome DevTools for debugging: 1. Start your container (if not already running): ```console docker compose up app-dev --build ``` 1. Open Chrome and go to `chrome://inspect`. 1. From the **Configure** option, add: ```text localhost:9229 ``` 1. When your Node.js target appears, select **inspect**. ### Debugging configuration details The debugger configuration: - **Container port**: 9230 (internal debugger port) - **Host port**: 9229 (mapped external port) - **Script**: `tsx watch --inspect=0.0.0.0:9230 src/server/index.ts` The 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. ### Troubleshooting debugger connection If the debugger doesn't connect: 1. Check if the container is running: ```console docker ps ``` 1. Check if the port is exposed: ```console docker port todoapp-dev ``` 1. Check container logs: ```console docker compose logs app-dev ``` You should see a message like: ```text 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](https://nodejs.org/en/docs/guides/debugging-getting-started). ## Summary 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: - [Volumes top-level element](/reference/compose-file/volumes/) - [Services top-level element](/reference/compose-file/services/) - [Multi-stage builds](/manuals/build/building/multi-stage.md) ## Next steps In the next section, you'll learn how to run unit tests using Docker.