Skip to content

Deployment

Running on zio

The examples in these docs run on init.io, the std.Io.Threaded implementation from the standard library. It gives every connection an OS thread of its own, and a second one for its timeouts. A handler waiting on a database keeps its thread busy until the answer comes back. You can use it for development and small servers.

For production, run the server on zio. It runs each connection as a coroutine, and many coroutines share a few OS threads.

Add it to the project:

$ zig fetch --save "git+https://github.com/lalinsky/zio"

In build.zig, import it into your module and into Dusty's module. Dusty uses it for its timeouts:

const zio = b.dependency("zio", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("zio", zio.module("zio"));
dusty_dep.module("dusty").addImport("zio", zio.module("zio"));

Then create the zio runtime in main, and pass its io to the server instead of init.io:

const zio = @import("zio");

pub const std_options_debug_io = zio.debug_io;

pub fn main(init: std.process.Init) !void {
    var rt = try zio.Runtime.init(init.gpa, .{ .executors = .auto });
    defer rt.deinit();
    const io = rt.io();

    var app: App = .{};
    var server = http.Server(App).init(init.gpa, io, .{}, &app);
    defer server.deinit();

    // ...
}

.executors = .auto runs one executor thread per CPU. Without it, zio runs everything on the main thread.

std_options_debug_io makes logging and std.debug.print go through zio too. Without it, a task writing a log line can block a whole executor thread while it waits for stderr.

Pass the same io to anything else that does I/O, like a database client, so it runs on zio too.

Graceful Shutdown

server.run() serves until its task is canceled. Then it stops accepting connections, waits for the requests in flight to finish, and returns. This is from examples/todo. It runs the server in a task and cancels it on Ctrl-C or SIGTERM. systemd and Docker send SIGTERM when they stop a service:

var interrupt = try zio.Signal.init(.interrupt);
defer interrupt.deinit();
var terminate = try zio.Signal.init(.terminate);
defer terminate.deinit();

var server_task = try zio.spawn(http.Server(App).run, .{&server});
defer server_task.cancel();

switch (try zio.select(.{ .interrupt = &interrupt, .terminate = &terminate, .server = &server_task })) {
    .server => |result| return result,
    else => std.log.info("Shutting down", .{}),
}

The server waits up to timeout.shutdown, 30 seconds by default, and then cancels the requests that are still running. WebSocket and event stream handlers run until the connection closes, so they keep the shutdown waiting until the timeout. Docker kills the process 10 seconds after SIGTERM by default, so raise its stop_grace_period or lower timeout.shutdown. If the server stops on its own, main returns its error.

Release Builds

Build with optimizations for production:

$ zig build -Doptimize=ReleaseSafe

Logging

Dusty logs under the .dusty scope. To change how much it logs, set a level for that scope in std_options:

pub const std_options: std.Options = .{
    .log_scope_levels = &.{.{ .scope = .dusty, .level = .warn }},
};

Behind a Reverse Proxy

Timeouts and connection limits are on by default, and the server can run without a proxy. For public traffic, we still recommend running it behind a reverse proxy or an HTTP load balancer, like nginx, HAProxy or the one from your cloud provider. The proxy can take care of TLS, and it deals with the common attacks before they reach the server, like floods of connections or clients that send their requests very slowly. Most proxies also have rate limiting and circuit breaking, which stops sending requests to a server that's failing or overloaded.

See the nginx and Caddy guides for how to set one up. nginx needs a few more lines for WebSocket, and proxy_buffering off for event streams.

Behind a proxy, set trusted_proxy_hops, so req.remote_address is the client's address instead of the proxy's. See Configuration.

Next Steps