Real-world recipes for deploying, configuring, and managing infrastructure with Pitcrew.
This recipe installs Node.js, copies your application, installs dependencies, and sets up a systemd service.
import "fs"
import "pkg"
import "svc"
import "user"
import "tpl"
host := "10.0.0.5"
app_name := "myapp"
app_port := 3000
# Define the deployment function
fn deploy(name string, port int) {
# Create app user
if !user.exists(name) {
user.create(name)
}
# Install Node.js
pkg.install("nodejs")
# Create app directory and copy files
app_dir := "/opt/#{name}"
`mkdir -p #{app_dir}`
`cp -r ./app/* #{app_dir}/`
# Install dependencies
cd app_dir {
`npm install --production`
}
# Set ownership
`chown -R #{name}:#{name} #{app_dir}`
# Create systemd service
service := tpl.render(
>> [Unit]
>> Description={{name}}
>> After=network.target
>>
>> [Service]
>> User={{name}}
>> WorkingDirectory={{app_dir}}
>> ExecStart=/usr/bin/node index.js
>> Environment=PORT={{port}}
>> Restart=always
>>
>> [Install]
>> WantedBy=multi-user.target,
{"name": name, "app_dir": app_dir, "port": "#{port}"}
)
fs.write("/etc/systemd/system/#{name}.service", service) catch {|err|
print("Failed to write service: " + err)
}
# Enable and start
`systemctl daemon-reload`
svc.enable(name)
svc.restart(name)
print("Deployed #{name} on port #{port}")
}
# Run on the remote host
ssh deploy(app_name, app_port), host: host, user: "root"
The ssh keyword serializes the function and all captured values, then runs them on the remote host. Outer variables like app_name and app_port are available as read-only constants inside the SSH session.
Install nginx and set up a reverse proxy to a backend application.
import "fs"
import "pkg"
import "svc"
import "tpl"
host := "10.0.0.5"
domain := "myapp.example.com"
backend_port := 3000
fn setup_nginx(domain string, port int) {
# Install nginx
pkg.install("nginx")
# Generate config from template
config := tpl.render(
>> server {
>> listen 80;
>> server_name {{domain}};
>>
>> location / {
>> proxy_pass http://127.0.0.1:{{port}};
>> proxy_set_header Host $host;
>> proxy_set_header X-Real-IP $remote_addr;
>> proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
>> proxy_set_header X-Forwarded-Proto $scheme;
>> }
>> },
{"domain": domain, "port": "#{port}"}
)
# Write config and remove default
fs.write("/etc/nginx/sites-available/#{domain}", config) catch {|err|
print("Failed: " + err)
}
`ln -sf /etc/nginx/sites-available/#{domain} /etc/nginx/sites-enabled/#{domain}`
# Test config and reload
test_result := $`nginx -t`
if test_result.status != 0 {
print("Nginx config test failed: " + test_result.stderr)
return
}
svc.enable("nginx")
svc.restart("nginx")
print("Nginx configured for #{domain} -> :#{port}")
}
ssh setup_nginx(domain, backend_port), host: host, user: "root"
The ! after a backtick command gives you a struct with stdout, stderr, and status fields instead of raising on non-zero exit.
Configure a primary PostgreSQL server and a streaming replica.
import "fs"
import "pkg"
import "svc"
import "user"
primary_host := "10.0.0.10"
replica_host := "10.0.0.11"
# --- Configure the primary ---
fn setup_primary(replica_ip string) {
pkg.install("postgresql")
# Create replication user
$`sudo -u postgres psql -c "CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'repl_pass';"`
# Configure replication in postgresql.conf
pg_conf :=
>> listen_addresses = '*'
>> wal_level = replica
>> max_wal_senders = 3
>> wal_keep_size = 256MB
fs.write("/etc/postgresql/16/main/conf.d/replication.conf", pg_conf) catch {|err|
print("Failed: " + err)
}
# Allow replica to connect
hba_line := "host replication replicator #{replica_ip}/32 md5\n"
`echo #{hba_line} >> /etc/postgresql/16/main/pg_hba.conf`
svc.restart("postgresql")
print("Primary configured for replication")
}
# --- Configure the replica ---
fn setup_replica(primary_ip string) {
pkg.install("postgresql")
# Stop PostgreSQL and clear data directory
svc.stop("postgresql")
`rm -rf /var/lib/postgresql/16/main/*`
# Base backup from primary
`sudo -u postgres pg_basebackup -h #{primary_ip} -U replicator -D /var/lib/postgresql/16/main -Fp -Xs -R`
svc.start("postgresql")
print("Replica streaming from " + primary_ip)
}
# Deploy to both servers
p := async ssh setup_primary(replica_host), host: primary_host, user: "root"
resolve(p) # wait for primary first
ssh setup_replica(primary_host), host: replica_host, user: "root"
In production, use proper secrets management instead of hardcoded passwords. See the Secrets Management section.
Deploy to multiple servers one at a time, verifying health after each.
import "net"
import "svc"
import "sync"
servers := ["10.0.0.5", "10.0.0.6", "10.0.0.7"]
app_port := 3000
deploy_version := "v1.2.3"
# Deploy to a single server
fn deploy_one(version string) {
# Pull new version and restart
`cd /opt/myapp && git fetch && git checkout #{version}`
`cd /opt/myapp && npm install --production`
svc.restart("myapp")
}
# Health check: wait for the app to respond
fn wait_healthy(server string, port int) bool {
attempts := 0
while attempts < 30 {
if net.port_open(server, port) {
resp := net.get("http://#{server}:#{port}/health") catch {|err|
attempts++
`sleep 1`
continue
}
if resp.status == 200 {
return true
}
}
attempts++
`sleep 1`
}
false
}
# Roll through each server sequentially
for i := 0; i < len(servers); i++ {
server := servers[i]
print("Deploying to " + server + "...")
ssh deploy_one(deploy_version), host: server, user: "deploy"
if wait_healthy(server, app_port) {
print(" #{server} healthy")
} else {
print(" #{server} FAILED health check - aborting!")
break
}
}
print("Rolling deploy complete")
For parallel deploys with a concurrency limit, use async(limiter) instead of a sequential for loop. See the Async section of the language tour.
Several approaches to handling secrets without hardcoding them in your scripts.
import "env"
db_password := env.get("DB_PASSWORD")
if db_password == "" {
print("DB_PASSWORD not set")
return
}
import "aws"
db_password := aws.ssm_get("/prod/db-password")
api_key := aws.ssm_get("/prod/api-key")
import "fs"
# Decrypt with age/sops on the local machine, then pass to remote
secrets := `sops -d secrets.enc.json`
fn configure(secrets_json string) {
fs.write("/opt/myapp/.env", secrets_json) catch {|err|
print("Failed: " + err)
}
`chmod 600 /opt/myapp/.env`
`chown myapp:myapp /opt/myapp/.env`
}
ssh configure(secrets), host: "10.0.0.5", user: "root"
Secrets are decrypted locally and passed as function arguments to the remote host via the SSH wire protocol. They never touch the remote filesystem as plaintext until explicitly written.
fn generate_secrets() {
password := `openssl rand -base64 32`
`echo #{password} > /root/.db_password`
`chmod 600 /root/.db_password`
}
ssh generate_secrets(), host: "10.0.0.5", user: "root"