Learning objectives
After reading this article you will be able to:
- Explain what each environment is for and when staging is worth having
- Separate config, secrets and databases per environment
- Promote the same code from development to staging to production
On this page
- What are development, staging and production?
- Why is staging worth the extra work?
- What must differ between environments?
- How do you set up the environments, step by step?
- How do you automate the promotion with GitHub Actions?
- How do you handle database changes across environments?
- What are the most common mistakes?
- How to do this on Square Cloud
- FAQs
Every app starts the same way: you run it on your laptop, it works, you upload it, and real users see it. The trouble starts when "it works on my machine" meets a production database, a different API key or a payment provider that charges real cards.
Separate environments solve that. You run the same code in more than one place, and each place has its own configuration and data. A mistake in one cannot reach the others.
What are development, staging and production?#
An environment is one running copy of your app together with everything it depends on: config, secrets, database and storage.
| Environment | Who uses it | Data | Purpose |
|---|---|---|---|
| Development | You, on your machine | Fake, disposable | Write and debug code quickly |
| Staging | You and your team, sometimes a client | Fake or anonymized | Final check of a build that looks like production |
| Production | Real users | Real | Serve the live app |
Some teams add a preview environment per pull request, or call staging "QA" or "pre-production". The names do not matter. What matters is that each one is isolated.
Why is staging worth the extra work?#
Local development hides problems that only appear on a server: a missing environment variable, a different runtime version, a firewall rule, a database that is larger than your test data. The Twelve-Factor App calls these differences the dev/prod parity problem and names three gaps: the time between writing and shipping code, the people who write it versus the people who deploy it, and the tools used in each place.
Staging closes the tools gap. It runs on a server, with the same runtime and the same kind of database as production, so a failure shows up before users see it. It is also the right place to test things you should never test on real data: database migrations, payment webhooks in a provider's test mode, and bulk imports.
You do not always need it. A solo hobby bot with no paying users can skip staging, as long as you can roll back quickly. Add it when a bad deploy costs money or trust.
What must differ between environments?#
Three things change from one environment to the next, and nothing else should.
1. Configuration. The Twelve-Factor App stores config in the environment, not in the code. Anything that varies between deploys is config: database URLs, API keys, the public URL, log level. Your code reads all of it from environment variables, as explained in what environment variables are.
2. Secrets. Staging should use different tokens and keys than production. If a staging key leaks, or a teammate pastes it into a chat, production stays safe. For payment providers and bot platforms, use the test mode or a separate test bot, never the live credentials.
3. Data. Never point staging at the production database. A test script with a bug would edit real rows. Create a separate database, with the same engine and version as production. Fill it with fake data, or with a copy of production that has names, emails and tokens removed or scrambled.
Everything else, from the runtime version to the dependency list, should match. The code is the same commit in every environment.
How do you set up the environments, step by step?#
1. Pick one variable that names the environment. For example APP_ENV with the values development, staging and production. Use it for logs and a few switches, not for big behavior changes. The more your code branches on the environment name, the less staging proves.
2. Load config in one place. This Node.js module reads variables once, fails fast when one is missing and keeps the rest of the code free of process.env:
// config.js
const required = (name) => {
const value = process.env[name];
if (!value) throw new Error(`Missing environment variable: ${name}`);
return value;
};
export const config = {
env: process.env.APP_ENV ?? "development",
databaseUrl: required("DATABASE_URL"),
apiKey: required("API_KEY"),
logLevel: process.env.LOG_LEVEL ?? "info",
};Locally, put the values in a .env file that is not committed (see the .env file). On a server, set them in the platform's environment settings. Recent Node.js versions can load the file without a library: node --env-file=.env config.js.
3. Create a separate app and database per environment. Staging gets its own app, its own database and its own domain or subdomain. Do not reuse the production app with a different flag.
4. Map branches to environments. A common flow is: feature branches are tested locally, a push to develop or staging deploys to staging, and a merge to main deploys to production. Keep branches short-lived so staging does not drift from main.
5. Promote the same code. What you tested on staging should be the exact commit that goes to production. If you rebuild from different sources, or merge extra changes after testing, the staging result no longer proves anything.
How do you automate the promotion with GitHub Actions?#
GitHub Actions has a feature built for this: environments. An environment is a named target, such as staging or production. Each one can hold its own secrets and variables, and a job that declares environment: production reads that environment's values. For production you can also require a reviewer to approve the run, add a wait timer or restrict which branches may deploy.
Environment secrets only reach jobs that reference the environment. There are also plan limits: for private repositories, environments and rules such as required reviewers and wait timers depend on your GitHub plan, so check the environments documentation for your case.
This workflow deploys staging on every push to develop and production on every push to main, using the same deploy script with different values:
name: Deploy
on:
push:
branches: [develop, main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: lts/*
- run: npm ci
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
environment:
name: ${{ github.ref_name == 'main' && 'production' || 'staging' }}
steps:
- uses: actions/checkout@v7
- name: Deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
TARGET_APP: ${{ vars.TARGET_APP }}
run: ./scripts/deploy.shCreate two environments in Settings > Environments, and define DEPLOY_TOKEN and TARGET_APP in each with different values. The workflow file stays identical. Where your plan allows it, turn on required reviewers for production so a human confirms the release. For the full workflow basics, see automatic deploys with GitHub Actions.
How do you handle database changes across environments?#
Schema changes are the riskiest part of promotion. Write them as migration files in the repository, run them on staging first, and only then on production. If a migration fails or takes too long on staging, you found out for free. Database migrations on deploy covers ordering and safe patterns.
Staging data is rarely as large as production. A migration that takes a second on a few hundred rows can lock a table for minutes on millions. If that risk matters, test against an anonymized copy of production data.
What are the most common mistakes?#
- Staging pointed at production resources. One copied
.envfile is enough to make staging write to the live database or send real emails. Name variables clearly and review them when you create an environment. - Sharing secrets across environments. A leaked staging key should not unlock production.
- Staging that drifts. If it runs an old branch, a different runtime version or a different database engine version, passing there proves little. Update it on every merge.
- Testing with code that differs from production. Do not add
if (staging)shortcuts that skip auth or mock services you use in production. - Real notifications from staging. Staging bots that join real servers, or apps that email real customers, cause confusion. Use test accounts, a test bot token and a mail sandbox.
- Skipping the check after deploy. A green pipeline only means the command finished. Open the app and read the logs after each release.
How to do this on Square Cloud#
On Square Cloud, each environment is its own application with its own environment variables, so staging and production never share config by accident. Create one application for production and a second for staging. Variables are set per application in the dashboard, the API, the CLI or the VS Code extension; the dashboard can import a .env file, and a change applies on the next restart, with no rebuild. The SQUARECLOUD_APP_ID variable gives the app its own ID at runtime, which is handy for logs.
For data, database hosting starts on the Standard plan; from there, create a separate staging database and give each app its own connection string. Web apps get a *.squareweb.app subdomain on every plan, which is enough for a staging URL; a custom domain for production starts on the Standard plan. The plan's RAM is a pool shared by all your applications, with a minimum of 256 MB per background app and 512 MB per web app, so a staging copy draws from the same pool as production.
To automate promotion, use the official GitHub Action with the environments pattern above. Save SQUARECLOUD_API_KEY as a secret and the app ID as the variable SQUARECLOUD_APP_ID in each GitHub environment:
- uses: actions/checkout@v7
- uses: squarecloudofc/github-action@v2
with:
token: ${{ secrets.SQUARECLOUD_API_KEY }}
command: commit ${{ vars.SQUARECLOUD_APP_ID }} --restartThe staging environment holds the staging app's ID, and the production environment holds the production one. Before a risky release, take a manual snapshot of the production app from the dashboard; manual snapshots are available on every plan, and one restores into the same app or, in one click, into a new application. The squarecloud.app configuration file is optional: the dashboard generates it on deploy, and writing it at the project root is the alternative. The GitHub Action page shows the full workflow and the action inputs.
Deploy
Push from GitHub, the CLI or a ZIP and go live in seconds
FAQs
Do I need a staging environment for a small project?
Not always. If you are the only developer and a bad deploy costs little, a good local setup plus a quick rollback may be enough. Add staging when mistakes become expensive: paying users, shared databases, payments or a team.
Can staging share the production database?
No. A test run, a bad migration or a script with a bug would change real user data. Give staging its own database, filled with fake or anonymized data.
Should staging use the same code as production?
Yes. The point of staging is to test the exact code that will go to production. Only the configuration changes, such as the database URL, API keys and the environment name.
How many environments do I need?
Two is the common minimum: your machine for development and production. A third, staging, sits between them. Large teams add more, such as a preview environment per pull request.
More in Deploy
See all 33 articles- What are environment variables?Environment variables are key-value pairs the operating system hands to a process at startup. Learn to read them in Node.js and Python and keep tokens out of code.
- What is a deployment?A deployment takes a version of your code to a server, prepares it and starts it. See the stages, the methods and how to roll back when it goes wrong.
- How much RAM and CPU does my app need?Size RAM and CPU by measuring your app's real peak and adding about 30% headroom. Learn how to measure in Node.js, Python and Java and what to do if short.
- Automatic deploys with GitHub ActionsAutomatic deploys with GitHub Actions ship your code to the server on every push. See the workflow step by step, secrets, tests before deploy and common errors.