How to Run Headless Drupal and NextJS on DDEV
Headless website solutions are becoming increasingly popular because they deliver highly customizable site experiences with HTML and JavaScript. By decoupling a site’s frontend presentation from its backend logic, developers can focus on its backend and frontend strengths separately and meet in the middle with content APIs.
As an API-first content repository, Drupal is ideal for powering headless websites. By default, it serves content via HTML to a webpage. But it also has great serialization capabilities, which don’t require additional development, and can expose your data via API routes as JSON:API formatted data. With Drupal, you can also serve data as XML or regular JSON, .CSV, .TXT, or any valid data format from Encoder classes provided in contributed modules.
Most NodeJS applications run directly on the host system and are usually bound to port 3000. For standalone development, this is typically fine. But when you work in a production environment and/or with Docker-based applications, that setting can cause issues. Fortunately, there are workarounds in Drupal like running NodeJS in a separate container.
In this article, we’ll focus on two ways you can leverage NextJS for Drupal and how you can run it within its own NodeJS container under DDEV.
Method 1: Using the DDEV config.yaml Approach
DDEV is a fantastic Docker-based tool for developing applications because you can configure it in many ways. One way you can run a NodeJS application in DDEV is to define extra ports and daemons for DDEV when a project starts by editing its config.yaml file.
If you want to run a NextJS application within DDEV, the NextJS application runs on “localhost” and port 3000 by default. By adding information into the web_extra_exposed_ports and web_extra_daemons section of config.yaml, you can tell DDEV to open two extra ports and run an extra daemon when a project begins.
In this example, you define an extra port with “NextJS Website Name” as a label, telling DDEV the container port should be port 3000, and the public ports (HTTP and HTTPS) are 9998 and 9999, respectively.
web_extra_exposed_ports:
- name: "NextJS Website Name"
container_port: 3000
http_port: 9998
https_port: 9999
web_extra_daemons:
- name: "nextjs-site"
command: "npm install && npm run dev -- -H 0.0.0.0 -p 3000"
directory: /var/www/html/(nextjs project directory)
The web_extra_daemons section lets you tell DDEV what to do. In the previous example, you defined a “nextjs-site” daemon, the command to run, and what directory to run that command. The directory contains a NextJS codebase, so when “npm install” and “npm run dev” are executed, they're done in that directory.
In this example, you also override the default hostname of the NextJS application from “localhost” to 0.0.0.0. This allows network traffic to see and route to the application successfully. For a short explanation of “why”, here is an excerpt from the DDEV documentation:
“Many examples on the internet show starting daemons starting up and binding to 127.0.0.1 or localhost. Those examples are assuming that network consumers are on the same network interface, but with a DDEV-based solution the network server is essentially on a different computer from the host computer (workstation). If the host computer needs to have connectivity, then bind to 0.0.0.0 (meaning “all network interfaces”) rather than 127.0.0.1 or localhost (which means only allow access from the local network).”
Now, when you open the browser and visit your DDEV site URL with port :9999, you’ll see a NextJS website running inside of DDEV.
Method 2: Create a New Docker Container with Docker Compose
The first method for running NextJS in its own NodeJS container under DDEV will work fine for some scenarios. However, if you want multiple NextJS sites or NodeJS services running, you may run into trouble getting the ports you want or exposing vanity domains for the NextJS sites.
The second method for running NextJS in its own NodeJS container under DDEV involves creating an isolated Docker container in DDEV. This is beneficial for a few reasons:
- The service and processes are isolated to the new Docker container and not shared by the default DDEV web container (from the first method)
- You don’t have to juggle a running list of ports in config.yaml
- You can have a vanity domain for your service
To do this, you can define a Docker Compose file within the .ddev directory of your project. Create a file called docker-compose.site1.yml. Replace “site1” with whatever you want. Within that file you can have the following configuration:
services:
site1:
container_name: ddev-${DDEV_SITENAME}-site1
hostname: ${DDEV_SITENAME}-site1
image: node:20.12.0
working_dir: /var/www/html
user: "node"
command: sh -c 'npm install && npm run dev -- -H 0.0.0.0 -p 3000'
networks: [default, ddev_default]
restart: "no"
environment:
- VIRTUAL_HOST=site1.$DDEV_HOSTNAME
- HTTP_EXPOSE=80:3000
- HTTPS_EXPOSE=443:3000
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: $DDEV_APPROOT
external_links:
- "ddev-router:${DDEV_SITENAME}.${DDEV_TLD}"
volumes:
- ../nextjs-code-directory:/var/www/html
This defines a new service (container) to DDEV using the official NodeJS Docker image. You bind the default HTTP and HTTPS port to port 3000 (where the NodeJS app is running) as well as define a vanity domain of site1.$DDEV_HOSTNAME where $DDEV_HOSTNAME will be replaced dynamically by the hostname of your DDEV application. That means you can access this application in your browser at https://site1.drupalproject.ddev.site, for example.
When the container starts, it executes the command listed. Like before, you’re overriding the NextJS app hostname from “localhost” to 0.0.0.0 so external traffic can be routed to it from the outside, making the application visible to the network. Since this is an isolated container, you can safely bind ports 80/443 to 3000 without interfering with any other services using those ports since this is specific only to this container, something you can’t do with the first method. You could replicate this pattern as much as you need to add other NodeJS applications or NextJS sites.
Headless Drupal
With either of these methods in place, you now have a way to serve both Drupal and NextJS within DDEV. From here you can develop headless sites using the NextJS for Drupal library from the folks at Chapter Three. At Velir, we’ve done some initial R&D around contributing to this library and proofing a concept that allows you to use Layout Builder in a headless, decoupled way. That means your site builders and content authors could do all the content and administration from Drupal and control the output from end-to-end on a headless site. We’ll have more posts on that subject in the coming months.
Need help with either of these methods so you can take advantage of Drupal for your headless website? Contact us. Our Triple Certified Drupal Expert and our Drupal team would be happy to help!