Skip to content

Liber Tertius · Chapter 4 · Shipping

Change config on a live app

A deployed app reads its .env once, when its containers are created, and caches the result. So every change here has two halves, the file and the restart, and the quickest tool is the one that never forgets the second half.

≈10 min 6 steps A deployed project Your key accepted for the deploy user
Source

Before you start

The project is deployed, as First deploy to a fresh VPS leaves it. The .env of a deployed app lives in the project's directory on the server and nowhere else. It is not in the image and not in the repository, so a release never changes it.

You are in the project directory, and the server knows your key.

$ ssh deploy@203.0.113.10 true

The env:* commands take the environment as their last argument, and without one they mean production. This page names it every time. Where the host comes from is the same for all three: deploy.environments.<environment> in grim.json, then deploy, then your own ~/.grim/servers.json.

The ritual, in order

Six steps, and you rarely need all of them. The first, the third and the fifth change something on a server. Each restart takes the site away for a few seconds.

  1. Change one value

    grim env:set replaces one line in the server's .env, or adds it, and recreates the containers that read the file. There is no confirmation and no flag for the restart. It always happens.

    $ grim env:set MAIL_HOST=smtp.example.com production
    Recreating PHP services (app, horizon, scheduler)...

    The summary names the project, the host and the key, and says whether the key was updated or added. The value is not printed. A project with Reverb gets that container recreated too, although the line does not mention it. For a password, prefer step three: what you type here stays in your shell history.

    Do not add -v to grim env:set or grim env:pull. Verbose mode echoes every SSH command's output, and one of those commands reads the file. The whole production .env scrolls through your terminal, and through any recording of it.

  2. Ask the app what it sees

    The file is one thing and the running app another. grim remote:artisan asks the app itself. Ask for the config key, not the variable: the app caches its configuration at start, and env() returns nothing after that.

    $ grim remote:artisan tinker --execute="echo config('mail.mailers.smtp.host');"
    smtp.example.com

    grim remote:artisan takes no environment. It reaches the one registered first on your machine, which is production in the usual case. For staging, connect yourself: ssh deploy@203.0.113.10, then cd /opt/shop-staging and docker compose exec app php artisan tinker.

  3. Change many values at once

    When several values change together, or a line has to go away, make the round trip. grim env:pull writes the server's file next to your own as .env.production. You edit it. grim env:push sends it back, and --restart makes the app read it.

    $ grim env:pull production
    $ $EDITOR .env.production
    $ grim env:push production --restart
    Push .env.production (42 variables) to 203.0.113.10:/opt/shop/.env?

    Read the target in the question before you answer y. The whole file is replaced, nothing is merged and the server keeps no backup. Pull right before you edit, never from a copy of last week, or a value a colleague set yesterday is gone. Without --restart the file is on the server and the app keeps its old values until the next release recreates the containers. That is a way to stage a change, and a way to confuse yourself.

  4. Throw the copy away

    .env.production holds the database password, the app key and the mail credentials in plain text. A project made by grim ignores the file in git. It still has no reason to stay on a laptop.

    $ rm .env.production
    $ git status --short

    git status should not list the file. When it does, your .gitignore is not the one grim wrote. Fix that before the next git add.

  5. Rotate a secret the whole team shares

    Some keys are the same in every project of the team, and the registry keeps them once. grim secrets:set replaces the value there. That is all it does. A shared secret reaches a project only when grim install writes it into the local .env, and it never reaches a server by itself. So rotating is three jobs: the registry, every deployed environment, and every checkout.

    $ grim secrets:set GEOIP_API_KEY=abc123
    $ grim secrets:list
    $ grim env:set GEOIP_API_KEY=abc123 production
    $ grim env:set GEOIP_API_KEY=abc123 staging
    $ grim install

    ✓ Secret GEOIP_API_KEY and the registry's status is the first answer. The list shows keys and descriptions, never values. grim install writes every shared secret over the same key in the local .env, so a value you set by hand under a shared name does not survive it. Install also builds and starts the local stack. When that is more than you want today, put the new value into your local .env by hand and go to step six.

  6. Apply an edit on a stack you run yourself

    Optional. On your own machine, and on a shared dev box, the .env is a file you edit directly. The rule about the restart is the same, and grim reload is the restart.

    $ grim reload

    It recreates every container of the stack and clears the config cache. It is not for a production server. There the restart belongs to grim env:set and grim env:push --restart.

What you have now

The server's .env carries the new values and the PHP containers were recreated on them: app, and horizon, scheduler and reverb where the project has them. nginx and Redis were left running.

Nothing in the repository changed, and nothing in the image. The next release keeps the file as it is. Another environment of the same project has its own .env in its own directory, and got only what you sent there by name.

A shared secret now has the same value in the registry, on the servers you named and in your checkout. Your colleagues get it the next time they run grim install.

When it does not work

“No host for "shop" (production): set deploy.environments.production.host in grim.json or run "grim server:add-project".”

Neither grim.json nor your servers.json knows a server for that environment. Write the host into grim.json. Then everyone who clones the project can use the env:* commands without registering anything.

“Invalid .env key: mail-host”

A key may hold letters, digits and underscores, and may not start with a digit. Nothing was changed on the server. Invalid format. Use: grim env:set KEY=VALUE means there was no = at all.

“.env.production not found.”

There is nothing to push. The pull in step three was skipped, or it ran for another environment. The argument picks the file and the server together, so grim env:push staging looks for .env.staging.

The value is on the server and the app still uses the old one

The push ran without --restart. Push again with it. Clearing the config cache by hand does not help: the container built its cache from the old file when it started, and only a new container reads the new one.

The push was “Aborted.” in a script

Without a terminal the question answers itself with no, and the command still exits successfully. A pipeline needs --force. grim secrets:delete behaves the same way and has no --force at all, so under --no-interaction it deletes nothing and says nothing.

“Failed to set secret: HTTP 403 Forbidden”

The registry knows you, and your account may not manage shared secrets. Ask whoever administers the registry. The servers can still be updated with grim env:set.

Questions

Can grim env:set remove a variable?

No. grim env:set KEY= sets it to an empty string, and the app then sees that instead of the default from its config file. To delete the line, make the round trip in step three.

How do I take a shared secret out of the registry?

grim secrets:delete GEOIP_API_KEY asks once and deletes it. Every .env that already holds the value keeps it, on laptops and on servers. When the credential must stop working, revoke it at the service that issued it.