Guide

Hosting

A Quillo program in production is one process, quillo run main.qwe, answering HTTP on a port. Hosting it takes four things: a machine that can keep a process running, a proxy in front for HTTPS, a few environment variables, and a database on a disk that lasts. This page goes through each, with a checklist at the end.

What the server needs

The quillo binaryOne file of 20 to 25 MB, nothing else: SQLite and the time zone database are built in (the system's copy of the zones is read first, and the built-in one for a name it leaves out, so US/Pacific and EST work on Ubuntu 24.04, which moved them to its tzdata-legacy package), and TLS is rustls, not OpenSSL. The Linux release builds from 0.7.0 on are linked against glibc 2.17, so they start on CentOS 7, CloudLinux 7 and 8, AlmaLinux 8 and anything newer (0.6.0's need glibc 2.35). They are glibc builds, not static ones, since a plugin is a library the binary loads. Build it on (or for) the same system and CPU as the server: a Linux binary built on a newer distribution than the server's may not start there
The program's filesmain.qwe and the files it uses, migrations/, public/
Rust files, builtA program with .qwer files builds them the first time it starts, which needs Rust. Run quillo build main.qwe beforehand on the same kind of machine, and deploy .quillo/ with the program: .quillo/native/ and .quillo/plugins.lock, each library's SHA-256. The server then needs no Rust. With --no-build it never builds: a library the lock does not name is refused rather than built again
A long-running processAny VPS, cloud VM or container service. On shared hosting with mod_fcgid (cPanel), the web server starts it: see Shared hosting. Hosting that runs only PHP cannot run it; see static hosting

Quillo needs no Node, no package install and no build step on the server.

Running it

quillo run main.qwe --port 8080 --log json

The words after the program's file are options of quillo run, given when the program is started. The table has the ones that matter in production; the options lists every one, --watch and --split included.

OptionIn production
--port Nthe port, 0 to 65535 (default $PORT, then 8080); 0 takes any free port
--listen unix:PATHa Unix socket instead of a TCP port, for a web server on the same machine: mode 0600 (--socket-mode 660 for the web server's group), removed when the server stops, at most 100 bytes. Give --trust-proxy 127.0.0.1 with it
--no-buildnever build a .qwer file on the server: load only what quillo build made
--policy FILEnarrow what the program may do (see the policy)
--host Hthe address to listen on. The default, 127.0.0.1, takes connections from this machine only, which is right behind a proxy on the same machine; localhost is 127.0.0.1 too. 0.0.0.0 in a container
--instances Nthe program N times over, a core each (see several cores)
--log jsona JSON line per request, for log collectors
--reloadreload when the program's files change, without dropping requests (see new versions); with --instances, each instance loads the change, and --split passes it to its services
--max-body 50MBthe largest request body (default 10 MiB): raise it for big uploads. Past its first 30 s a body must keep coming at 1 KiB/s (since 0.7), and one that sends nothing for 30 s is answered 408, with a line in the log. A gzipped body (Content-Encoding: gzip) is decoded, and the limit counts what it decodes to
--max-connections Nconnections open at once (default: what the file descriptors allow, less 4,608); past it one more is closed as it comes (since 0.7)
--connections-per-client Nconnections one client (an address, or an IPv6 /64) may hold open (default 512); this machine and --trust-proxy's proxies are not held to it (since 0.7)
--base /shopwhen the program is served under a path rather than at the root: its links, redirects, scripts and cookies carry the prefix, and the proxy strips it from requests (Quillo routes the path it is given)
--metricsanswer /_quillo/metrics (keep it private: see below)

A program learns where it listens from platform().server: { host, port, url, base }, with the port it got under --port 0, and a url this machine can reach (0.0.0.0 becomes 127.0.0.1). It is null in a script and while the program loads.

route GET /about {    let s = platform().server    <p>Listening at {s.url}{s.base}</p>}

Never --watch in production (quillo run main.qwe --watch is for your own machine, reloading the program as it is edited): it shows errors on pages, refreshes them, and leaves Secure off cookies, since it expects plain HTTP on your own machine. Under --watch the server answers only localhost, addresses and the names --allowed-hosts gives (myapp.test, .dev.test for its subdomains too), and 421 to any other name (since 0.7): a page elsewhere whose name was made to point at your machine (DNS rebinding) cannot call it.

Environment

VariableWhat for
QUILLO_SECRETthe key that signs sessions. Set it: without it a key is made at start, and every restart signs everyone out. A long random string (crypto.token() makes one), the same on every server
PORTthe port, when --port is not given
DATABASE_URL, or any nameread with env("DATABASE_URL") in the program
QUILLO_DBthe database when the program does not call db.open
QUILLO_SYSTEM_DBwhere Quillo keeps its own tables when each tenant has a database
QUILLO_SERVICE_SECRETsigns calls between services split across servers: the same on all of them; new,old (or QUILLO_SERVICE_SECRET_PREVIOUS) while it is rotated: the first signs, any is taken
QUILLO_SERVICE_KEYa service's own key, which a copy joins the app with and which proves it is that service (see authentication between services)
QUILLO_SERVICE_<NAME>where service NAME runs, as quillo run main.qwe --service-url NAME=URL says
LIMER_ADMIN_SECRETfor quillo run main.qwe --register URL, joining limer's pool
PUBLIC_...variables components may read, written into the pages' scripts

Keep them out of the program's files: in the service manager's environment file, or the container platform's secrets. quillo run also reads a .env beside the program (or the file quillo run main.qwe --env-file prod.env names), handy on your own machine; a variable the environment has wins over it, so a .env left on a server cannot override production's. Only a .env that is not there is nothing: one that cannot be read, is not UTF-8, or is a link to nothing stops the start with exit 2, saying where and the ways out (save it as UTF-8, remove it, or give --env-file /dev/null). A line that is not KEY=value is skipped and said at the start; KEY= # set me is "", while KEY=#fff is #fff. The one other configuration is a policy, which only takes away: /etc/quillo/policy.toml, --policy FILE and quillo.policy beside the program (see the policy).

The database

db.open("data/app.db") | unwrap         // SQLite, in a folder that lastsdb.open(env("DATABASE_URL")) | unwrap   // PostgreSQL or MySQLdb.migrate("migrations")

HTTPS: a proxy in front

Quillo answers plain HTTP/1.1: it has no HTTPS or HTTP/2 of its own. A proxy in front gets the certificate, answers HTTPS, and passes requests on. Session cookies are Secure and named __Host-quillo_session (unless --watch), so the site must be served over HTTPS: a browser sends them to http://localhost, but curl and other cookie jars do not, so test those under --watch or through the proxy.

With Caddy, which gets and renews the certificate itself and passes WebSockets as they are:

example.com {    reverse_proxy 127.0.0.1:8080}

With nginx, add what WebSockets need (live data and channels use them) and the host name (tenants are told apart by it):

map $http_upgrade $connection_upgrade {    default upgrade;    ''      close;}server {    listen 443 ssl;    server_name example.com;    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;    location / {        proxy_pass http://127.0.0.1:8080;        proxy_http_version 1.1;        proxy_set_header Host $host;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection $connection_upgrade;        proxy_read_timeout 1h;    }}
site_headers({ "content-security-policy": "default-src 'self'" })site_headers("/embed", { "x-frame-options": null })

Keeping it running

With systemd, /etc/systemd/system/app.service:

[Unit]Description=My Quillo appAfter=network.target[Service]User=quilloWorkingDirectory=/srv/appExecStart=/usr/local/bin/quillo run main.qwe --port 8080 --log jsonEnvironmentFile=/etc/quillo/app.envRestart=on-failureTimeoutStopSec=15[Install]WantedBy=multi-user.target

/etc/quillo/app.env holds the environment (QUILLO_SECRET=...), readable by root only (chmod 600). Then systemctl enable --now app starts it now and at every boot, and journalctl -u app -f follows its log.

On SIGTERM (what systemctl stop and docker stop send), Quillo stops taking connections, answers the requests in progress for up to 10 seconds, and exits with status 0; give the service manager a little longer than that, as TimeoutStopSec=15 does.

In a container

A Dockerfile that builds quillo from its source, then runs the program in a small image as a user of its own, with the database on a volume:

FROM rust:1-bookworm AS quilloRUN cargo install --locked --git https://github.com/quillolang/quillo quilloFROM debian:bookworm-slimCOPY --from=quillo /usr/local/cargo/bin/quillo /usr/local/bin/quilloRUN useradd --system --home /app quillo \ && mkdir -p /app/data && chown quillo /app/dataWORKDIR /appCOPY app/ .USER quilloVOLUME /app/dataEXPOSE 8080CMD ["quillo", "run", "main.qwe", "--host", "0.0.0.0", \     "--port", "8080", "--log", "json"]
docker build -t my-app .docker run -d --name my-app -p 127.0.0.1:8080:8080 -v my-app-data:/app/data \    -e QUILLO_SECRET=... --stop-timeout 15 my-app

The program opens its database at data/app.db, on the volume, so it outlasts the container. The first build takes a few minutes (about two on a laptop); later ones reuse the quillo stage while the Dockerfile's first two lines stay the same. The image is about 170 MB, most of it Debian; quillo needs only the C library from it.

New versions

Several cores, several servers

Health, logs and metrics

Static hosting

Quillo can be the static host too. quillo serve site serves a folder of files as they are, with the options of quillo run (--port, --host, --log): a folder's index.html for / and /docs/, /about for about.html, /docs redirected to /docs/, the folder's 404.html for a miss (sent no-cache), and no file whose name starts with a dot (but .well-known/), no program source and no SQLite database, whatever its name. A file asked for with a / after it (/about/) is moved to its path without one (301), when it would be served there. Inside a program, route GET /*path = files("site", path) does the same beside its other routes, but serves /about/ as /about, since a program's URLs are its own. Each file goes out as it is on disk, with its ETag, Last-Modified, ranges and gzip.

quillo serve dist --port 8080 --host 0.0.0.0

The other way round, a site whose components call nothing on the server (no server fn, database, sessions or live data) can be rendered to files and put on any static host, shared hosting included. This site is one: its export script runs it, fetches every page its sitemap lists and the component bundles they load, and writes them to a folder that is uploaded as it is.

Before going live

Thread
10Useful? Like it, or add to its thread below.

Thread 0 posts

On Hosting: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.