Running on localhost:8080 to a Vagrant box, all form submissions redirect to localhost with no port (default 80). I traced the problem to url() returning a port-free URL, because Drupal::urlGenerator has the wrong baseUrl ("http://localhost/") set, because it's based on Request:getPort, which uses $_SERVER['SERVER_PORT'], which is based on the server's port, not the request port. In my case, the Vagrant box accepts requests on 80, which get mapped on my host machine from 8080. This kind of Vagrant setup is maybe a minor use case, but I think the same thing would happen on requests via a proxy server to a different port, a more common use case.
The global $base_url is still correct, since it's based on $_SERVER['HTTP_HOST'], which is tied to the original request. So I'm temporarily working around this by adding $generator->setBaseUrl($base_url . '/'); in url(). I expect there's a better solution somewhere a little deeper down the stack, though I'm not sure where.