Using a ubuntu base image vs an application specific image (node, python, etc)
They're all functionally equivalent.  When you docker push or docker pull an image (or mention it in a Kubernetes manifest), all of its layers are pushed or pulled, so you always get a complete image with all of its dependencies at the point where it was built.  (It turns out the versioned ubuntu images are re-released pretty routinely to include current security patches.)

Building your application outside of Docker was pretty common before there were multi-stage builds.  There's nothing wrong with doing it, if it makes sense.  It makes more sense if your built application artifact is portable (in your example, Webpack JavaScript/HTML/CSS artifacts; Java .jar files work well here too).  This is also pretty easy to plug into an existing CI system that produces the artifacts, and then you just need to wrap them up in an image.

Using prebuilt images like nginx or node, or building your own on top of ubuntu or alpine, is mostly a matter of taste and organizational requirements.  Building an image FROM node will be easier than building your own Node image and can get you running faster; if you start FROM ubuntu then you know exactly who built it and what security updates and packages are in it.

If you're pinning to specific versions of software, the other important thing to be aware of is that the Docker Hub packages eventually stop getting updates.  If you're using node:8 that image isn't being automatically built any more; if you pin to a more specific version, node:12.13 or node:12.14.0 aren't getting updates either.  (As of this writing the Docker Hub node page lists 12.14.1 as the current LTS build.)  That could be an issue if you're forced to take a language runtime update to get an OS-level security fix.