Publish a static page:

echo '# Hello, world!' | ssh kedge.dev

Your SSH key identifies your account. Markdown becomes HTML.

a function

Add a #! shebang to publish a script that runs on each request:

printf '#!/bin/bash \n uptime' | ssh kedge.dev

the kedge shortcut

Install the optional kedge shortcut, a small wrapper around ssh kedge.dev:

ssh kedge.dev setup | sh

It also adds kedge up, a Git commit-and-push helper for source directories.

a routed site

Deploy a directory, and the files become a route tree:

~/quickstart-site$ show index.html about/index.html
── index.html
<h1>Field notes</h1>
<p>A tiny site with more than one route.</p>
<a href="/about/">How this site works →</a>

── about/index.html
<h1>About this site</h1>
<p>The directory tree becomes the URL tree.</p>
<a href="/">← Back home</a>

~/quickstart-site$ kedge up

index.html serves /; about/index.html serves /about/. kedge up derives a default app name from the working directory.

Switch branches for an isolated preview:

~/quickstart-site$ show about/index.html
── about/index.html
<h1>Previewing a new route</h1>
<p>This only exists on the <code>tweak</code> branch.</p>
<a href="/">← Back home</a>

~/quickstart-site$ kedge up

The preview gets its own URL; main stays live.

an HTML app

Add data-kedge to ordinary HTML for shared, live data without writing an application server, SQL, or JavaScript.

~/quickstart-clicks$ show index.html
── index.html
<!doctype html>
<title>Clicks</title>
<h1>Clicks</h1>

<strong data-kedge="counters/home">{{clicks | plural:click}}</strong>
<button data-kedge="counters/home?$increment=clicks">add one</button>

~/quickstart-clicks$ kedge up

At deploy time, Kedge parses the bindings, creates the counter column, and manages the underlying database. Increments merge without losing simultaneous clicks, and bound values update live. See HTML apps for a more detailed example.

a database and a filesystem

Every app has /shared.db and /shared/, ordinary local paths for state that needs code. Two handlers, the same rollup:

~/quickstart-state$ show index.sh files.sh
── index.sh
#!/usr/bin/env bash
agent=${HTTP_USER_AGENT//"'"/"''"}
printf 'Content-Type: text/plain\n\n'
sqlite3 -header /shared.db "
CREATE TABLE IF NOT EXISTS hits(
  id INTEGER PRIMARY KEY, dc TEXT, agent TEXT, at TEXT DEFAULT (datetime('now')));
INSERT INTO hits(dc, agent) VALUES('$KEDGE_DC', '$agent');
SELECT dc, count(*) AS hits, max(at) AS latest, agent
  FROM hits GROUP BY dc, agent ORDER BY hits DESC;"

── files.sh
#!/usr/bin/env bash
echo "$KEDGE_DC $HTTP_USER_AGENT" >> /shared/agents.log
printf 'Content-Type: text/plain\n\n'
sort /shared/agents.log | uniq -c | sort -rn

~/quickstart-state$ kedge up

/ groups the request log in SQL; /files appends a line and counts it with sort | uniq -c. Both replicate across instances and regions. Use the database when you want a query or an index. See shared data.

an app built from source

Kedge detects common frameworks and builds them from source. This Rust service reports its optimized binary and resident-memory sizes:

~/quickstart-rust$ show src/main.rs Cargo.toml
── src/main.rs
use memory_stats::memory_stats;
use std::env::{current_exe, var};
use tiny_http::{Response, Server};

fn main() {
  let port = var("PORT").unwrap_or_else(|_| "8080".into());
  let srv = Server::http(format!("0.0.0.0:{port}")).unwrap();
  let bin = current_exe().unwrap().metadata().unwrap().len() / 1024;
  for (n, req) in srv.incoming_requests().enumerate() {
    let rss = memory_stats().unwrap().physical_mem / 1024;
    let body = format!("release binary: {bin} KiB\n\
      process RSS: {rss} KiB\nrequest: {}\n", n + 1);
    req.respond(Response::from_string(body)).unwrap();
  }
}

── Cargo.toml
[package]
name = "quickstart-rust"
version = "0.1.0"
edition = "2021"
[dependencies]
memory-stats = "1.2"
tiny_http = "0.12"
[profile.release]
strip = true

~/quickstart-rust$ kedge up

Cargo.toml is the build plan; Kedge compiles a release binary and supplies $PORT.

a prebuilt image

Publish an existing Docker image straight from a public registry:

kedge publish "$(kedge whoami)-quickstart-httpbin" \
  --image ghcr.io/mccutchen/go-httpbin:2.23.1

a Dockerfile

Use a Dockerfile when the image itself is the point. This multi-stage Go build compiles a static binary and copies it into an empty scratch image:

~/quickstart-scratch$ show main.go Dockerfile
── main.go
package main
import "net/http"
func main() {
  http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
    w.Write([]byte("Hello from scratch.\n"))
  })
  http.ListenAndServe(":8080", nil)
}

── Dockerfile
FROM golang:1.24-alpine AS build
COPY main.go /
RUN CGO_ENABLED=0 go build -o /server /main.go
FROM scratch
COPY --from=build /server /
EXPOSE 8080
ENTRYPOINT ["/server"]

~/quickstart-scratch$ kedge up

See builds & images for the full build order.

clean up

Delete the example apps:

kedge delete 'hello-world-*' 'bash-*' \
  "$(kedge whoami)-quickstart*"

beyond the defaults

Software that expects local disk, such as Postgres, a game server, or a dev box, needs a persistent volume:

kedge up --volume /data

Connecting a repository through GitHub deploys gives every pull request its own preview.