Skip to content

Deployment Overview

Zig produces statically-linked, single-binary executables with no runtime dependencies. This makes zzz applications straightforward to deploy — copy the binary, set environment variables, and run.

Build an optimized release binary with:

Terminal window
zig build -Doptimize=ReleaseFast

The resulting binary is in zig-out/bin/. It is a self-contained executable that includes all compiled templates, static assets (if embedded), and vendored C libraries.

FlagDescriptionUse case
-Doptimize=ReleaseFastMaximum runtime performanceProduction servers
-Doptimize=ReleaseSafeOptimized with safety checks (bounds checking, overflow detection)Staging, security-sensitive deployments
-Doptimize=ReleaseSmallMinimize binary sizeContainers, embedded
(none)Debug mode with full safety and debug infoDevelopment only

For most production deployments, ReleaseFast is recommended. Use ReleaseSafe if you prefer catching undefined behavior at the cost of a small performance overhead.

Pass the target environment at build time with the -Denv option:

Terminal window
zig build -Doptimize=ReleaseFast -Denv=prod

The Environment enum recognizes these values:

ValueAliasesDescription
devdevelopmentDevelopment mode (default)
prodproductionProduction mode
stagingStaging/pre-production
testingtestTest suite

zzz uses .env files and system environment variables for runtime configuration. The Env loader follows this precedence (highest to lowest):

  1. System environment variables (via getenv)
  2. .env.{environment} entries (e.g., .env.prod)
  3. .env entries (base)

See the Environment Config page for full details.

  1. Create a .env file with your base configuration:

    Terminal window
    HOST=127.0.0.1
    PORT=4000
    DATABASE_URL=sqlite:data/app.db
  2. Create environment-specific overrides in .env.prod:

    Terminal window
    HOST=0.0.0.0
    PORT=8080
    DATABASE_URL=postgres://deploy@db.internal:5432/myapp_prod
  3. Load and merge in your application:

    const zzz = @import("zzz");
    const AppConfig = struct {
    host: []const u8,
    port: u16,
    database_url: []const u8,
    };
    const defaults = AppConfig{
    .host = "127.0.0.1",
    .port = 4000,
    .database_url = "sqlite:data/dev.db",
    };
    pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    var result = try zzz.configInit(AppConfig, defaults, allocator, .{
    .environment = "prod",
    });
    defer result.env.deinit();
    const config = result.config;
    // config.host == "0.0.0.0" (from .env.prod)
    // config.port == 8080 (from .env.prod)
    }

Before deploying to production, verify each of the following:

  • Build with -Doptimize=ReleaseFast (or ReleaseSafe)
  • Build with -Denv=prod
  • Test the release binary locally before deploying
  • Verify the binary runs without any shared library dependencies (ldd on Linux, otool -L on macOS)
  • All secrets (API keys, database passwords, SMTP credentials) are provided via environment variables or .env.prod, not hardcoded
  • HOST is set to 0.0.0.0 (to bind all interfaces) or the specific interface for your deployment
  • PORT is set appropriately for your reverse proxy or load balancer
  • DATABASE_URL points to the production database
  • Application is behind a reverse proxy (nginx, Caddy, etc.) that handles TLS termination
  • Sensitive environment variables are not logged (the Env.maskSensitive method automatically masks keys containing SECRET, PASSWORD, TOKEN, KEY, DATABASE_URL, or PRIVATE)
  • CORS, CSRF, and rate limiting middleware are configured for production
  • Health check endpoint is available (e.g., GET /health)
  • Application logs are directed to a persistent location or log aggregator
  • Process manager (systemd, Docker, etc.) is configured to restart on failure

The simplest approach. Copy the binary and run it:

Terminal window
# Build
zig build -Doptimize=ReleaseFast -Denv=prod
# Deploy
scp zig-out/bin/myapp server:/opt/myapp/
ssh server 'cd /opt/myapp && PORT=8080 DATABASE_URL=postgres://... ./myapp'

Use a process manager like systemd to keep it running:

/etc/systemd/system/myapp.service
[Unit]
Description=My zzz application
After=network.target
[Service]
Type=simple
ExecStart=/opt/myapp/myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env.prod
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Terminal window
sudo systemctl enable --now myapp

The DatabaseUrl parser understands PostgreSQL and SQLite URL formats:

Terminal window
# PostgreSQL
DATABASE_URL=postgres://user:password@host:5432/dbname
DATABASE_URL=postgresql://deploy@db.internal/myapp_prod
# SQLite
DATABASE_URL=sqlite:data/app.db
DATABASE_URL=data/app.db # bare filename treated as SQLite

The parser is zero-allocation — all returned slices point into the original URL string. For PostgreSQL, toConninfo() generates a libpq-compatible connection string.