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

  1. 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.com

    If you provisioned the server with gon infra:aws-ec2, this step has already run.

  2. Register the staging environment of your existing project:

    gon server:add-project myapp \
    --host=staging-1 \
    --domain=staging.myapp.com \
    --env=staging

    The first time you pass --env on a flat-shape project, gon-cli quietly migrates the existing record to the nested shape with both environments side by side. No edits to servers.json needed.

  3. 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 staging
gon release production # Explicit production
gon deploy staging --tag=v1.0.0
gon 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/.env
gon env:set DEBUG=false production # Sets on production host's /opt/myapp/.env
 
gon env:pull staging # Pulls staging's .env to .env.staging locally
gon 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-1
gon 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.

  1. 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-db

    Stops 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.

  2. Register the staging environment under the canonical project:

    gon server:add-project myapp \
    --host=staging-1 \
    --domain=staging.myapp.com \
    --env=staging

    The new host nginx vhost gets the existing cert wired in by certbot, and a fresh /opt/myapp directory 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.

  3. Verify the new staging deployment is healthy:

    gon release staging
    curl https://staging.myapp.com/api/v1/healthcheck
  4. (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).