Check a deployment
deploy:check visits a deployment the way a customer does and tells you what is wrong with
it. It works on any host, and on bun run production:up.
bun run deploy:check https://app.example.com --site https://example.com --email you@example.comBefore you deploy
Section titled “Before you deploy”bun run deploy:check --before railwaybun run deploy:check --before awsIt reads .env.railway or .env.aws and holds it to the rules the API checks when it starts,
so a wrong value stops you here instead of a new version that never starts.
deploy:apply and deploy:apply:aws run it first and stop on a FAIL. A setting left empty
is only a note: it may already be set on the host. A name nothing reads is a note too,
since it is usually a typo.
What it looks at
Section titled “What it looks at”| Check | It passes when |
|---|---|
| The API answers and reaches the database | /api/health says ok |
| Requests are given an id | The answer has an x-request-id header. See Logs |
| The API says how people sign in | /api/config answers. It prints “by email” or the providers that are on |
| Strangers are turned away | /api/me answers 401 without a session |
| The app loads | / is a page with its script |
| Pages come with protective headers | Strict-Transport-Security, X-Content-Type-Options and X-Frame-Options are set |
| Browsers may keep its files | The script is served as immutable |
A missing file is a 404 |
/assets/not-there.js is not answered with the app’s page |
| A link into the app loads on its own | /sign-in is a page, not a 404 |
| Billing refuses an unsigned webhook | An unsigned webhook gets 400 |
| It is served over HTTPS | The address is https://, and http:// is sent on to it |
| Stand-ins are off | STAND_INS is not set: Stripe and the model are the real ones |
With --site, also:
| Check | It passes when |
|---|---|
| The site loads | / answers 200 |
| A page that does not exist says so | /not-there answers 404 |
| The sitemap has the site’s address | Otherwise SITE_URL was wrong when the image was built |
| The contact form reaches the API | An empty message is refused by the API, through the site |
| The site is served over HTTPS | As above |
With --email, it asks the app to send a sign-in link to that address. That is the one thing
it changes: everything else is a request that is read or refused. Check that the email arrives.
Read the output
Section titled “Read the output”Checking https://app.example.com
ok The API answers and reaches the database ok Requests are given an id ok The API tells the app how people sign in: with google and github note Billing is off: no payment provider key is set FAIL It is served over HTTPS: http://app.example.com answered 200 instead of sending people on
9 passed, 1 failedFAIL names what is wrong. note is something to know that may be what you want: billing or
the contact form is off, demo mode is on, or HTTPS was not checked because the address is your
own machine.
The checks run at the same time, and one that cannot be made at all, such as an address that
does not resolve, is a FAIL with the reason. It exits with 1 if anything failed and 2 if
the arguments are wrong, so you can run it after every release in a script.
Add a check of your own
Section titled “Add a check of your own”When production has to guarantee something new, add it to packages/deploy/src/check.ts,
with a test beside it in check.test.ts. CI runs the same command against the
production images on every pull request, so the check also guards your next change.