This page packages a TypeScript server as a container image on elide.dev/elide.
Write the app
// 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;// healthcheck.mjs
const res = await fetch("http://127.0.0.1:8080/healthz");
process.exit(res.ok ? 0 : 1);{
"name": "deploy-app",
"type": "module",
"dependencies": {
"hono": "^4.7.0"
}
}Run elide install --ecosystems=npm once to write aube-lock.yaml.
Write the 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"]# .dockerignore
node_modules
.env| Line | Why |
|---|---|
FROM elide. | The image’s entrypoint is elide, so CMD holds only its arguments. Tags: Container Images. |
RUN elide install --ecosystems= | 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. |
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/{"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:
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.
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=. |
--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:
Error: Filesystem read denied by policy: /app/node_modules/honoA blocked outbound call names the flag:
fetch to '169.254.169.254:80' denied by --deny-netEvery flag: Sandbox and Limits.
Read logs
console.log writes to stdout and console.error to stderr; docker logs shows both:
{"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.
Deploy a JVM app
- Build a JVM image from
elide.pklwith aContainerImageartifact: JVM Containers. - Compile a native executable: Native Image.