How to Set Up a Development, Staging, and Production Workflow
Learn how to set up a three-tier development, staging, and production workflow to catch bugs early, deploy confidently, and avoid downtime. This guide covers Git branching, Docker Compose staging, CI/CD automation, blue-green deployments, and rollback strategies.
Advertisement
You’ve probably heard the advice: never push code directly to production. But if you’re working alone or on a small team, it’s tempting to skip the middle steps. I’ve been there. You fix a bug, push to production, and hope for the best. But that approach breaks things more often than not.
At PythonSkillset, we’ve learned that a proper three-tier workflow—development, staging, and production—saves you from embarrassing downtime and helps you ship better code. Here’s how to set it up without overcomplicating things.
Why Three Environments?
Think of it like cooking. You wouldn’t serve a dish straight from the chopping board. You prep in the kitchen (development), taste-test in the dining room (staging), and only then serve to guests (production). Each environment has a purpose:
- Development: Where you write and test code locally. Mistakes are fine here.
- Staging: A near-exact copy of production. You test integrations, data migrations, and performance here.
- Production: The live site your users see. Only stable, tested code goes here.
Step 1: Set Up Your Development Environment
Start with version control. Git is the standard. Create a repository and branch off for features. For example, at PythonSkillset, we use a main branch for production-ready code and develop for ongoing work.
git init
git checkout -b develop
Use virtual environments to isolate dependencies. For Python projects, venv or poetry works well.
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
Keep your development environment lightweight. You don’t need a full database server locally—use SQLite or a Docker container for PostgreSQL. The goal is to write and test code fast.
Step 2: Create a Staging Environment
Staging should mirror production as closely as possible. If your production runs on Ubuntu with PostgreSQL and Nginx, your staging should too. This catches environment-specific bugs before they hit users.
At PythonSkillset, we use Docker Compose for staging. It’s easy to spin up a replica of production on a separate server or even the same machine.
# docker-compose.staging.yml
version: '3'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/staging_db
db:
image: postgres:13
environment:
- POSTGRES_DB=staging_db
Deploy to staging automatically when you merge to a staging branch. Use a CI/CD tool like GitHub Actions or GitLab CI. Here’s a simple GitHub Actions workflow:
name: Deploy to Staging
on:
push:
branches: [ staging ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to staging server
run: |
ssh user@staging-server "cd /app && git pull && docker-compose -f docker-compose.staging.yml up -d --build"
Test everything in staging: database migrations, API endpoints, third-party integrations. If something breaks, it breaks here—not in front of your users.
Step 3: Production Deployment
Production is where the stakes are high. Never deploy directly from a feature branch. Instead, merge to main (or production) and trigger a deployment.
Use a deployment strategy that minimizes risk. Blue-green deployments work well: you have two identical production environments. You deploy to the inactive one, test it, then switch traffic over. If something goes wrong, you switch back instantly.
Here’s a simplified example using environment variables to toggle between blue and green:
# config.py
import os
ENV = os.getenv('DEPLOY_ENV', 'blue')
if ENV == 'blue':
DATABASE_URL = os.getenv('BLUE_DB_URL')
else:
DATABASE_URL = os.getenv('GREEN_DB_URL')
In your CI/CD pipeline, you can automate the switch:
deploy-production:
stage: deploy
script:
- echo "Deploying to green environment"
- ssh user@prod-server "DEPLOY_ENV=green docker-compose up -d"
- sleep 10 # Wait for health checks
- ssh user@prod-server "echo 'DEPLOY_ENV=green' > .env"
Step 4: Automate Testing Between Environments
The whole point of this workflow is catching bugs early. Set up automated tests that run in development and staging before anything reaches production.
For Python projects, use pytest and run it in your CI pipeline:
test:
script:
- pip install -r requirements-dev.txt
- pytest tests/
only:
- develop
- staging
Add integration tests that hit your staging API. If they pass, the deployment to production can proceed automatically. If they fail, the pipeline stops.
Step 5: Handle Database Migrations Carefully
Database changes are the most common cause of production issues. Never run migrations directly on production without testing them in staging first.
Use a migration tool like Alembic (for SQLAlchemy) or Django’s built-in migrations. In your deployment script, run migrations as a separate step:
# In staging
alembic upgrade head
# In production (after testing in staging)
alembic upgrade head
If a migration is risky (e.g., dropping a column), do it in stages. First, add the new column and update the code to use it. Then, in a later deployment, remove the old column.
Step 6: Use Feature Flags for Gradual Rollouts
Even with staging, some bugs only appear under real traffic. Feature flags let you turn new features on for a subset of users. If something breaks, you disable the flag without rolling back the entire deployment.
In Python, you can use a simple dictionary or a library like flask-featureflags:
import os
FEATURE_FLAGS = {
'new_checkout': os.getenv('FEATURE_NEW_CHECKOUT', 'false') == 'true'
}
if FEATURE_FLAGS['new_checkout']:
# Use new checkout logic
else:
# Use old checkout logic
Set the flag to true in staging for testing, then gradually enable it in production.
Step 7: Monitor and Rollback
Production will still have issues. That’s okay. What matters is how quickly you recover.
Set up monitoring with tools like Sentry for errors and Prometheus for performance. When something goes wrong, you need a rollback plan.
Keep your previous production build available. With Docker, you can tag images:
docker build -t myapp:latest .
docker tag myapp:latest myapp:v1.2.3
In your deployment script, keep the last two successful builds. If the new one fails, redeploy the previous tag.
Real-World Example: PythonSkillset’s Workflow
Here’s how we do it at PythonSkillset:
- Developer works on a feature branch off
develop. - Pull request triggers tests in CI. If they pass, the branch merges to
develop. - Automatic deployment to staging. We run integration tests against the staging API.
- Manual approval before merging to
main. A senior developer reviews the staging results. - Production deployment uses blue-green. We deploy to the inactive environment, run smoke tests, then switch traffic.
This workflow has caught countless issues—from a misconfigured database connection to a breaking change in a third-party API. It adds maybe 30 minutes to each deployment, but it saves hours of debugging later.
Common Pitfalls to Avoid
- Staging drift: If staging isn’t identical to production, you’ll miss bugs. Use the same OS, database version, and environment variables.
- Skipping staging for “small” changes: Every change goes through staging. Even a one-line fix can break something unexpected.
- Manual deployments: Automate everything. Manual steps are error-prone and slow.
Final Thoughts
Setting up a development, staging, and production workflow isn’t just about preventing downtime. It gives you confidence to ship changes quickly. When you know staging caught the bugs, you can deploy to production without holding your breath.
Start small. Even a basic staging server with a CI pipeline is better than nothing. As your project grows, add more layers—feature flags, blue-green deployments, automated rollbacks. The goal is to make deployment boring. Boring is good. Boring means your code works.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.