gon server
Provision and manage VPS servers for GON deployments.
Architecture
Each gon server runs a stack of cooperating services. Knowing the layout makes the failure modes far easier to debug:
Internet ↓ :443 (TLS)host nginx ← /etc/nginx/sites-enabled/gon-{project}.conf ↓ proxies to 127.0.0.1:8080Traefik (Docker) ← /opt/traefik/docker-compose.yml ↓ routes by Host header via container labelsProject containers • nginx (in traefik-public network) • app (php-fpm) • redis • horizon (optional) • scheduler Host services (shared by all projects): • Docker daemon • MySQL (bound to 0.0.0.0; containers reach it via host.docker.internal) • certbot (issues + auto-renews Let's Encrypt certs for nginx vhosts)
Containers reach MySQL through Docker's host.docker.internal magic hostname, which resolves to the docker bridge IP (typically 172.17.0.1). The MySQL grants created by server:add-project pin each project user to 'localhost' + '172.%' so even with bind-address 0.0.0.0, only the docker bridge subnet can authenticate.
server:setup
One-time server initialization. Idempotent — safe to re-run after gon-cli upgrades to pick up template fixes for the system-level pieces.
gon server:setup 142.93.174.86 \--alias=vps1 \--user=root \--email=ssl@example.com \--ghcr-token=ghp_xxxxx
What it does (9 steps)
- Check SSH connectivity
- Detect OS
- Install Docker (via
get.docker.com) - Install system packages:
mysql-server,nginx,certbot,python3-certbot-nginx - Configure MySQL bind-address to
0.0.0.0so containers can reach it through the docker bridge - Create
deployuser (indockergroup, your local SSH key copied over) - Create
traefik-publicDocker network - Setup Traefik reverse proxy (host ports
8080/8443) - GHCR login (optional — backfill later via
server:ghcr-loginif skipped)
Sudo-aware (works on AWS, GCP, Azure)
Modern cloud Ubuntu AMIs disable root SSH by default — you log in as ubuntu / ec2-user / admin with NOPASSWD sudo. Pass that as --user and gon-cli automatically wraps every privileged command (apt install, systemctl, /etc/nginx writes) with sudo -n bash. Servers configured for direct root SSH (Hetzner, DigitalOcean classic) keep working bit-identically — the wrapper is a no-op when --user=root.
If you skip --ghcr-token, the server boots without GHCR credentials and the first gon deploy will fail with error from registry: unauthorized. Backfill anytime with gon server:ghcr-login --host=vps1 — no need to re-run setup.
server:add-project
Prepare server for a project: MySQL database, nginx vhost, SSL, Docker config.
gon server:add-project myapp \--host=vps1 \--domain=myapp.example.com
Vhost conflict pre-flight
Before any mutation runs, gon-cli checks whether another project on the same host already serves the requested domain. Two nginx vhosts with the same server_name make nginx pick the alphabetically-first one and silently ignore the rest — a footgun that surfaces as 502s through the older un-buffered vhost. The pre-flight refuses to proceed and prints the exact remediation:
$ gon server:add-project gon --host=vps1 --domain=app.example.com --env=staging Domain "app.example.com" is already served by another project's nginx vhost on vps1: /etc/nginx/sites-enabled/gon-oldapp.conf Two vhosts for the same server_name make nginx pick the alphabetically-first one and silently ignore the rest → buffer / SSL config drift, eventually 502 on the live site. To resolve, retire the old project first: gon server:remove-project oldapp --host=vps1 --keep-ssl --keep-db --keep-ssl: hold the existing Let's Encrypt cert so the new vhost can reuse it --keep-db: leave the old project's database untouched (drop it later if unused) Or pass --force-vhost to overwrite anyway (advanced — risk of orphaned SSL/DB).
The current project's own gon-{project}.conf is excluded from the check, so idempotent re-runs of the same project don't trip it. Pass --force-vhost if you really want to leave both vhost files on disk (advanced — typically only useful when manually migrating SSL between vhosts).
SSL diagnostics
The certbot step passes -m EMAIL from the server config (saved by server:setup as acme_email) so --non-interactive mode can register a fresh ACME account. When certbot fails, gon-cli surfaces the actual reason in the summary table (parsed from certbot's Detail: line — typically DNS not propagated, HTTP-01 challenge timeout, or rate-limit) and prints a copy-pasteable retry command using the correct SSH user:
SSL retry once DNS for app.example.com resolves to 1.2.3.4: ssh ubuntu@1.2.3.4 "sudo certbot --nginx -d app.example.com -m you@example.com --non-interactive --agree-tos --redirect"Tip: dig +short A app.example.com @1.1.1.1 # confirm the A record before retrying
Multiple environments per project
Pass --env=NAME to register a non-production environment of the same project on a different host. The first time you do this on a project, gon-cli migrates the existing flat record into a nested environments map, then appends the new env alongside.
# Production (default — equivalent to --env=production)gon server:add-project myapp \ --host=vps1 \ --domain=myapp.example.com # Staging on a different hostgon server:add-project myapp \ --host=staging-1 \ --domain=staging.myapp.example.com \ --env=staging
After this, gon release / gon deploy / gon env:set/push/pull all accept the env name as the routing argument:
gon release staging # builds + deploys to staging-1gon env:set DEBUG=true staging # sets DEBUG on staging's .env only
Each environment is independent: its own /opt/myapp directory, its own MySQL database, its own SSL certificate. Tearing one down with gon server:remove-project myapp --env=staging leaves the others untouched. Full walk-through with migration tips: multi-env recipe.
Options
| Option | Description |
|---|---|
--host | Server alias or IP (must already be set up via gon server:setup) |
--domain | Domain to bind for this environment |
--env | Environment name (default production). Multiple envs per project are stored side by side under environments[name]. |
--horizon | Enable Laravel Horizon worker container |
--no-ssl | Skip Let's Encrypt certificate issuance |
--no-db | Skip MySQL database / user creation |
--force-vhost | Bypass the "domain already served by another project" pre-flight check (advanced — may leave orphaned SSL / vhost) |
server:auth
Toggle HTTP Basic Auth for staging/pre-launch.
gon server:auth myapp --enablegon server:auth myapp --disable
server:ghcr-login
Register or refresh GitHub Container Registry credentials on a server so docker can pull private ghcr.io/rozklad/gon-* images. Use this when the server was set up without --ghcr-token and gon deploy fails with error from registry: unauthorized, or when your PAT has rotated.
gon server:ghcr-login --host=vps1 # Prompts for the PAT (hidden input)gon server:ghcr-login --host=vps1 --token=ghp_xxx # Pass the PAT explicitlygon server:ghcr-login --all --token=ghp_xxx # Push the same PAT to every registered server
The login runs as the deploy user (placed in the docker group during server:setup), so credentials land in /home/deploy/.docker/config.json — exactly where docker compose looks during a pull.
PAT requirement: GHCR pulls only accept classic personal access tokens with the read:packages scope. Fine-grained tokens are rejected by the registry. Create a PAT at https://github.com/settings/tokens/new?scopes=read:packages&description=gon-cli.
server:remove-project
Tear down a deployed project from a server: stops containers, drops database, removes nginx vhost, SSL cert, and project directory.
gon server:remove-project myapp # Default env (with confirmation)gon server:remove-project myapp --env=staging # Tear down only staginggon server:remove-project myapp --force # Skip confirmationgon server:remove-project myapp --keep-db # Keep database and usergon server:remove-project myapp --keep-ssl # Keep SSL certificategon server:remove-project myapp --host=vps1 # Override saved host
What gets removed
- Docker containers and volumes
- Nginx vhost configuration
- SSL certificate (unless
--keep-ssl) - MySQL database and user (unless
--keep-db) - Project directory (
/opt/{project}) - The targeted environment's entry in
~/.gon/servers.json
Multi-environment projects
For projects with several environments, --env=NAME narrows the teardown to that one environment only — other envs stay registered and running on their own hosts. Without --env, the project's default environment is removed. If that was the last remaining env, the project record is unregistered entirely. To wipe a multi-env project completely, repeat the command for each env.
server:upgrade
Sync per-project Docker configuration files to a running server without a full deploy. Useful for applying infrastructure changes (e.g., new docker-compose.yml template, nginx config updates).
gon server:upgrade myapp # Sync config files onlygon server:upgrade myapp --restart # Sync + restart containersgon server:upgrade myapp --host=vps1 # Override saved host
What gets synced
docker-compose.yml— regenerated from latest gon-cli templatedocker/nginx-production.conf— latest nginx config (project's IN-CONTAINER nginx)
Note: gon deploy also syncs these files automatically before each deployment. For HOST-level Traefik config see server:upgrade-traefik.
server:upgrade-traefik
Re-render and redeploy the system-level Traefik docker-compose.yml on a server. Use this to pick up gon-cli template fixes that server:setup wouldn't apply on its own (setup skips Traefik when /opt/traefik/docker-compose.yml already exists, which is the right call for first-time idempotency but means template patches never reach existing servers without an explicit redeploy path).
gon server:upgrade-traefik vps1 # Re-render + force-recreate containergon server:upgrade-traefik vps1 --no-restart # Sync file only, don't recreategon server:upgrade-traefik vps1 --email=new@example.com # Override stored acme_email
What it does
- Backs up the existing
/opt/traefik/docker-compose.ymlto.bak(recoverable in seconds) - Renders the bundled
traefik/docker-compose.yml.twigwith the server's storedacme_email - Uploads the new compose under sudo (the dir is root-owned)
docker compose up -d --force-recreate— required so changed CLI flags take effect even when the image hash hasn't changed- Polls Traefik for up to 30s to confirm it reaches
Upstate, otherwise fails with a "check the logs" hint
When to run
After a gon-cli release that mentions Traefik changes (image bump, command flags, port pinning), or when gon server:status reports Traefik in a restart loop. Common symptoms that mean you need this:
- 502 Bad Gateway on every project on the host while project containers themselves report
healthy. Means host nginx can reach port 8080 but Traefik isn't listening — usually a Traefik startup crash you'd see indocker compose logs traefik. - 404 from Traefik on a Host header that should match an existing project. Traefik is up but its Docker provider can't see container labels, often because of a Docker SDK ↔ daemon API version mismatch (Docker 25+ rejects pre-v3.6 Traefik clients).
- "bind: address already in use" in Traefik logs — earlier templates exposed both an explicit
webentrypoint and the implicit dashboard one on the same port.
server:status
gon server:status # All serversgon server:status vps1 # Detailed status