GitHub deploy
Clones, builds and publishes a GitHub repo on the VPS; redeploys on every push, phase history and rollback.
- Category: Servers and infrastructure
- Status: Available
- Requirements: An active company VPS with the
deploy_githubfeature available; The linking person's GitHub account connected to ArduMaker with access to the repo; Being a company member with access to that VPS (in Git a member can do the same as an admin) - MCP tools (Read only):
listar_repos_github_vps,ver_repo_github_vps,listar_commits_repo_github_vps,ver_commit_repo_github_vps,listar_logs_repo_github_vps,ver_log_repo_github_vps,ver_asistente_git_app,ver_config_deploy_app,ver_propietarios_github,ver_operacion_app - MCP tools (Write or modify):
instalar_app,vincular_repo_github_vps,crear_repo_github_vps,editar_repo_github_vps,redeploy_repo_github,guardar_config_deploy_app,controlar_app - MCP tools (Destructive):
eliminar_repo_github_vps,desinstalar_app,publicar_repo_en_dominio
What can you do with it?
Git deploy takes a GitHub repository to a VPS and keeps it up to date: it clones it, builds it, leaves it running as a systemd service (or container), publishes it with HTTPS and redeploys it on every push to the chosen branch.
- Detect what the repo is (Node, Django, Python, static site, Docker, Procfile) and propose build and start commands.
- Declare in a deploy document the phases (
pre_pull,install,build,release), the processes (services, timers, one-off jobs, static, Docker, TCP), environment variables and publishing. - Deploy automatically on every push, by hand, or a specific commit of the branch.
- Roll back to the commit of a previous deploy that finished well.
- See the deploy history with every phase, its timing and the commands with their output (secrets redacted).
- Create a new GitHub repository from ArduMaker and deploy it.
When should you use it?
- A website, an API or a worker that lives on GitHub and must always match the main branch.
- Scheduled jobs of the project (a
timer) that must travel with the code. - A team that wants to see which commit is in production and be able to roll back without logging into the server.
How does it work in ArduMaker?
1. Link and clone
There are two doors. The recommended one is installing it as an app (instalar_app with app_type: git, repo_id, branch and optionally port and the deploy document): it lands in the Apps inventory and the operation is durable. The other is the VPS GitHub tab (vincular_repo_github_vps), which only clones and runs in the background.
When linking, ArduMaker creates a webhook on GitHub with a secret of its own for that repo: each delivery arrives signed with HMAC-SHA256 and nothing secret in the URL. The repo is cloned into <deploy-folder>. The URL with the GitHub token stays in the .git folder, owned by root and unreadable by the app.
2. Detect and configure
ver_asistente_git_app lists the repos the GitHub account can read, their branches and, with repo and branch, what it detects by reading files without running anything: framework, build and start commands and a free VPS port. Known formats are Docker (also Compose), Procfile, Node web, Django, Vite static site, Python web and manual. A proposed migration always arrives unconfirmed: the document does not validate until a person confirms it.
What every deploy runs is always a document: the one saved with guardar_config_deploy_app or, if the repo never saved one, the one derived from its usual scripts (pre-pull, post-pull and services). Saving a document migrates the repo: from then on the document rules.
3. Deploy in phases
Every deploy runs these phases in order and records them in the history. If one fails, the following ones do not run and the repo stays in error with a short text (Deploy failed at the <phase> phase), never the raw output of a command.
| Phase | What it does |
|---|---|
preparar | App Linux user, platform folder, variable files, Docker if needed |
pre_pull | phases.pre_pull in the clone folder |
pull | Pulls the branch (or leaves the folder at the requested commit) |
install | phases.install, long step |
build | phases.build, long step; then the containers of docker processes |
release | The migration, only if confirmed |
procesos | Writes or restarts units, timers and one-off jobs |
publicar | Domain server block in the VPS nginx, if the profile publishes |
salud | Checks that the app answers or that the units stay alive |
With the app installed in Apps, the deploy is run by the operations runner and resumes if the worker dies. A repo without an app runs the same deploy in the background. Several pushes in a row are merged: at most one deploy running and one queued per app.
What actually changes?
<pk> is the repo number in ArduMaker. With policies.run_as: "root" the user is not created and everything belongs to root.
| What | Where | Owner and mode |
|---|---|---|
| App user | ardu-app<pk>, system user, no login | — |
| Clone | <deploy-folder> | the user's; .git owned by root |
| Platform folder | /etc/ardumaker/deploy/ardu-repo<pk>/ | root:ardu-app<pk> 0750 |
| Runtime and build variables | …/run.env, …/build.env | ardu-app<pk> 0600 |
| Scripts of each step and their output | …/install.sh, build.sh, release.sh, <paso>.log | 0750 / root |
| Main service | /etc/systemd/system/ardu-repo<pk>.service | root 0644 |
| Other processes and timers | ardu-repo<pk>-<proceso>.service / .timer | root 0644 |
| Containers | ardu-repo<pk>, ardu-repo<pk>-<proceso> | — |
| Server block | /etc/nginx/sites-available/<dominio>.conf | — |
A unit that did not change is only restarted; one that changed is rewritten. An adopted unit (from an older repo) is restarted and never written or deleted. Whatever a deploy created and the document no longer has is deleted on the next deploy.
The deploy document
It is a versioned JSON (schema_version: 2). It is read with ver_config_deploy_app and fully replaced with guardar_config_deploy_app (apply=true applies it right away with a reconfigure).
| Part | Values |
|---|---|
profile | static, web, worker, scheduled, job, docker, network and code (code only: each push fetches the repository and nothing else; no install, build, migration, processes or publishing) |
phases | pre_pull, install, build and release (command, confirmed, on_update) |
processes | Up to 20, of type service, timer (with schedule), oneoff, static, docker or tcp; ports 1024 to 65535, no repeats |
network.publish.mode | none, auto_subdomain, domain, port |
network.health.type | http, unit, timer, static |
env | List of {name, value} with scope build, run or both |
policies | build_timeout_s 60–7200 (600 by default, per step), memory_max_mb and cpu_quota_pct unlimited by default, run_as (app_user or root), rollback_on_failed_health |
autodeploy | enabled: whether a push deploys |
Variables and secrets
A variable with a secret-like name (*_TOKEN, *_PASSWORD, *_KEY…), with secret: true or with a URL carrying user and password is stored encrypted and only a reference stays in the document. On read, env shows only {name, scope, has_value}, never a value. Sending a variable without value keeps the stored one.
- Values are written to
run.envandbuild.env(0600) only in theprepararphase. A value with a line break is rejected. - Changing a build variable (
NEXT_PUBLIC_*,VITE_*) requires a rebuild:reconfiguredoes it with the same code and without release. - In each command's log the secret payload is replaced by its size and the output has every secret redacted (
[redacted]).
Long steps, limits and health
install, build and release each run in their own transient unit, as the app user, with a private /tmp and set -e (stops at the first failing command):
sudo systemd-run --unit=ardu-deploy<pk>-<paso> --wait --collect --pipe \
-p User=ardu-app<pk> -p RuntimeMaxSec=<build_timeout_s> -p NoNewPrivileges=yes -p PrivateTmp=yes \
-p EnvironmentFile=/etc/ardumaker/deploy/ardu-repo<pk>/build.env \
/bin/bash /etc/ardumaker/deploy/ardu-repo<pk>/<paso>.sh| Health | What is checked |
|---|---|
http | curl to 127.0.0.1:<port><path> up to 25 times; without expect any answer other than 5xx is fine |
unit | Each unit active twice with 8 s in between and no new restarts |
timer | The .timer enabled and active, and its schedule valid |
static | The folder exists and the VPS nginx answers without 5xx for the domain |
Ports and publishing
The app listens on an internal VPS port. If installed through Apps without port (or with "auto"), the first free port is taken. Engine and agent ports are never published. The automatic address is git-<id> inside the platform zone; a custom domain pointing to the VPS can be used too.
The publicar phase writes the server block in the VPS nginx; DNS, the proxy to the VPS, the certificate and the HTTPS link check are done by Apps publishing. If that part fails (for example, DNS has not propagated yet), the retry publishes without rebuilding. A hand-edited nginx is not overwritten.
Pushes, specific commits and rollback
- A push to another branch is ignored. With
autodeployoff, a repeated GitHub delivery or a deploy already running, the push records the commits but does not deploy, and the answer says why. redeploywithcommit_shaleaves the folder at that commit of the branch and runs a normalupdate, with release if confirmed.rollbackwithrun_idreturns to the commit of a deploy that finished well (git reset --hard <sha>) with the current configuration. A deploy of another repo, a failed one or the one already deployed is refused.- With
rollback_on_failed_health: true, a push-triggered deploy that failssaludgoes back once to the last healthy commit and turns autodeploy off, recording the reason.
Unlinking a repo
Deleting the repo removes what the document created: units and timers, containers, the platform folder, the app user with its folder and the safe.directory entry; then the main unit and container, the clone and the GitHub webhook. Adopted items are left alone.
Diagram
- GitHub: push to the tracked branch; signed webhook
- ArduMaker backend: records commits and queues the deploy
- VPS agent: runs each phase
- Clone + build:
systemd-runasardu-app<pk> - `ardu-repo<pk>.service`: the app on
127.0.0.1:<port> - VPS nginx: domain server block
- Internet: HTTPS on the published domain
Expected result
The repo ends up pulled (or online after installation) with last_commit_sha equal to the commit in the folder. ver_repo_github_vps returns last_runs: origin (webhook, manual or mcp), commits, result and how each phase went. If it was published, ver_app shows the HTTPS URL only once the link was verified.
systemctl status ardu-repo<pk>.service
curl -I http://127.0.0.1:<puerto>/Errors and troubleshooting
| Error | Cause | Retry? |
|---|---|---|
github_no_vinculado | The requester has no GitHub connected or the token expired | Yes, after reconnecting GitHub |
repo_inaccesible / rama_inexistente | The account cannot read the repo or the branch does not exist | No |
clon_fallido / github_no_responde | The clone failed (leaves nothing behind) or GitHub did not answer | Yes |
puerto_ocupado | Another app on the VPS already uses that port | With another port |
deploy_en_curso | Another deploy was already running on that repo | Yes, when it finishes |
preparacion_fallida, pull_fallido, build_fallido, release_fallida, app_no_arranco, salud_fallida, nginx_del_vps_invalido | The phase that failed | By hand, never by itself |
config_invalida | The document does not validate; errors by path (processes.web.port) | After fixing the document |
preparar failed - missing value for <VARIABLE>: a variable value is missing; the deploy stops before touching the VPS.limits_not_appliedwarning: the VPS rejected the memory or CPU limits and the step ran without them.health_check_failedwarning: on a repo with inherited configuration a failing health check is a warning, not an error.- The app answers only on
[::1]: the VPS nginx reaches127.0.0.1; it must listen on127.0.0.1or0.0.0.0.
Where to look: ver_repo_github_vps (latest deploys and phases), listar_logs_repo_github_vps and ver_log_repo_github_vps (command and output, with token and secrets redacted), and ver_operacion_app for the Apps operation.
MCP access
Git deploy has full MCP access; reference at /mcp/docs and /mcp/deploy. Writes can be done by any member with access to the VPS, except an agent authorized as read-only.
| Kind | Tools |
|---|---|
| Read | listar_repos_github_vps, ver_repo_github_vps, listar_commits_repo_github_vps, ver_commit_repo_github_vps, listar_logs_repo_github_vps, ver_log_repo_github_vps, ver_asistente_git_app, ver_config_deploy_app, ver_propietarios_github |
| Modify | instalar_app, vincular_repo_github_vps, crear_repo_github_vps, editar_repo_github_vps, guardar_config_deploy_app, redeploy_repo_github, controlar_app, publicar_repo_en_dominio |
| Destructive | eliminar_repo_github_vps, desinstalar_app |
controlar_app on a git app accepts redeploy (with optional commit_sha), rollback (with run_id) and reconfigure (applies the document without pulling code). publicar_repo_en_dominio does domain, verification, SSL activation, server block and installation in one call; with simular=true it shows the plan and with desde it resumes where it failed.