> ## Documentation Index
> Fetch the complete documentation index at: https://docs.archil.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Host a web app

> Run a Python web app with a SQLite database on an Archil disk, serve it at a public URL, and fork preview environments from a template.

This guide deploys a small guestbook app: a Python HTTP server that stores messages in [SQLite](https://www.sqlite.org/). The code and the database live on an Archil disk mounted into a [persistent sandbox](/compute/sandboxes/introduction), the server runs as a supervised service, and you reach it at a public HTTPS URL. Then you fork preview environments that run a new version of the code, without touching production.

SQLite doesn't need to know anything about Archil. It reads and writes a file on the mounted disk, and Archil keeps the data durable in object storage, so the database outlives any one sandbox.

## Prerequisites

Export your Archil API key and a region where sandboxes are available (`aws-us-east-1`, `aws-us-west-2`, or `aws-eu-west-1`):

```bash theme={null}
export ARCHIL_API_KEY=key-...
export ARCHIL_REGION=aws-us-east-1
```

Then install the SDK:

<CodeGroup>
  ```bash TypeScript theme={null}
  npm init -y && npm pkg set type=module
  npm install disk tsx
  ```

  ```bash Python theme={null}
  pip install archil
  ```
</CodeGroup>

## Write the app

The app uses only the Python standard library. `POST /` saves the request body as a message, and `GET /` lists every message under the app's version. At startup it switches the database to [write-ahead logging](https://www.sqlite.org/wal.html) (WAL), so readers don't block the writer.

```python app.py theme={null}
import sqlite3
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

VERSION = "v1"


def query(sql, *params):
    db = sqlite3.connect("guestbook.db", timeout=5)
    try:
        with db:
            return db.execute(sql, params).fetchall()
    finally:
        db.close()


query("PRAGMA journal_mode=WAL")
query("CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, body TEXT NOT NULL)")


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        rows = query("SELECT body FROM messages ORDER BY id")
        self.reply(200, f"guestbook {VERSION}\n" + "".join(f"- {body}\n" for (body,) in rows))

    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", 0))).decode()
        query("INSERT INTO messages (body) VALUES (?)", body)
        self.reply(201, "saved\n")

    def reply(self, status, text):
        data = text.encode()
        self.send_response(status)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)


ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
```

## Deploy it

The deploy script creates a disk for the app, creates a sandbox from the `python:3.13` image with the disk mounted at `/srv/app`, and uploads `app.py` onto the disk. It then registers the server as a service with `archil-sandbox services create`. Services run in the background, restart if they crash, and start again whenever the sandbox boots. Finally, the script exposes port 8080 publicly and prints the URL.

<CodeGroup>
  ```typescript deploy.ts theme={null}
  import { readFile } from "node:fs/promises";
  import * as archil from "disk";

  const { disk } = await archil.createDisk({ name: "guestbook-data" });
  const sandbox = await archil.createSandbox({
    name: "guestbook",
    baseImage: "python:3.13",
    mounts: [{ disk, path: "/srv/app" }],
  });

  await sandbox.files.uploadFile(await readFile("app.py"), "/srv/app/app.py");
  await sandbox.exec("archil-sandbox services create web --tcp-port 8080 --dir /srv/app -- python3 app.py");

  const hostname = await sandbox.exposePort(8080);
  console.log(`App URL: https://${hostname}`);
  ```

  ```python deploy.py theme={null}
  import archil

  disk = archil.create_disk(name="guestbook-data").disk
  sandbox = archil.create_sandbox(
      name="guestbook",
      base_image="python:3.13",
      mounts=[archil.SandboxMountSpec(disk=disk, path="/srv/app")],
  )

  sandbox.files.upload_file("app.py", "/srv/app/app.py")
  sandbox.exec("archil-sandbox services create web --tcp-port 8080 --dir /srv/app -- python3 app.py")

  hostname = sandbox.expose_port(8080)
  print(f"App URL: https://{hostname}")
  ```
</CodeGroup>

<CodeGroup>
  ```bash TypeScript theme={null}
  npx tsx deploy.ts
  ```

  ```bash Python theme={null}
  python deploy.py
  ```
</CodeGroup>

```text theme={null}
App URL: https://8080-agquugxt3zyrdjwtv2avpwfa5m.prod-aws-us-west-2-green.archil.app
```

Save the printed URL, then sign the guestbook from your machine:

```bash theme={null}
export APP_URL=https://8080-agquugxt3zyrdjwtv2avpwfa5m.prod-aws-us-west-2-green.archil.app
curl --data "hello from production" $APP_URL
curl $APP_URL
```

```text theme={null}
saved
guestbook v1
- hello from production
```

<Warning>
  `exposePort` makes the port reachable by anyone who has the URL. This guestbook accepts writes from anyone; a real
  app needs its own authentication before you expose it. For programmatic clients, use a port token instead; see
  [Hosting network services](/compute/sandboxes/introduction#hosting-network-services).
</Warning>

## Stop it and let traffic wake it

A stopped sandbox doesn't use compute, and a request to its public URL starts it again. The next script lists the files on the disk and checks the database's journal mode, then stops the sandbox and requests its URL.

<CodeGroup>
  ```typescript wake.ts theme={null}
  import * as archil from "disk";

  const sandbox = (await archil.listSandboxes()).find((s) => s.name === "guestbook")!;
  const check = await sandbox.exec("ls && python3 -m sqlite3 guestbook.db 'PRAGMA journal_mode'", {
    cwd: "/srv/app",
  });
  console.log(check.stdout);

  await sandbox.stop();
  const [{ hostname }] = await sandbox.listPorts();
  const response = await fetch(`https://${hostname}/`);
  console.log(response.status, await response.text());
  console.log("sandbox is", (await archil.getSandbox(sandbox.id)).status);
  ```

  ```python wake.py theme={null}
  import urllib.request

  import archil

  sandbox = next(s for s in archil.list_sandboxes() if s.name == "guestbook")
  check = sandbox.exec("ls && python3 -m sqlite3 guestbook.db 'PRAGMA journal_mode'", cwd="/srv/app")
  print(check.stdout)

  sandbox.stop()
  hostname = sandbox.list_ports()[0].hostname
  with urllib.request.urlopen(f"https://{hostname}/") as response:
      print(response.status, response.read().decode())
  print("sandbox is", archil.get_sandbox(sandbox.id).status)
  ```
</CodeGroup>

```text theme={null}
app.py
guestbook.db
('wal',)

200 guestbook v1
- hello from production

sandbox is running
```

The database file sits on the Archil disk next to the code, in WAL mode. SQLite keeps its `-wal` and `-shm` files beside it while connections are open. The request booted the sandbox, which mounted the disk and started the `web` service before answering, typically in a second or two. Nothing in the app had to handle the restart.

A plain mount like this one gives the sandbox exclusive access to the disk, so SQLite's locking works exactly as it does on a local disk. Don't open the same database file from several sandboxes at once: SQLite's locks don't extend across machines. To read the data from elsewhere while the app runs, see [Sharing disks](/concepts/sharing-disks).

## Create preview environments

To try a change before it reaches production, give it its own sandbox. Rather than setting each one up from scratch, keep a template sandbox with the app already installed and fork it: a fork starts from a copy of the template's root filesystem, with every package, file, and service definition in place.

The template keeps the app and its database on its root filesystem rather than on an Archil disk. A fork mounts the same disks as its source, and a plain mount can be held by only one sandbox at a time, so a template with a disk couldn't run more than one preview. Previews are disposable, so their data doesn't need to outlive them. Production isn't involved at all: it keeps running on `guestbook-data` while you create and delete previews.

Create the template once. It runs the same `app.py` as a service, then stops; a stopped sandbox doesn't use compute.

<CodeGroup>
  ```typescript template.ts theme={null}
  import { readFile } from "node:fs/promises";
  import * as archil from "disk";

  const template = await archil.createSandbox({ name: "guestbook-template", baseImage: "python:3.13" });
  await template.exec("mkdir -p /srv/app");
  await template.files.uploadFile(await readFile("app.py"), "/srv/app/app.py");
  await template.exec("archil-sandbox services create web --tcp-port 8080 --dir /srv/app -- python3 app.py");
  await template.stop();
  ```

  ```python template.py theme={null}
  import archil

  template = archil.create_sandbox(name="guestbook-template", base_image="python:3.13")
  template.exec("mkdir -p /srv/app")
  template.files.upload_file("app.py", "/srv/app/app.py")
  template.exec("archil-sandbox services create web --tcp-port 8080 --dir /srv/app -- python3 app.py")
  template.stop()
  ```
</CodeGroup>

Each preview is a fork of the template. The fork boots with the `web` service running; the script deploys a change to it, exposes its port, and prints its URL. The `sed` command stands in for deploying a new version of `app.py`.

<CodeGroup>
  ```typescript preview.ts theme={null}
  import * as archil from "disk";

  const template = (await archil.listSandboxes()).find((s) => s.name === "guestbook-template")!;
  const preview = await template.fork({ name: "guestbook-preview" });

  await preview.exec(
    `sed -i 's/^VERSION = .*/VERSION = "v2"/' app.py && archil-sandbox services restart web`,
    { cwd: "/srv/app" },
  );
  const hostname = await preview.exposePort(8080);
  console.log(`Preview URL: https://${hostname}`);
  ```

  ```python preview.py theme={null}
  import archil

  template = next(s for s in archil.list_sandboxes() if s.name == "guestbook-template")
  preview = template.fork(name="guestbook-preview")

  preview.exec(
      """sed -i 's/^VERSION = .*/VERSION = "v2"/' app.py && archil-sandbox services restart web""",
      cwd="/srv/app",
  )
  hostname = preview.expose_port(8080)
  print(f"Preview URL: https://{hostname}")
  ```
</CodeGroup>

<CodeGroup>
  ```bash TypeScript theme={null}
  npx tsx template.ts
  npx tsx preview.ts
  ```

  ```bash Python theme={null}
  python template.py
  python preview.py
  ```
</CodeGroup>

Sign the preview's guestbook, then compare the two environments:

```bash theme={null}
export PREVIEW_URL=https://8080-agquugzjsr6ilkopelwwgkhslq.prod-aws-us-west-2-green.archil.app
curl --data "hello from the preview" $PREVIEW_URL
curl $PREVIEW_URL
curl $APP_URL
```

```text theme={null}
saved
guestbook v2
- hello from the preview
guestbook v1
- hello from production
```

The preview runs the new code with its own database, and production is untouched. Fork the template again, with a different name, for each branch you want to preview.

## Clean up

A fork must be deleted before the sandbox it was forked from, and a sandbox must be stopped before it's deleted. Delete the preview, the template, and production, then production's disk.

<CodeGroup>
  ```typescript cleanup.ts theme={null}
  import * as archil from "disk";

  for (const name of ["guestbook-preview", "guestbook-template", "guestbook"]) {
    const sandbox = (await archil.listSandboxes()).find((s) => s.name === name);
    await sandbox?.stop();
    await sandbox?.delete();
  }
  const [disk] = await archil.listDisks({ name: "guestbook-data" });
  await disk?.delete();
  ```

  ```python cleanup.py theme={null}
  import archil

  for name in ["guestbook-preview", "guestbook-template", "guestbook"]:
      for sandbox in archil.list_sandboxes():
          if sandbox.name == name:
              sandbox.stop()
              sandbox.delete()
  for disk in archil.list_disks(name="guestbook-data"):
      disk.delete()
  ```
</CodeGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Persistent sandboxes" icon="box" href="/compute/sandboxes/introduction">
    Lifecycle, services, network access, and forking in detail.
  </Card>

  <Card title="Coding agent in a sandbox" icon="code" href="/guides/agents/coding-agent">
    Have an agent build and run an app like this one in a sandbox.
  </Card>

  <Card title="Sharing disks" icon="share-nodes" href="/concepts/sharing-disks">
    Mount modes, delegations, and reading a disk from several machines.
  </Card>

  <Card title="TypeScript SDK" icon="js" href="/sdks/typescript">
    Full reference for sandboxes, disks, and serverless execution.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.