
環境拆分:你不會在高速公路上練習開車
軟體開發最怕的事情之一,就是「在正式環境上測試」。改了一行程式碼、推上去、使用者看到 500 error、緊急 rollback——這種場景只要經歷過一次就永生難忘。環境拆分的核心目的就是把「開發與實驗」和「真正面對使用者的服務」隔離開來。Dev 環境讓開發者自由實驗,Staging 環境模擬正式環境做最後驗證,Production 環境只跑已經驗證過的穩定版本。聽起來理所當然,但實務上很多團隊的環境管理一團亂:Dev 連到 Prod 資料庫、Staging 的配置跟 Prod 完全不同、部署到哪個環境全看個人心情。這篇文章定義每個環境的用途、設計原則、資源隔離策略,以及如何透過 CI/CD 自動化環境部署。
架構概覽
flowchart LR Dev[Dev 環境\n開發測試] -->|Code Review\nMerge| Staging[Staging 環境\n預上線驗證] Staging -->|QA 通過\nTag 發版| Prod[Production 環境\n正式上線] Dev ---|獨立 DB & Config| DevR[(Dev 資源)] Staging ---|獨立 DB & Config| StagR[(Staging 資源)] Prod ---|獨立 DB & Config| ProdR[(Prod 資源)]
架構概覽
flowchart LR Dev[Developer] -->|git push\ndevelop branch| GitLab[GitLab Repo] GitLab -->|auto deploy| DevEnv[Dev Environment\n開發測試\n頻繁部署] Dev -->|merge to release| GitLab GitLab -->|auto deploy| StagingEnv[Staging Environment\n預上線驗證\nQA 測試] Dev -->|tag v*| GitLab GitLab -->|manual trigger| ProdEnv[Production Environment\n正式環境\n真實使用者] subgraph "環境隔離" DevEnv -->|獨立 DB / Config| DevRes[(Dev Resources\ndev-db / dev-redis)] StagingEnv -->|獨立 DB / Config| StagingRes[(Staging Resources\nstaging-db / staging-redis)] ProdEnv -->|獨立 DB / Config| ProdRes[(Prod Resources\nprod-db / prod-redis)] end
程式碼從 develop 分支自動部署到 Dev 環境,merge 到 release 分支後自動部署到 Staging,打 tag 後經人工確認部署到 Production。每個環境擁有獨立的資料庫、Redis、設定檔和 secrets,絕對不共用。
核心概念
-
為什麼需要多環境拆分:多環境存在的根本原因是「風險隔離」。開發過程中會有未完成的功能、未修好的 bug、未驗證的效能瓶頸,這些如果直接暴露給使用者,後果從「使用者體驗變差」到「資料損毀」都有可能。具體來說:(1)風險隔離——開發者在 Dev 環境炸掉資料庫不會影響任何真實使用者。(2)測試規模——有些 bug 只在接近 Production 配置的環境才會出現,Staging 環境就是為了抓這種 bug。(3)合規需求——金融、醫療等行業法規要求正式環境的變更必須經過審核流程,不能隨便部署。(4)協作效率——前端、後端、QA 同時在不同的功能上工作,如果只有一個環境,互相干擾的機率非常高。
-
Dev 環境(Development):Dev 環境的定位是「開發者的實驗場」。用途:日常功能開發、整合測試、新技術 POC(Proof of Concept)。特徵:(1)不穩定——隨時可能掛掉,因為開發者在嘗試各種東西。(2)頻繁部署——每天可能部署數十次,每次 push 到 develop 分支都自動部署。(3)資源精簡——不需要和 Production 一樣的 CPU/RAM 配置,省成本。(4)資料可拋棄——資料庫裡的資料是測試資料,隨時可以清空重建。誰在用:開發者、CI pipeline 的自動化測試。注意事項:Dev 環境的 log level 通常設為
debug,方便除錯,但正式環境不能用 debug level(效能差、資訊過多)。 -
Staging 環境(Pre-production):Staging 環境的定位是「正式環境的鏡像」。用途:上線前的最後驗證、QA 團隊的功能測試、客戶 demo、效能測試。特徵:(1)盡量和 Production 配置一致——同樣的 Nginx 設定、同樣的環境變數結構、同樣的資料庫版本。(2)穩定性要求比 Dev 高——不能隨便有人 push 就部署,需要經過 Code Review 和 merge 才能到 Staging。(3)資料接近真實——用脫敏(anonymized)的 Production 資料,或結構一致的種子資料。(4)有限的存取權限——不是所有開發者都能直接操作 Staging 環境。誰在用:QA 團隊、產品經理、外部客戶(demo 用途)。
-
Production 環境(正式環境):Production 環境的定位是「面對真實使用者的環境」。用途:提供服務給真實使用者、處理真實資料、產生營收。特徵:(1)高可用——服務不能隨便掛,需要 監控、告警、自動重啟。(2)嚴格的變更管控——所有部署必須經過 CI/CD pipeline,不允許手動 SSH 上去改東西。(3)完整的 日誌 和 監控——出問題時必須能追蹤到原因。(4)資料保護——定期備份、災難復原計畫、權限最小化原則。誰在用:終端使用者、客戶。誰能操作:只有少數有權限的工程師,透過 CI/CD 或 Portainer 進行變更。
-
UAT / QA 環境(視團隊需求):有些團隊會額外建立 UAT(User Acceptance Testing)或 QA 環境。UAT 環境讓業務端(非技術人員)在上線前做最終驗收,確認功能符合需求規格。QA 環境則是 QA 團隊跑自動化測試的專用環境。是否需要獨立的 UAT / QA 環境取決於團隊規模和流程複雜度。小團隊 Staging 兼做 UAT 就夠了;大團隊或需要嚴格測試流程的組織,獨立環境可以避免 QA 和開發者互相干擾。
-
環境一致性原則(Environment Parity):Twelve-Factor App 強調「儘可能讓開發、預發佈、正式環境保持一致」。環境之間的差異越大,Staging 測試通過但 Production 出問題的機率就越高。需要保持一致的項目:(1)作業系統和基底映像檔版本。(2)資料庫、Redis、Message Queue 等中介軟體版本。(3)Nginx / Reverse Proxy 設定。(4)環境變數的結構(key 一致,value 可以不同)。允許不同的項目:(1)資源規格(CPU / RAM / Disk)——Dev 可以用較小的規格。(2)資料內容——Staging 用脫敏資料,不用真實使用者資料。(3)外部服務——Dev 和 Staging 用 Sandbox API,Production 用正式 API。
使用情境
-
新功能開發:開發者在本地完成功能,push 到
develop分支後自動部署到 Dev 環境。前後端工程師可以在 Dev 環境做整合測試。確認功能完成後,建立 MR(Merge Request)merge 到release分支,自動部署到 Staging。QA 團隊在 Staging 上驗證,確認沒問題後打 tag,手動觸發 Production 部署。 -
hotfix 緊急修復:Production 發現嚴重 bug,從
main分支切出hotfix/xxx分支修復。修復完先部署到 Dev 驗證修復有效,再快速推到 Staging 做 regression test(確認修復沒有弄壞其他功能),最後部署到 Production。整個流程可能在一小時內完成,但每個環境的驗證步驟不能省。 -
效能壓測:上線前需要確認新功能不會拖垮系統效能。在 Staging 環境用接近 Production 的配置和資料量跑壓力測試。如果在 Dev 環境壓測,因為資源規格差異太大,結果可能沒有參考價值。
實作範例 / 設定範例
Docker Compose 環境拆分(以覆蓋檔實現)
# docker-compose.yml - 基礎設定(所有環境共用)
services:
api:
image: registry.example.com/myapp/api:${IMAGE_TAG:-latest}
restart: unless-stopped
env_file:
- .env
networks:
- app-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
db:
image: postgres:16-alpine
restart: unless-stopped
env_file:
- .env
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-network
redis:
image: redis:7-alpine
restart: unless-stopped
networks:
- app-network
networks:
app-network:
volumes:
db-data:# docker-compose.dev.yml - Dev 環境覆蓋設定
services:
api:
environment:
- APP_ENV=development
- LOG_LEVEL=debug
- DEBUG=true
ports:
- "8000:8000" # Dev 直接開放 port,方便除錯
- "9229:9229" # Node.js debugger port
db:
environment:
- POSTGRES_DB=myapp_dev
- POSTGRES_USER=dev
- POSTGRES_PASSWORD=dev_password_123
ports:
- "5432:5432" # Dev 環境開放 DB port,方便本地連線
redis:
ports:
- "6379:6379" # Dev 環境開放 Redis port
# 啟動方式:docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d# docker-compose.staging.yml - Staging 環境覆蓋設定
services:
api:
environment:
- APP_ENV=staging
- LOG_LEVEL=info
- DEBUG=false
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
# Staging 不直接開放 port,透過 Nginx 反向代理
db:
environment:
- POSTGRES_DB=myapp_staging
deploy:
resources:
limits:
cpus: "1.0"
memory: 2G
# Staging 不開放 DB port,只有應用服務可以連
# 啟動方式:docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d# docker-compose.prod.yml - Production 環境覆蓋設定
services:
api:
environment:
- APP_ENV=production
- LOG_LEVEL=warn
- DEBUG=false
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
db:
environment:
- POSTGRES_DB=myapp_prod
deploy:
resources:
limits:
cpus: "2.0"
memory: 4G
volumes:
- db-data:/var/lib/postgresql/data
- ./backups:/backups # Production 需要備份掛載
# 啟動方式:docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dGitLab CI/CD Pipeline:分支對應環境
# .gitlab-ci.yml - 環境分支策略
variables:
SERVICE_NAME: "myapp-api"
REGISTRY: "registry.example.com"
IMAGE: "${REGISTRY}/${SERVICE_NAME}"
stages:
- lint
- test
- build
- deploy
# ===== Build =====
build:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $REGISTRY
script:
- docker build -t ${IMAGE}:${CI_COMMIT_SHA} .
- docker push ${IMAGE}:${CI_COMMIT_SHA}
rules:
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_BRANCH =~ /^release\//
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
# ===== Deploy to Dev(develop 分支自動部署)=====
deploy_dev:
stage: deploy
environment:
name: dev
url: https://dev.example.com
variables:
DEPLOY_HOST: $DEV_HOST
COMPOSE_OVERRIDE: docker-compose.dev.yml
ENV_FILE: .env.dev
script:
- ./scripts/deploy.sh dev ${CI_COMMIT_SHA}
rules:
- if: $CI_COMMIT_BRANCH == "develop"
# ===== Deploy to Staging(release 分支自動部署)=====
deploy_staging:
stage: deploy
environment:
name: staging
url: https://staging.example.com
variables:
DEPLOY_HOST: $STAGING_HOST
COMPOSE_OVERRIDE: docker-compose.staging.yml
ENV_FILE: .env.staging
script:
- ./scripts/deploy.sh staging ${CI_COMMIT_SHA}
rules:
- if: $CI_COMMIT_BRANCH =~ /^release\//
# ===== Deploy to Production(tag 觸發,需人工確認)=====
deploy_prod:
stage: deploy
environment:
name: production
url: https://example.com
variables:
DEPLOY_HOST: $PROD_HOST
COMPOSE_OVERRIDE: docker-compose.prod.yml
ENV_FILE: .env.prod
script:
- ./scripts/deploy.sh prod ${CI_COMMIT_TAG}
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
when: manual環境變數管理(.env 分檔策略)
# .env.dev - Dev 環境設定
APP_ENV=development
APP_PORT=8000
LOG_LEVEL=debug
DB_HOST=db
DB_PORT=5432
DB_NAME=myapp_dev
DB_USER=dev
DB_PASSWORD=dev_password_123
REDIS_URL=redis://redis:6379/0
# 外部服務用 Sandbox
PAYMENT_API_URL=https://sandbox.payment.com/api
PAYMENT_API_KEY=sk_test_xxxxx
# 寬鬆的限制
RATE_LIMIT=1000
SESSION_TIMEOUT=86400# .env.staging - Staging 環境設定
APP_ENV=staging
APP_PORT=8000
LOG_LEVEL=info
DB_HOST=staging-db.internal
DB_PORT=5432
DB_NAME=myapp_staging
DB_USER=staging_app
DB_PASSWORD= # 從 CI/CD Variables 注入
REDIS_URL=redis://staging-redis.internal:6379/0
# 外部服務用 Sandbox
PAYMENT_API_URL=https://sandbox.payment.com/api
PAYMENT_API_KEY= # 從 CI/CD Variables 注入
RATE_LIMIT=100
SESSION_TIMEOUT=3600# .env.prod - Production 環境設定
APP_ENV=production
APP_PORT=8000
LOG_LEVEL=warn
DB_HOST=prod-db.internal
DB_PORT=5432
DB_NAME=myapp_prod
DB_USER=prod_app
DB_PASSWORD= # 從 CI/CD Variables 注入,絕對不寫在檔案裡
REDIS_URL=redis://prod-redis.internal:6379/0
# 外部服務用正式環境
PAYMENT_API_URL=https://api.payment.com/api
PAYMENT_API_KEY= # 從 CI/CD Variables 注入
RATE_LIMIT=50
SESSION_TIMEOUT=1800Nginx 多環境反向代理設定
# /etc/nginx/conf.d/dev.conf - Dev 環境(寬鬆設定)
server {
listen 443 ssl http2;
server_name dev.example.com;
ssl_certificate /etc/letsencrypt/live/dev.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dev.example.com/privkey.pem;
# Dev 環境允許較大的 body(上傳測試用)
client_max_body_size 50M;
# Dev 環境加上 CORS header,方便本地前端開發
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
# 不快取(Dev 環境需要看到最新的變更)
add_header Cache-Control "no-store, no-cache, must-revalidate";
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# /etc/nginx/conf.d/prod.conf - Production 環境(嚴格設定)
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Production 限制 body size
client_max_body_size 10M;
# Production 嚴格的 CORS(只允許自己的 domain)
add_header Access-Control-Allow-Origin "https://example.com" always;
# 安全 header
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 靜態資源快取
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
proxy_pass http://127.0.0.1:8000;
expires 30d;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Rate limiting
limit_req zone=api burst=20 nodelay;
}
}常見問題與風險
-
Staging 和 Production 配置差異導致上線才發現問題:這是最常見也最痛的問題。Staging 用 PostgreSQL 15 但 Production 是 PostgreSQL 16、Staging 的 Nginx 沒有設 rate limit 但 Production 有、Staging 的 Redis 是單機但 Production 是 Cluster——這些差異都可能導致「Staging 測過但 Production 爆炸」。為什麼會發生:Staging 環境往往建立較早,後來 Production 升級了中介軟體版本但沒有同步更新 Staging。或者 Production 加了安全設定但忘了加到 Staging。避免方式:(1)用 Infrastructure as Code(docker-compose / Ansible)管理所有環境的配置,用版本控制追蹤差異。(2)定期做環境配置比對(diff),確認 Staging 和 Production 的中介軟體版本、Nginx 設定、環境變數結構一致。(3)把「更新 Staging 配置」加到 Production 變更的 checklist 裡。
-
Dev 環境資源不足影響開發效率:Dev 環境通常為了省成本而配置較少的 CPU 和 RAM。但如果資源差異太大,開發者在 Dev 上跑得很慢、timeout 頻繁,會嚴重影響開發效率和士氣。為什麼會發生:預算有限,Dev 環境被視為「不重要」而分配最少資源。或者多個開發者共用同一台 Dev 機器,資源被瓜分。避免方式:(1)Dev 環境的 CPU/RAM 至少是 Production 的 1/4,不能太差。(2)考慮讓開發者在本地跑完整的 docker-compose stack,減少對共用 Dev 環境的依賴。(3)如果資源真的有限,用 Serverless 或 cloud 的按需計費方案做開發環境。
-
環境之間的資料不一致:Staging 的資料庫是半年前從 Production 複製的,schema 已經跑過好幾次 migration 但資料量和分佈完全不同。結果 Staging 上跑很快的 SQL query,到 Production 因為資料量大 100 倍而 timeout。為什麼會發生:沒有定期同步 Staging 的資料,或者同步時沒有做資料脫敏。避免方式:(1)建立定期的資料同步排程,從 Production dump 資料、脫敏後 restore 到 Staging。(2)至少確保 Staging 的資料量級和 Production 相當(百萬筆 vs 百萬筆,而不是 100 筆 vs 百萬筆)。(3)效能相關的測試,用接近 Production 資料量的資料集。
-
權限管控不當(Dev 環境連到 Production 資料庫):開發者為了方便,在 Dev 的
.env裡填了 Production 資料庫的連線字串。結果 Dev 環境跑 migration 的時候把 Production 的 table 砍了、或跑了一個 DELETE 沒加 WHERE 條件。為什麼會發生:Production 的資料庫 credential 沒有被嚴格管控,開發者為了「測試真實資料」而自行連線。避免方式:(1)Production 資料庫只允許 Production 網段的 IP 連入(防火牆規則)。(2)Production 的 secrets 只有少數人知道,並且不給 Dev 環境使用。(3)如果開發者需要真實資料,提供脫敏後的 Staging 資料庫,永遠不直接連 Production。(4)在 CI/CD Variables 用 Protected Variables,確保 Prod 的 credential 只在 protected branch 上可用。 -
環境數量過多導致維護成本暴增:有些團隊為了不同需求(Dev、QA、UAT、Staging、Pre-prod、Production)建了六七個環境,每個環境都要維護獨立的配置、資料庫、中介軟體。結果是環境越多越難保持一致,反而增加了出問題的機率。為什麼會發生:每次有新的測試需求就建一個新環境,沒有定期清理。避免方式:(1)大多數團隊 Dev + Staging + Production 三個環境就夠了。(2)如果需要臨時的測試環境,用 Docker Compose 快速建立、用完就刪,不要變成永久環境。(3)定期盤點環境,問「這個環境最後一次被使用是什麼時候?」超過一個月沒人用就考慮關掉。
環境管理比較表
| 項目 | Dev | Staging | Production |
|------|-----|---------|------------|
| 部署觸發 | push to develop | merge to release | tag + manual |
| 部署頻率 | 每天數次 | 每週數次 | 每週/每兩週一次 |
| 穩定性要求 | 低 | 中 | 高 |
| 資源規格 | 最小化 | 接近 Production | 依需求配置 |
| 資料 | 測試資料 | 脫敏資料 | 真實資料 |
| 存取權限 | 所有開發者 | QA + 資深工程師 | 限定人員 |
| 監控告警 | 基本 | 完整 | 完整 + On-call |
| Log Level | debug | info | warn |
| 外部服務 | Sandbox | Sandbox | Production |
| 備份 | 不需要 | 建議 | 必要 |優點
- 風險隔離:開發和實驗不影響真實使用者
- 品質保障:每個變更經過多環境驗證後才上線
- 合規支持:變更審核流程有跡可循
- 協作效率:不同角色(開發、QA、PM)各有適合的環境
缺點 / 限制
- 維護成本:每個環境都需要獨立的資源和配置管理
- 環境差異:如果沒有刻意維護一致性,環境差異會隨時間擴大
- 資料同步:保持 Staging 資料接近 Production 需要額外的流程和工具
- 成本:多環境意味著多倍的基礎設施費用(雖然非 Production 環境可以用較小規格)