Chapter 10
A push to your repository deploys the site.
Chapter 04 got your code on by hand. This is the automated version: connect the site to a Gitea repository once, and every push runs your build and publishes it — with visitors seeing a short maintenance page instead of a half-updated site.
This is for catalog sites. A custom stack cannot use it — its deploys build images, and the runner deliberately has no access to Docker. A stack is deployed by pressing Deploy in the panel.
How it runs, and why that is safe
When deployments are enabled, a runner is registered for your repository and runs inside your site's own container. That means it already has your app's tooling — PHP, Composer, the bundled Node — and it reloads the site through the container's own supervisor. It never touches the box's Docker, and a push to your repo can only ever affect your site. There is no shared build farm to queue behind and nothing your job can reach beyond its own container.
Turning it on
Enabling happens on the site's Deployments page in the panel: find your repository, pick it, and switch it on. If the site is yours you can do that yourself; if you have no panel account, your operator does it for you. Either way the deploys themselves need no account — you interact with them entirely through your repository.
Finding it is a search rather than a scroll, and the search has no ceiling: the matching happens inside Gitea, so it makes no difference how many repositories there are or where yours falls among them.
- Type at least three characters of the repository's name into the search field and press Search. A shorter query is refused, and the page says so under the list.
- The matches appear in the list below, which stays empty and greyed out until you have searched.
- Pick yours, then press Enable deployments.
The match is on the repository name alone, never the owner or the organisation. Typing my-org/my-repo finds nothing; type my-repo.
If a search returns nothing at all for a repository you know exists, that is one to raise with your administrator — it is about what the panel's connection to Gitea can see, which is a setting rather than anything you can fix from here. And if Gitea cannot be reached, the search says so rather than the page quietly failing.
Two things come back to you from that page:
- A runner label — the value your workflow targets with
runs-on:. It is your site's container name, e.g.no-app-example-com. - Confirmation that the runner is registered and idle, waiting for your first push.
Because the runner is registered before your repo has anything in it, your very first push is an ordinary deploy — there is no separate "seed the folder" step. Checkout authenticates automatically; you do not add a deploy key or store any secret.
The workflow
Copy the example matching your image into your repo as .gitea/workflows/deploy.yml and fill in the label and your domain. The full files are in .gitea/workflows-examples/ (a Laravel one and a static one). The shape is the same for both:
jobs:
deploy:
runs-on: no-app-example-com # your runner label
steps:
- run: nimbus deploy:begin # maintenance page on, workers stopped
- uses: actions/checkout@v4
# … install, build, publish, migrate …
- run: nimbus deploy:end # new code live, workers back, page off
if: always()
There is a companion file written for coding agents, deployment.md (next to this guide). It states the whole deploy model, the exact facts an agent needs from you — the runner label, the domain, the image type — and ready-to-adapt workflow templates. Drop it into your repository as deployment.md, or point your agent at it, and it can write the workflow itself.
The two hooks, and the one rule
nimbus deploy:begin and nimbus deploy:end bracket the risky middle of a deploy. Between them the site answers every request with a maintenance page (a 503), so no visitor ever sees a half-published folder or a half-run migration, and your background workers are stopped so none runs old code against a freshly migrated database. deploy:end then reloads PHP — which is what makes your new code actually take effect — restarts the workers, and drops the maintenance page.
The closing hook must carry if: always(). Without it, a step that fails — a red test, a broken build — would stop the workflow before deploy:end runs, and your site would sit behind the maintenance page. With it, the page always lifts and visitors get the previous working site back. As a safety net the platform also clears a stuck window automatically after a few minutes, but do not rely on that — it is the last resort, not the plan.
What you do not have to do
No restart from the panel after a normal deploy — deploy:end already reloaded PHP, so the opcache is reset and your new code is live. Even a push that adds a background worker for the first time takes effect without a restart. A restart is only needed the first time deployments are enabled — one click, by whoever turned them on.
When it does not deploy
- The maintenance page stays up. A job failed after
deploy:beginand either did not reachdeploy:endor you omittedif: always(). Fix the workflow; the window clears itself after the deadline in the meantime. - Nothing happens on push. Check the branch in your workflow's
on: pushmatches the branch you pushed, and thatruns-on:is exactly the label from the Deployments page. - A change is not visible. Confirm your publish step actually wrote into the live folder (
/var/www/virtual/<your-domain>) — the runner's checkout lands in a work directory, not the docroot, until you copy it across.