Deployments
What it is
Section titled “What it is”A deployment is one attempt to build and run an app. It records the
source (git, binary or zip), the commit or upload, a status, the build
log, and a snapshot of the configuration it ran with. A successful deployment
publishes a release directory that stays on disk so you can roll back to
it without rebuilding.
When to use it
Section titled “When to use it”| Source | Use it when |
|---|---|
git |
Your code is in a repository the server can clone. The panel builds on the host. Supports push-to-deploy webhooks and SSH deploy keys for private repos. |
binary |
You build in CI and upload a ready-to-run executable (an ELF binary or a #! script). Nothing is compiled on the server. |
zip |
You can’t give the server repository access, or want to deploy a working tree. The archive is unpacked and built like a git checkout. |
An app’s source type is fixed when you create it. A deployment request whose
sourceType differs from the app’s is rejected.
When not to use it
Section titled “When not to use it”- Don’t build large projects on a 1 GB VPS from git if you can avoid it:
compiling Rust or Dart AOT can use more memory than the host has. Build in CI
and deploy a
binaryinstead. - Uploads are capped at 50 MB (the API’s request body limit and the panel
vhost’s
client_max_body_size). For bigger artifacts, use git.
How it works
Section titled “How it works”Runtimes and deploy modes
Section titled “Runtimes and deploy modes”An app targets an installed Application (a runtime toolchain, managed by a superuser on the Applications page). Each runtime supports one deploy mode:
| Runtime | Mode | Default build command | Default start command |
|---|---|---|---|
dart |
build_execute |
dart pub get && dart compile exe bin/server.dart -o build/app |
<workDir>/current/app |
go |
build_execute |
go build -o build/app ./... |
<workDir>/current/app |
rust |
build_execute |
cargo build --release |
none, you set it |
node |
build_execute |
npm ci |
detected from the framework |
bun |
build_execute |
bun install |
bun run start |
celery |
build_execute |
venv + pip install -r requirements.txt |
Celery workers (+ beat, Flower) |
python |
direct_run |
venv + pip install -r requirements.txt |
gunicorn |
binary |
direct_run |
none, the upload is the program | <workDir>/current/app |
zig |
build_execute |
you set it | you set it |
static |
static_publish |
none | none, Nginx serves files |
build_execute: build or package, then run the artifact.direct_run: run the source or uploaded binary in place.static_publish: no process. Nginx serves the files.
dart, go, rust, node, bun and python are versioned: several
toolchain versions can be installed side by side. An app can pin one
(dartVersion, goVersion, …) or use the runtime’s default version.
For compiled runtimes, the build output that ends up at
<workDir>/current/app is found in this order: build/app, then the path
after -o/--output in your build command, then bin/server.exe,
bin/server, build/server, build/server.exe.
sourceSubdir builds and runs from a subdirectory, so you can deploy one
project out of a monorepo.
The build runs as the app’s own Linux user, so a malicious build step can only touch that app’s directory.
Lifecycle
Section titled “Lifecycle”queued ──► building ──► deploying ──► succeeded └─► failed └─► rolled_back- queued: the row exists and the job is on
gisila:queue:deployments. The app’s status becomesbuilding. - building: the worker runs
provision(user, directories,.env), thenbuild(fetch the source and run the runtime’s build step). PassforceRebuild: trueto skip the dependency and build cache. - deploying:
apply-unitwrites the AppArmor profile and systemd unit. Then, depending on the app’s exposure mode,apply-vhost(web) orexpose-port/unexpose-port(tcp) runs. Finally the unit restarts. - health check: the agent’s
app-healthdecides whether the app is actually serving (see below). - succeeded: the deployment becomes active, every earlier one inactive,
and the app
running. failed: the reason is stored infailure_reason, and the app becomescrashedif the health check failed.
Health check after every deploy
Section titled “Health check after every deploy”A systemd Type=simple unit is “active” the moment it forks, which says
nothing about whether your app started. So after the restart the agent polls
for up to 45 seconds, and the app has to stay healthy for 5 seconds in a row:
- Every unit stays active and its restart counter doesn’t climb.
- If the app has a port, something accepts a TCP connection on
127.0.0.1:<port>. - If it’s a
webapp with a health check path (for example/healthz),GET http://127.0.0.1:<port><path>answers with any status below 500. Redirects aren’t followed.
Static sites only check that Nginx is running. Celery and process apps are
judged by unit state alone. If the check fails, the build log ends with the
reason and the last journal lines of the unit.
Release retention
Section titled “Release retention”Each successful build is published as releases/<deploymentId> and recorded
on the deployment, together with a JSON snapshot of the unit arguments,
including env vars. The snapshot holds secret values and is never returned by
the API. The agent keeps the new release plus those of the newest
releasesToKeep successful deployments (default 5, settable from 1 to 50 per
app) and prunes the rest.
Rollback
Section titled “Rollback”POST /apps/{id}/deployments/{depId}/rollback queues a new deployment with
rollback_of_id set. The worker skips provision and build:
activate-release points current back at the target’s release, apply-unit
runs with the target’s recorded env vars and settings (not today’s), then
the unit restarts and the health check runs. A rollback takes seconds.
You can roll back only to a deployment that succeeded, isn’t already live,
still has its release on disk, and was made after releases started being
recorded. The deployments list marks these with canRollback: true.
Push to deploy
Section titled “Push to deploy”A git app can have a webhook (POST /apps/{id}/webhook, admin). GitHub,
Gitea, Forgejo and GitLab deliveries to POST /webhooks/apps/{id} are checked
against the app’s secret. A push to the app’s branch (or the default branch
when none is set) queues a deployment of that commit. Pings, tags, other
branches, redeliveries and pushes while a deployment is still queued are
acknowledged and skipped. With a status token set, the panel reports each
push-triggered deployment back to the commit as gisila-panel/deploy.
Example
Section titled “Example”Deploy a binary built in CI:
H='Authorization: Bearer gsl_xxxxxxxxxxxxxxxx'
ARTIFACT=$(curl -sX POST https://panel.example.com/apps/42/deployments/artifacts \ -H "$H" -H 'Content-Type: application/octet-stream' \ --data-binary @build/app | jq -r .artifactId)
curl -sX POST https://panel.example.com/apps/42/deployments/ \ -H "$H" -H 'Content-Type: application/json' \ -d "{\"sourceType\": \"binary\", \"artifactId\": \"$ARTIFACT\"}"