6. Deploying it
melee checkcompiles it,melee pushships it, andmelee envis where the secret goes.
Everything so far ran under CRuby. The real thing is a native binary, and this is the step that finds out whether your Ruby survives the compiler.
Compile it here first
melee check
melee build: /path/to/notes/build/bin/notes (2002 KB) in 3.4 s
That is the whole app — your code, the melee library and SQLite — in a two-megabyte executable. Nothing was sent anywhere.
If it does not compile you get one line per problem, with the file and line:
views/broken.erb:2: unexpected end-of-input, assuming it is closing the parent top level context
db/migrations/003_bad.sql:1: near "TABEL": syntax error
app.rb:5: views/index.erb does not declare a local `nots`
app.rb:12: unsupported eval of a runtime string is not supported by AOT compilation (define the code statically)
Template errors are mapped back to the .erb you wrote, not the Ruby it was compiled into.
What melee check does not catch
This is the part to take seriously. A green build is not a working app.
db.qeury("SELECT 1") # compiles. 500 at run time.
Stats.get("visits").hitt # compiles. 500 at run time.
render :index, notes: n # arity and nil are not checked either
An undefined method with an explicit receiver compiles and raises NoMethodError when the line runs.
So do wrong arity and calling anything on nil. What is reliably caught is the shape of the program:
templates, migrations, forbidden constructs like eval, unknown requires, and calls to a helper you never
defined.
So: click through the app under melee dev before pushing, and read
When something breaks.
Push it
melee.toml says where:
name = "notes"
server = "http://127.0.0.1:7070"
url = "http://notes.localhost:8080"
server has to be https://, or http:// to a loopback address (127.0.0.1, ::1, localhost) as above
— anywhere else over plain http:// would send the token below in the clear, and melee refuses it.
The control API needs a token. melee looks for it in MELEE_TOKEN, or at a token_file path named in
melee.toml — which has to be outside the app directory, because melee push uploads everything in it.
melee push
using control API at http://127.0.0.1:7070
pushing notes (3 KB) to http://127.0.0.1:7070
deployed notes release 1789257455384-0000 (2002 KB binary) in 3.7s
http://notes.localhost:8080
(That 3.7 s is a local server on a development machine with a warm compiler cache. A push from a laptop to a Linux server measured 10.8 s, and the first build on a fresh machine takes about half a minute while SQLite is compiled.)
Three kilobytes went up — the source, not the binary. The server compiled it, wrote a new release directory,
swapped the current symlink and stopped the old warm process. The next request starts the new one.
A failed build answers with the same diagnostics, deploys nothing, and leaves the previous release serving.
The secret
melee env NOTES_SECRET letmein
set NOTES_SECRET for notes; the app will restart on its next request
It arrives in the app as ENV["NOTES_SECRET"]. This is the only place secrets belong: not in app.rb, not
in melee.toml, not in the tarball.
Watch it work
curl -s -o /dev/null -w "%{http_code} in %{time_total}s\n" https://notes.example/
200 in 0.315581s # cold: the server had to start the process
200 in 0.002725s # warm
200 in 0.001870s
The first request after an idle stop pays for starting the binary. After that it is a fork. On the Linux
test machine, sandboxed, that is 0.2 ms warm and 1.6–2.6 ms cold.
The other commands
melee logs # the last 50 lines
melee logs -f # follow
melee apps # what is deployed on this server
melee objects # the durable objects and their pending timers
melee restart # stop the warm process; the next request starts it again
melee open # print and open the URL
melee objects is how you check the timer you wrote in step 5 is actually scheduled:
CLASS ID CREATED LAST CALL TIMER
Stats visits 0s ago 0s ago in 23h
And the log shows the object being called, one line per event:
23:57:39 call method=hit ms=0.16 object=Stats/visits ok=true
23:57:39 call method=today ms=0.04 object=Stats/visits ok=true
When it breaks in production
There is no error page and no backtrace. A visitor gets:
Something went wrong
and the log gets the line that matters:
undefined method 'qeury' for an instance of Melee::DB class=NoMethodError method=GET path=/ route=GET /
Class, message, method, path and route — no line number, because the compiled binary has no backtrace. That
is why the log line and the route name have to be enough, and why log.info with fields is worth writing as
you go. When something breaks is the whole subject.
The whole thing
For reference, the finished app.rb — 45 lines, and the only Ruby file besides lib/stats.rb:
# frozen_string_literal: true
require_relative "lib/stats"
title "Notes"
before "/admin" do
redirect "/login" unless session[:admin]
end
get "/" do
Stats.get("visits").hit
render :index, notes: open_notes, views: Stats.get("visits").today
end
get "/login" do
render :login, error: nil
end
post "/login" do
if secure_equal?(params.fetch(:secret), ENV["NOTES_SECRET"].to_s)
session[:admin] = "1"
redirect "/admin"
else
status 401
render :login, error: "That secret is wrong."
end
end
get "/admin" do
render :admin, notes: db.query("SELECT id, text, done FROM notes ORDER BY done, id DESC")
end
post "/admin/notes" do
text = params.fetch(:text).strip
halt 400, "A note needs some text" if text.empty?
db.run "INSERT INTO notes (text, created_at) VALUES (?, ?)", text, Time.now.to_i
redirect "/admin"
end
post "/admin/notes/:id/done" do
db.run "UPDATE notes SET done = 1 - done WHERE id = ?", params.fetch(:id).to_i
redirect "/admin"
end
def open_notes = db.query("SELECT id, text FROM notes WHERE done = 0 ORDER BY id DESC")
The rest is lib/stats.rb, five templates, three .sql files and melee.toml.
Done
You have written and deployed an app with routes, templates, a database, a login, and an object that does work on a schedule with nobody watching.
Where to go next:
- Background jobs and scheduled work — the patterns for everything step 5 only sketched.
- What melee cannot do — read before the next app.
- The API reference — everything you can call.