Switching from Nvm to Nodejs_version in DDEV
I opened up WSL2 this morning and ran a ddev command the way I always do, and there was a tip of the day sitting there that changed how I set my projects up
TIP OF THE DAY: These days it makes more sense to use nodejs_version with a full version than using ddev nvm, which is more complex and error-prone. You can now set nodejs_version to any major or minor version, not just currently supported major versions. So nodejs_version: 21.2.0 or nodejs_version: 6 will work.
That’s a small enough message, but it does away with a workaround I’ve been carrying in half my projects for years. So I sat down for the hour and took nvm out of the whole lot of them, and here’s how it went.
Why we were using nvm at all
For a long time, nodejs_version in .ddev/config.yaml would only take a short list of the currently supported major versions. That’s grand until you’re minding an older site whose front-end tooling is pinned to Node 14, or some old npm lockfile that only behaves with the npm version that came with one particular minor release. The major-only setting couldn’t say that, so people reached for the ddev-nvm add-on or wrote their own post-start hooks to get the exact version they were after.
It worked, but it was fiddly. The heart of the problem is that nvm is a shell function, not a proper standalone binary, so it can’t change the environment inside the DDEV container the way you’d expect it to. What that means in practice is that ddev nvm use <version> doesn’t hold. It looks like it took, and then the next ddev npm call quietly goes back to the old version. You end up going around in circles with ddev nvm alias default <version> and post-start hooks just to make it stick from the one command to the next. And anyone new on the team had to learn that same carry-on.
What actually changed
The main thing from that tip is this: nodejs_version now takes any major, minor or full patch version, not only the majors DDEV officially tracks. Underneath, DDEV uses n to install whatever you ask it for, and it understands the same version specifiers n does. Having a read through the DDEV issue queue, one of the maintainers lays out the full range of what’s allowed:
nodejs_version: "21.2.0" # an exact, pinned version
nodejs_version: "v20" # the latest 20.x
nodejs_version: "6" # yes, even an ancient major
nodejs_version: "latest" # whatever the newest release happens to beThat one line covers just about every reason I ever put the nvm add-on in. It’s plain to read, it lives in version control, and it does the same thing for me, for CI, and for anyone who clones the repo. No nvm install step in a hook, and no surprises depending on the shell session you happen to be in.
Moving a project over
Here’s the switch, start to finish. It took me about two minutes a project.
1. Take out the old machinery. If you had the add-on in, remove it:
ddev add-on remove nvmIf you were using post-start hooks to install Node, take those lines out of .ddev/config.yaml. They’d usually look something like this:
hooks:
post-start:
- exec-host: ddev nvm install 14.21.3 && ddev nvm alias default 14.21.32. Set the version you actually want. Either put it straight into the config:
nodejs_version: "14.21.3"Or let DDEV write it for you:
ddev config --nodejs-version=14.21.33. Restart and check it.
ddev restart
ddev exec node -vThat last command should print exactly what you asked for. One thing worth knowing: if the version won’t budge, a cached build is often the reason. A ddev debug refresh or a ddev restart --no-cache will clear it out.
4. Commit .ddev/. However you set the version, commit the change so the whole team gets it without having to think about it.
Two extra bits I was glad to find
While I was in there, I came across a couple of things I wish I’d known a long time ago.
If you already keep your Node version in a file, a .nvmrc, a .node-version, or the engines.node field in package.json, you can point DDEV at it instead of writing the number out twice:
nodejs_version: autoWith auto, DDEV reads the version from .n-node-version, .node-version, .nvmrc, then engines.node, in that order (nodejs_version: engine locks it to engines.node only). It does away with the old mess where the Node version drifts apart across production, staging, CI and local, all because it’s written down in four separate config files. Put it in the one file, point every environment at that file, and the container and every non-DDEV tool are finally reading off the same page.
And if the reason you were wrestling with nvm was really about getting a newer yarn or pnpm, you can skip the whole lot of it with corepack:
ddev config --corepack-enable && ddev restartBut I still use nvm on Ubuntu, does that break anything?
No, it doesn’t. And this is the bit that caught me out for a second, so it’s worth saying plainly.
The nvm on your Ubuntu (or WSL2) host and the Node inside DDEV are two separate installations in two separate places, and neither one knows the other is there. Host nvm looks after the node and npm you get when you type node at your Ubuntu prompt. nodejs_version looks after the node and npm inside the container, the ones you reach with ddev npm, ddev exec node, or after ddev ssh. That tip that started all this was only ever about ddev nvm, which is nvm running inside the container. Your host nvm isn’t touched at all, so carry on using it exactly as you do now.
The one thing to be clear about isn’t breakage, it’s consistency. For a given project, decide where you’re running your Node tooling and stick with it.
- Running
npm install/npm run buildstraight in your Ubuntu shell uses host nvm’s Node, and the container’snodejs_versionhas nothing to do with that work. - Running
ddev npm install/ddev npm run builduses the container’s Node, the one set bynodejs_version.
Where you can run into bother is mixing the two on the one project. A native module built against host Node 20 landing in a node_modules that the container’s Node 18 then tries to load, say, or lockfiles written by different npm versions. That’s not a DDEV quirk, it’s just what happens with any two Node environments that don’t match. So don’t build half of it in the one place and half in the other.
If you want the two of them to agree, there’s a handy way to do it. Keep your .nvmrc for host nvm, then set nodejs_version: auto in DDEV so it reads that same file. Host and container land on the one version, out of the one source.
Was it worth it?
For me, yes, no question about it. The nvm approach was a clever way around a real limitation, but a way around is all it ever was, and it brought complexity into onboarding, into keeping CI in line, and into my own habits. Now the Node version is the one honest line in a YAML file that means the same thing everywhere.
If you’re still carrying ddev-nvm or post-start Node hooks in your projects, the only real reason left to keep them is if you genuinely need more than one Node version inside the one container while it’s running, and the add-on is still there for you if you do. For everyone else, take the workaround out, set nodejs_version, restart, and you’re done.
For once a tip of the day meant taking code out instead of putting more in, and I was glad of it.
