Skip to content

Deployments

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.

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.

  • 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 binary instead.
  • 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.

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.

queued ──► building ──► deploying ──► succeeded
└─► failed
└─► rolled_back
  1. queued: the row exists and the job is on gisila:queue:deployments. The app’s status becomes building.
  2. building: the worker runs provision (user, directories, .env), then build (fetch the source and run the runtime’s build step). Pass forceRebuild: true to skip the dependency and build cache.
  3. deploying: apply-unit writes the AppArmor profile and systemd unit. Then, depending on the app’s exposure mode, apply-vhost (web) or expose-port/unexpose-port (tcp) runs. Finally the unit restarts.
  4. health check: the agent’s app-health decides whether the app is actually serving (see below).
  5. succeeded: the deployment becomes active, every earlier one inactive, and the app running. failed: the reason is stored in failure_reason, and the app becomes crashed if the health check failed.

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 web app 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.

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.

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.

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.

Deploy a binary built in CI:

Terminal window
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\"}"