Skip to content

App runtime contract

This page lists what your app can rely on when the panel runs it, and what it has to do to be considered healthy. There is no build or run config file to add to your repository: everything is set on the app in the panel (or through the Apps API).

Variable Value When
PORT The app’s internalPort Apps with a port: web, tcp, internal. Not static or process.
GISILA_APP_ID The app’s numeric id Always
HOME <workDir>/tmp Always. XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME and XDG_STATE_HOME point inside it.
PATH The runtime’s bin directory, then /usr/local/bin:/usr/bin:/bin Runtimes with a toolchain directory
MEDIA_ROOT, MEDIA_URL <workDir>/shared/media, /media/ When mediaEnabled is on, unless you set them
<PREFIX>_ENDPOINT, _REGION, _BUCKET, _ACCESS_KEY, _SECRET_KEY, _PUBLIC_URL, _FORCE_PATH_STYLE The linked bucket’s credentials When a bucket is linked (default prefix S3)

Your own env vars are written to <workDir>/.env (mode 0640, one KEY="value" per line) and loaded with systemd’s EnvironmentFile=. They override values the panel sets for the same name. They’re refreshed on each deploy, not on restart. The same file can be loaded in a shell with set -a; source .env; set +a.

Env vars are also present during the build.

Variables the panel reads from your app’s env

Section titled “Variables the panel reads from your app’s env”
Variable Effect
GISILA_SYSCALL_FILTER=off Drops the seccomp syscall filter for this app.
FLOWER_BASIC_AUTH=user:password Celery apps: Flower UI credentials (generated if unset).
  • Listen on PORT. For a web app, 127.0.0.1:$PORT is enough: Nginx proxies the app’s domains to it. Binding 0.0.0.0 also works but exposes the port whenever the host firewall allows it.
  • A tcp app must bind 0.0.0.0:$PORT. Public reachability is controlled by the panel’s ufw rule.
  • An internal app is reachable by other processes on the host at 127.0.0.1:$PORT.
  • A process app gets no port and must not need one.
  • Behind Nginx, the client address is in X-Real-IP and X-Forwarded-For, and the original host in Host.

Outbound connections are allowed (AF_INET, AF_INET6, AF_UNIX, AF_NETLINK).

Path Access Use
<workDir>/current read-only The live build. The working directory for compiled runtimes.
<workDir>/releases read-only Kept builds
<workDir>/shared read-write Persistent data across deploys (uploads, SQLite files)
<workDir>/tmp read-write Scratch space and HOME
<workDir>/logs read-write Optional log files
/tmp private A per-service private /tmp

<workDir> is /srv/apps/app_<6 chars>. Everything else on the host is read-only or invisible. Python, Node, Bun and Rust apps run with their working directory in the source tree, which is also writable for Node and Bun.

With no startCommand, the runtime’s default runs:

Runtime Default
dart, go, zig, binary <workDir>/current/app (the compiled or uploaded executable)
node Detected from the project (for example Next, Nuxt or npm start)
bun bun run start
python gunicorn serving wsgiApp (pythonMode wsgi or asgi)
celery Celery workers, optional beat, and Flower
rust None. Set one.
static No process

A start command is typed as you would in a shell inside the project. The agent rewrites its first word to an absolute path, because systemd doesn’t search the project’s tooling:

You type Becomes
app (compiled runtimes) <workDir>/current/app
gunicorn app:application <venv>/bin/gunicorn app:application
python manage.py migrate <venv>/bin/python manage.py migrate
next start -p 3000 <node>/bin/node <src>/node_modules/.bin/next start -p 3000
npm start <node>/bin/node <node>/bin/npm start
api (rust) <src>/target/release/api
/opt/x/bin/x unchanged

Rules:

  • Start and build commands can’t contain |, &, ;, $, <, >, backticks or control characters. Build commands therefore can’t chain steps. Put them in a script.
  • tcp, internal and process apps on python, node, bun or rust must have a start command.
  • Every rewrite is logged in the build output as [agent] ExecStart: "…" → "…".
  • Builds run as the app’s Linux user, in the source directory (or sourceSubdir), with the app’s env vars and the pinned toolchain on PATH.
  • Compiled runtimes must leave the executable at build/app, at the path given to -o/--output in the build command, or at bin/server.exe, bin/server, build/server or build/server.exe. It’s installed as <workDir>/current/app.
  • The Dart build sets PUB_CACHE=<workDir>/.pub-cache.
  • Dependency and build caches are reused across deploys unless the deployment sets forceRebuild.

After every deploy and rollback, the app must pass the health check within 45 seconds and stay healthy for 5 consecutive seconds:

  1. Its systemd unit(s) are active and their restart counter doesn’t climb.
  2. If it has a port, 127.0.0.1:$PORT accepts a TCP connection.
  3. If it’s a web app with a healthCheckPath, a GET of that path on 127.0.0.1:$PORT returns a status below 500. Redirects aren’t followed.

Use a cheap, unauthenticated path such as /healthz. In a Gisila app, AppConfig(healthCheckPath: '/healthz') serves one (see the framework docs).

Setting Value Meaning
Restart=always, RestartSec=5 A crashed process restarts after 5 seconds.
StartLimitBurst=5 in StartLimitIntervalSec=300 After 5 failures in 5 minutes the unit is parked as failed and the app shows crashed.
KillMode=mixed, KillSignal=SIGTERM On stop or restart, only the main process gets SIGTERM, so prefork servers can drain their workers.
TimeoutStopSec=30 Anything still running 30 seconds later is killed.

Handle SIGTERM by finishing in-flight work in under 30 seconds.

Setting From Default
MemoryMax memoryMbLimit 256 MB
CPUQuota cpuQuotaPercent (percent of one core) 50%
TasksMax tasksLimit (processes and threads) 100
LimitNOFILE exposure mode 4096 for web, 65536 otherwise

Hitting MemoryMax gets the process OOM-killed inside its own cgroup. Count threads against TasksMax: runtimes such as the Dart VM, Node and the JVM start several threads even for one process.