Deploy to Multiple Environments
One project (one gon.json) can deploy to multiple environments — production on one server, staging on another, each with its own domain and database. This recipe covers the typical lifecycle: add a second environment, release to it, manage its env vars, and decommission it later.
Mental model
Inside ~/.gon/servers.json a project record carries an environments map. Each environment has its own host, domain, path, and database settings. The default_environment is the target when commands run without an explicit env argument.
{ "projects": { "myapp": { "default_environment": "production", "environments": { "production": { "host": "1.2.3.4", "domain": "myapp.com", ... }, "staging": { "host": "5.6.7.8", "domain": "staging.myapp.com", ... } } } }}
Projects created by older gon-cli versions have a flat shape (no environments map). They keep working as the implicit production environment and migrate transparently the first time you add another env.
Add a staging environment to an existing project
Make sure the staging server is set up (Docker, deploy user, MySQL, nginx, certbot — same as production):
gon server:setup 5.6.7.8 \--alias=staging-1 \--user=ubuntu \--email=ssl@myapp.comIf you provisioned the server with gon infra:aws-ec2, this step has already run.
Register the staging environment of your existing project:
gon server:add-project myapp \--host=staging-1 \--domain=staging.myapp.com \--env=stagingThe first time you pass
--envon a flat-shape project, gon-cli quietly migrates the existing record to the nested shape with both environments side by side. No edits toservers.jsonneeded.Verify the project now has both envs:
cat ~/.gon/servers.json | jq '.projects.myapp'You should see
environments.production+environments.staging.
Release to a specific environment
Both release and deploy take an optional environment argument:
gon release # Release to default_environment (usually production)gon release staging # Release to staginggon release production # Explicit productiongon deploy staging --tag=v1.0.0gon deploy --rollback staging
The host is resolved from the project's environments[env] entry, so the same image hits the right server every time — no manual --host juggling.
Manage env vars per environment
gon env:* commands operate on the .env file living on the deploy host of the targeted env:
gon env:set DEBUG=true staging # Sets on staging host's /opt/myapp/.envgon env:set DEBUG=false production # Sets on production host's /opt/myapp/.env gon env:pull staging # Pulls staging's .env to .env.staging locallygon env:push staging # Pushes .env.staging to staging host
Local cache files are named .env.<environment> so production and staging don't overwrite each other.
Set up DNS for the second environment
Once the staging host is provisioned, point a subdomain at it:
gon infra:dns staging.myapp.com --alias=staging-1gon infra:dns staging.myapp.com --alias=staging-1 --provider=cloudflare
Then re-run gon server:add-project myapp --env=staging --host=staging-1 --domain=staging.myapp.com if the SSL step failed the first time (the command is idempotent — DB, vhost, and Docker config skip when already in place).
Choose a different default environment
The default env is whichever was registered first. To switch, edit the default_environment field in ~/.gon/servers.json:
"projects": { "myapp": { "default_environment": "staging", // was "production" "environments": { ... } }}
This is intentionally manual — flipping production to staging by accident in CI is the kind of thing you only want to happen with eyes on the file.
Remove a single environment
To decommission staging without touching production:
gon server:remove-project myapp --env=staging
The command stops containers, drops the staging database, removes the nginx vhost + SSL cert on the staging host, and unsets only the environments.staging entry. Production stays intact.
Without --env, the command removes the project's default environment (typically production). If that was the last remaining environment, the project record is unregistered entirely. To wipe all environments of a multi-env project, run gon server:remove-project once per env.
Migrating from "two separate projects" to multi-env
If you previously worked around the lack of multi-env by registering the same code under two project names (e.g. myapp on production, myapp-staging on staging), the cleanest path is to retire the old project first, then register the new env. Doing it in this order avoids a vhost collision on the host, which gon-cli now refuses to leave you in.
Retire the old standalone project — keep the SSL cert + DB so the new env can adopt them:
gon server:remove-project myapp-staging \--host=staging-1 \--keep-ssl \--keep-dbStops the old containers, removes the host nginx vhost, removes
/opt/myapp-staging. The Let's Encrypt cert stays in place because the new vhost will reuse it; the old MySQL database stays so you can dump data first if needed.Register the staging environment under the canonical project:
gon server:add-project myapp \--host=staging-1 \--domain=staging.myapp.com \--env=stagingThe new host nginx vhost gets the existing cert wired in by certbot, and a fresh
/opt/myappdirectory is provisioned on the staging host. If you skipped step 1 by accident, the pre-flight will refuse with the exact remediation command — see vhost conflict pre-flight below.Verify the new staging deployment is healthy:
gon release stagingcurl https://staging.myapp.com/api/v1/healthcheck(Optional) Drop the orphaned old database once you're sure nothing depends on it. Connect to the staging host and run
sudo mysql -e "DROP DATABASE myapp_staging; DROP USER 'myapp_staging'@'localhost'".
Vhost conflict pre-flight
Two nginx vhosts on the same host with the same server_name make nginx pick the alphabetically-first one and silently ignore the rest. The hidden vhost may have older buffer config, missing SSL, or stale Traefik routing — typical symptom is mysterious 502s on the live site even though the new project's containers report healthy. gon server:add-project checks for this on the target host before any mutation runs, and refuses to proceed when it finds a collision:
$ gon server:add-project myapp --host=staging-1 --domain=app.example.com --env=staging Domain "app.example.com" is already served by another project's nginx vhost on staging-1: /etc/nginx/sites-enabled/gon-myapp-staging.conf To resolve, retire the old project first: gon server:remove-project myapp-staging --host=staging-1 --keep-ssl --keep-db Or pass --force-vhost to overwrite anyway (advanced — risk of orphaned SSL/DB).
Idempotent re-runs of the same project don't trigger the check (the current project's own gon-{name}.conf is excluded). The --force-vhost escape hatch is for advanced operators who deliberately want both vhosts to coexist (rare — typically only useful when manually migrating SSL between vhosts).