I found the reason. Look at this funny code

import vibe.d;
import std.stdio;

void main() {
  runTask({
    while(true) {
      sleep(5.seconds);
      logInfo("working");
    }
  }).join;
  // this point will never be reached
  runEventLoop();
}

After pressing Ctrl+C we exit to console, but program continues working in background like daemon.

Workaround is simple - don't use join() before running event loop:

import vibe.d;
import std.stdio;

void main() {
  runTask({
    runTask({
      while(true) {
        sleep(5.seconds);
        logInfo("working");
      }
    }).join;
  });
  runEventLoop();
}

But that daemonization looks strange.