workerd is designed to be unopinionated about how it runs. For production deployments on Linux, systemd provides robust process management, automatic restarts, and socket activation for privileged ports.
Why systemd?
systemd offers several advantages for production workerd deployments:
- Socket activation: Open privileged ports (80, 443) without running as root
- Process supervision: Automatic restart on crashes
- Resource limits: Control CPU, memory, and file descriptor usage
- Logging integration: Structured logs via journald
- Security hardening: Sandboxing and capability restrictions
Basic deployment
Service file
Create /etc/systemd/system/workerd.service:
[Unit]
Description=workerd runtime
After=local-fs.target remote-fs.target network-online.target
Requires=local-fs.target remote-fs.target workerd.socket
Wants=network-online.target
[Service]
Type=exec
ExecStart=/usr/bin/workerd serve /etc/workerd/config.capnp --socket-fd http=3 --socket-fd https=4
Sockets=workerd.socket
# If workerd crashes, restart it.
Restart=always
# Run under an unprivileged user account.
User=nobody
Group=nogroup
# Hardening measure: Do not allow workerd to run suid-root programs.
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
Socket file
Create /etc/systemd/system/workerd.socket:
[Unit]
Description=sockets for workerd
PartOf=workerd.service
[Socket]
ListenStream=0.0.0.0:80
ListenStream=0.0.0.0:443
[Install]
WantedBy=sockets.target
Configuration file
Create /etc/workerd/config.capnp:
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [
(name = "main", worker = .mainWorker),
],
sockets = [
# Socket passed from systemd via fd 3
( name = "http",
address = "*:80",
http = (),
service = "main"
),
# Socket passed from systemd via fd 4
( name = "https",
address = "*:443",
http = (
tlsOptions = (
keypair = (
privateKey = embed "/etc/workerd/ssl/private.pem",
certificateChain = embed "/etc/workerd/ssl/cert.pem",
),
),
),
service = "main"
),
]
);
const mainWorker :Workerd.Worker = (
serviceWorkerScript = embed "/etc/workerd/workers/main.js",
compatibilityDate = "2024-01-01",
);
Enable and start
# Reload systemd configuration
sudo systemctl daemon-reload
# Enable socket activation (starts on boot)
sudo systemctl enable workerd.socket
# Start the socket
sudo systemctl start workerd.socket
# Check status
sudo systemctl status workerd.socket
sudo systemctl status workerd.service
The service starts automatically when the first connection is made to the socket, thanks to systemd’s socket activation.
Socket activation explained
The --socket-fd flag tells workerd to use file descriptors passed from systemd:
--socket-fd http=3 --socket-fd https=4
This maps:
- File descriptor 3 → socket named “http” in config
- File descriptor 4 → socket named “https” in config
Systemd opens the sockets with elevated privileges, then passes them to workerd running as an unprivileged user.
Security hardening
Additional service restrictions
Add these to your service file for enhanced security:
[Service]
# Existing configuration...
# Security hardening
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/workerd
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
RemoveIPC=true
PrivateMounts=true
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
# Resource limits
LimitNOFILE=65536
LimitNPROC=512
Create dedicated user
Instead of using nobody, create a dedicated user:
# Create workerd user
sudo useradd --system --no-create-home --shell /bin/false workerd
# Update service file
# User=workerd
# Group=workerd
File permissions
# Set correct ownership
sudo chown -R workerd:workerd /etc/workerd
sudo chmod 750 /etc/workerd
sudo chmod 640 /etc/workerd/config.capnp
# Protect SSL keys
sudo chmod 600 /etc/workerd/ssl/private.pem
Resource limits
Memory limits
[Service]
# Limit memory to 2GB
MemoryMax=2G
MemoryHigh=1.8G
CPU limits
[Service]
# Limit to 50% of one CPU core
CPUQuota=50%
File descriptor limits
[Service]
# Increase file descriptor limit
LimitNOFILE=65536
Logging
View logs
# Follow logs in real-time
sudo journalctl -u workerd.service -f
# View recent logs
sudo journalctl -u workerd.service -n 100
# Filter by time
sudo journalctl -u workerd.service --since "1 hour ago"
Structured logging
workerd outputs to stderr. Configure journald to capture:
[Service]
StandardOutput=journal
StandardError=journal
Log rotation
Configure /etc/systemd/journald.conf:
[Journal]
SystemMaxUse=500M
SystemMaxFileSize=100M
SystemMaxFiles=5
Health checks
Systemd health monitoring
Add a health check script:
#!/bin/bash
# /usr/local/bin/workerd-health-check
curl -f http://localhost/health || exit 1
Configure in service file:
[Service]
ExecStartPost=/usr/local/bin/workerd-health-check
Restart on failure
[Service]
Restart=on-failure
RestartSec=5s
StartLimitBurst=3
StartLimitInterval=60s
Multiple instances
workerd is single-threaded. To utilize multiple cores, run multiple instances:
Service template
Create /etc/systemd/system/[email protected]:
[Unit]
Description=workerd runtime instance %i
After=local-fs.target remote-fs.target network-online.target
PartOf=workerd.target
[Service]
Type=exec
ExecStart=/usr/bin/workerd serve /etc/workerd/config.capnp --socket-fd http=3
Sockets=workerd@%i.socket
Restart=always
User=workerd
Group=workerd
NoNewPrivileges=true
[Install]
WantedBy=workerd.target
Socket template
Create /etc/systemd/system/[email protected]:
[Unit]
Description=workerd socket instance %i
PartOf=workerd@%i.service
[Socket]
ListenStream=127.0.0.1:808%i
ReusePort=true
[Install]
WantedBy=sockets.target
Target file
Create /etc/systemd/system/workerd.target:
Start instances
# Start 4 instances on ports 8080, 8081, 8082, 8083
sudo systemctl enable workerd.target
sudo systemctl start workerd.target
# Check all instances
sudo systemctl status 'workerd@*'
Load balancer
Use nginx or haproxy to distribute load:
upstream workerd_backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
server {
listen 80;
location / {
proxy_pass http://workerd_backend;
}
}
Updates and rollbacks
Zero-downtime updates
# Update workerd binary
sudo cp workerd-new /usr/bin/workerd
# Reload without dropping connections (if supported)
sudo systemctl reload-or-restart workerd.service
Rollback
# Restore previous binary
sudo cp /usr/bin/workerd.backup /usr/bin/workerd
sudo systemctl restart workerd.service
Monitoring
Metrics collection
workerd doesn’t include built-in metrics. Collect metrics from:
- Application logs: Parse workerd’s stderr output
- System metrics: CPU, memory, network via systemd
- Health endpoint: Expose metrics in your worker
Example health endpoint
export default {
async fetch(request) {
if (new URL(request.url).pathname === "/health") {
return new Response("OK", { status: 200 });
}
// Handle other requests
}
};
Troubleshooting
Service won’t start
# Check service status
sudo systemctl status workerd.service
# View detailed logs
sudo journalctl -xeu workerd.service
# Test config manually
sudo -u workerd /usr/bin/workerd serve /etc/workerd/config.capnp
Socket activation issues
# Verify socket is listening
sudo systemctl status workerd.socket
sudo ss -tlnp | grep workerd
# Test socket activation
curl http://localhost:80
Permission denied
# Check file ownership
ls -la /etc/workerd/
# Fix permissions
sudo chown -R workerd:workerd /etc/workerd
Best practices
- Use socket activation: Avoid running workerd as root
- Enable security hardening: Use all available systemd security features
- Run multiple instances: One per CPU core for maximum throughput
- Monitor actively: Set up health checks and alerting
- Plan for updates: Test configuration changes before deploying
- Backup configs: Version control your workerd configurations
- Rotate logs: Configure appropriate journald retention
Reference