# Deploy an app

> Package a JavaScript or TypeScript server in a container image on elide.dev/elide, with environment, sandbox flags, logs and a health check.

This page packages a TypeScript server as a container image on `elide.dev/elide`.

## Write the app

```ts
// server.ts
import { Hono } from "hono";

const app = new Hono();

app.use(async (c, next) => {
  await next();
  console.log(JSON.stringify({ method: c.req.method, path: c.req.path, status: c.res.status }));
});

app.get("/healthz", (c) => c.text("ok"));
app.get("/", (c) => c.json({ greeting: process.env.GREETING ?? "hello" }));

export default app;
```

```js
// healthcheck.mjs
const res = await fetch("http://127.0.0.1:8080/healthz");
process.exit(res.ok ? 0 : 1);
```

```json
{
  "name": "deploy-app",
  "type": "module",
  "dependencies": {
    "hono": "^4.7.0"
  }
}
```

Run `elide install --ecosystems=npm` once to write `aube-lock.yaml`.

## Write the Dockerfile

```dockerfile
FROM elide.dev/elide:1.6.0
WORKDIR /app
COPY package.json aube-lock.yaml ./
RUN elide install --ecosystems=npm
COPY . .
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s CMD ["elide", "healthcheck.mjs"]
CMD ["--allow-read=/app,/root/.cache/aube", "--allow-net=api.example.com", "serve", "--no-tui", "--host", "0.0.0.0", "--port", "8080", "server.ts"]
```

```text
# .dockerignore
node_modules
.env
```

| Line | Why |
| --- | --- |
| `FROM elide.dev/elide:1.6.0` | The image's entrypoint is `elide`, so `CMD` holds only its arguments. Tags: [Container Images](https://elide.help/docs/installation/containers). |
| `RUN elide install --ecosystems=npm` | Installs inside the image. `node_modules` links into `/root/.cache/aube`, so a host `node_modules` would not work. |
| `--host 0.0.0.0` | A default-exported handler binds `127.0.0.1`, which is unreachable from outside the container. |
| `--no-tui` | Without it, `docker run -t` starts the terminal UI and the app's output never reaches `docker logs`. |
| `HEALTHCHECK` | The image has no `curl` or `wget`; a short Elide script does the check. |

```bash
docker build -t my-app .
docker run -d --name my-app -p 8080:8080 -e GREETING=hi my-app
curl http://127.0.0.1:8080/
```

```text
{"greeting":"hi"}
```

`docker inspect --format '{{.State.Health.Status}}' my-app` prints `healthy` once the first check passes. `docker stop`
ends the server within a second, with exit code `143`.

## Pass environment variables

`process.env` holds the container's variables; pass them with `docker run -e NAME=value` or `--env-file .env`. Elide
does not read `.env` files or set `NODE_ENV`. For local runs, load the file in code:

```ts
import { existsSync } from "node:fs";

if (existsSync(".env")) process.loadEnvFile();
```

`process.loadEnvFile()` without the check throws `ENOENT: no such file or directory, open '.env'` when the file is
missing. See [Environment Variables](https://elide.help/docs/env-variables).

## Restrict the server

Programs run unrestricted until a sandbox flag is given. Grant a server what it needs and nothing else:

| Flag | In the Dockerfile above |
| --- | --- |
| `--allow-read=PATHS` | `/app` and the npm store `/root/.cache/aube`. |
| `--allow-net=HOSTS` | Outbound hosts. Listening on a port needs no grant. |
| `--allow-run=CMDS` | Not given: subprocesses stay allowed. Give a list to restrict them. |
| `--deny-net=HOSTS` | Hosts to block even when allowed, e.g. `--deny-net=169.254.169.254`. |
| `--max-mem=SIZE`, `--timeout=DURATION` | Heap size and total run time. |

Global flags go before the command (`serve`, `run`). `--deny-net` and `--deny-run` need the `=` form. Leaving the npm
store out of `--allow-read` stops the server at startup:

```text
Error: Filesystem read denied by policy: /app/node_modules/hono
```

A blocked outbound call names the flag:

```text
fetch to '169.254.169.254:80' denied by --deny-net
```

Every flag: [Sandbox and Limits](https://elide.help/docs/sandbox).

## Read logs

`console.log` writes to stdout and `console.error` to stderr; `docker logs` shows both:

```text
{"method":"GET","path":"/healthz","status":200}
{"method":"GET","path":"/","status":200}
```

`elide serve --no-tui` prints no access log, and a handler that throws answers `500` without printing the error. Log
requests and errors in the app, or export request spans, metrics, and errors with [OpenTelemetry](https://elide.help/docs/opentelemetry).

## Deploy a JVM app

- Build a JVM image from `elide.pkl` with a `ContainerImage` artifact: [JVM Containers](https://elide.help/docs/toolchain/jvm/containers).
- Compile a native executable: [Native Image](https://elide.help/docs/toolchain/jvm/native-image).
