Install: binary, systemd and Docker
Rustiz is a headless, agent-first social-media scheduler: one Rust binary, one
SQLite database, no web GUI, no Redis and no Postgres. The same process serves
the REST API (/public/v1), the MCP endpoint (/mcp), the OAuth “connect a
channel” callbacks and the scheduler.
This document is the operator reference: install paths, every environment
variable the core reads, storage and ffmpeg, reverse proxies and TLS,
organizations/API keys and connecting agents over stdio or remote MCP.
Reality check. The port is complete but, as of this writing, no provider has been tested against a real platform. Provider tests use mock servers. Treat the first real connection to every platform as a debugging session. See Porting status and the per-provider pages in
./providers/.
1. Quickstart
| Path | TLS | Database / media | Typical use |
|---|---|---|---|
| Local binary (§1.1) | none, plain HTTP on loopback | ~/.rustiz/rustiz.db + ~/.rustiz config | Evaluating, one agent on one machine |
| Production binary + systemd (§1.2) | terminated by Caddy/Nginx/Cloudflare | /var/lib/rustiz/rustiz.db + /var/lib/rustiz/uploads (or S3/R2) | A real deployment with a public HTTPS origin |
| Docker (§1.3) | terminated by the host proxy | named volume /data | Reproducible deployment or a container platform |
1.1 Local, single user (no TLS)
Requirements: a Rust toolchain (stable), and optionally ffmpeg for video
thumbnails. SQLite is embedded.
cargo build --release --locked -p rustiz
./target/release/rustiz init
./target/release/rustiz providers --status
./target/release/rustiz serve
rustiz init creates:
~/.rustiz/(mode0700on Unix)~/.rustiz/config.env(mode0600) containingJWT_SECRET,DATABASE_URL,FRONTEND_URLandRUSTIZ_API_KEY~/.rustiz/rustiz.db(SQLite, WAL mode)- the first organization
It prints one JSON document with the paths, the organization, and an
mcpServers block to paste into an agent. When it creates an organization it
also prints the API key; when it adopts an existing one it prints
api_key_location instead and never replaces the existing secret or creates a
second organization.
Then:
- REST API:
http://localhost:5000/public/v1/... - MCP over HTTP:
http://localhost:5000/mcp(Bearer API key) - MCP over stdio:
rustiz mcp - Health:
http://localhost:5000/health→ok
To use another directory (for example a system service):
sudo rustiz init --dir /var/lib/rustiz --name production
init writes the config file but does not persist shell environment
variables. In particular, if you run
FRONTEND_URL=https://social.example.com rustiz init, the generated
config.env still contains FRONTEND_URL=http://localhost:5000; the same
applies to DATABASE_URL. Edit the file (or export the variable on every
start) before relying on it (see §3.1).
1.2 Production server (systemd + reverse proxy)
Do not expose the plain-HTTP process directly. Put Caddy, Nginx or Cloudflare in front; TLS terminates there.
sudo install -m 0755 target/release/rustiz /usr/local/bin/rustiz
sudo useradd --system --home /var/lib/rustiz --shell /usr/sbin/nologin rustiz
sudo install -d -o rustiz -g rustiz -m 0700 /var/lib/rustiz
sudo -u rustiz /usr/local/bin/rustiz init --dir /var/lib/rustiz --name production
sudo install -d -o rustiz -g rustiz -m 0700 /var/lib/rustiz/uploads
sudoedit /var/lib/rustiz/config.env
Set at least:
FRONTEND_URL=https://social.example.com
DATABASE_URL=sqlite:///var/lib/rustiz/rustiz.db
UPLOAD_DIRECTORY=/var/lib/rustiz/uploads
JWT_SECRET=<keep the value init generated>
RUSTIZ_API_KEY=<keep the value init generated>
EMAIL_PROVIDER=resend
RESEND_API_KEY=re_...
EMAIL_FROM_ADDRESS=notifications@example.com
EMAIL_FROM_NAME=Rustiz
TRUSTED_PROXY_HOPS=1
A systemd unit:
# /etc/systemd/system/rustiz.service
[Unit]
Description=Rustiz (API + MCP + scheduler)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=rustiz
Group=rustiz
WorkingDirectory=/var/lib/rustiz
Environment=RUSTIZ_CONFIG=/var/lib/rustiz/config.env
ExecStart=/usr/local/bin/rustiz serve
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/rustiz
[Install]
WantedBy=multi-user.target
If you want a separate scheduler process (for example API replicas that must not each run jobs):
# /etc/systemd/system/rustiz-worker.service
[Unit]
Description=Rustiz scheduler
After=rustiz.service
[Service]
Type=simple
User=rustiz
Group=rustiz
Environment=RUSTIZ_CONFIG=/var/lib/rustiz/config.env
ExecStart=/usr/local/bin/rustiz worker
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
and start the HTTP tier with rustiz serve --no-worker.
1.3 Docker
The repository Dockerfile builds the binary in rust:1-slim and copies it to
gcr.io/distroless/cc-debian12. The runtime image has no shell, no package
manager and no ffmpeg (see §5.4).
docker build -t rustiz:local .
# One-time: create the volume, config file and database.
docker volume create rustiz-data
docker run --rm -v rustiz-data:/data rustiz:local init --dir /data
# Run the server.
docker run -d --name rustiz \
-p 127.0.0.1:5000:5000 \
-v rustiz-data:/data \
-e RUSTIZ_CONFIG=/data/config.env \
-e FRONTEND_URL=https://social.example.com \
rustiz:local
The image already sets:
DATABASE_URL=sqlite:///data/rustiz.db
UPLOAD_DIRECTORY=/data/uploads
PORT=5000
RUSTIZ_CONFIG=/data/config.env is required if you rely on the file init
wrote: without it the binary looks for ~/.rustiz/config.env and will not see
the JWT_SECRET created there. Alternatively pass JWT_SECRET and
RUSTIZ_API_KEY as environment variables.
A minimal Compose file:
services:
rustiz:
build: .
restart: unless-stopped
ports:
- "127.0.0.1:5000:5000"
environment:
RUSTIZ_CONFIG: /data/config.env
FRONTEND_URL: https://social.example.com
TRUSTED_PROXY_HOPS: "1"
volumes:
- rustiz-data:/data
volumes:
rustiz-data:
Run docker compose run --rm rustiz init --dir /data once before the first
up. The distroless image has no shell, so docker exec ... sh will not work;
use logs and the /health endpoint instead. If you run the container as a
non-root user, chown the volume first.
2. What runs where
| Command | HTTP API | MCP | Scheduler | OAuth callback receiver |
|---|---|---|---|---|
rustiz serve | yes | yes (/mcp) | yes (unless --no-worker) | yes |
rustiz serve --no-worker | yes | yes | no | yes |
rustiz worker | no | no | yes | no |
rustiz mcp | no | stdio only | yes | yes, on 127.0.0.1:<FRONTEND_URL port> when the URL is plain HTTP on localhost |
rustiz init | no | no | no | no |
rustiz org ... | no | no | no | no |
rustiz providers | no | no | no | no |
rustiz migrate | no | no | no | no |
The HTTP server and the worker share only the SQLite database. Jobs are claimed
with a five-minute lease and a heartbeat every 100 seconds, so a separate
rustiz worker and rustiz serve can coexist. SQLite still has one writer at
a time; for anything beyond a small deployment, run one worker and one API
process, and use WORKER_CONCURRENCY to parallelize inside them.
rustiz with no subcommand is equivalent to rustiz serve.
In-process CLI commands (posts, channels, media, analytics,
notifications, find-slot) run the HTTP router against the local database but
do not start the scheduler. A post created with --type schedule stays
queued until rustiz serve, rustiz worker or rustiz mcp is running.