Manual deployment becomes a bottleneck quickly. A one-click deploy from GitHub flow removes repetitive steps, reduces production risk, and gives small teams strong release discipline. According to the 2019 Accelerate State of DevOps report, teams with fully automated deployment pipelines deploy 208x more frequently with 106x faster lead times than teams relying on manual deployment processes.
---
Why One-Click Deployment Matters
A healthy CI/CD workflow means:
- Every deployment follows the same tested path, regardless of who triggers it
- Production changes are traceable to specific commits and pull requests
- Rollbacks are fast and reliable when incidents happen
- Teams spend more time on product development and less on release anxiety
Architecture Overview
Developer pushes code
|
[GitHub Repository]
|
[GitHub Actions CI]
|-- lint, test, security scan
|-- build Docker image
|-- push to container registry
|
[Deploy to Staging]
|-- pull image
|-- run smoke tests
|
[Manual Approval Gate]
|
[Deploy to Production]
|-- rolling update
|-- post-deploy verification
|-- auto-rollback on failure
Complete GitHub Actions Pipeline
CI Stage: Build, Test, Artifact
<h1>.github/workflows/deploy.yaml</h1>
name: Build and Deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
skip_staging:
description: Skip staging deploy (emergency only)
type: boolean
default: false
env:
REGISTRY: ghcr.io
IMAGE: ${{ github.repository }}
jobs:
ci:
name: Build and Test
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run tests
run: npm test -- --coverage --ci
- name: Security audit
run: npm audit --audit-level=high
- name: Generate image tag
id: tag
run: echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT
- name: Build Docker image
run: |
docker build \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.tag.outputs.tag }} \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
.
- name: Push to registry
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.tag.outputs.tag }}
CD Stage: Staging Deploy
deploy-staging:
name: Deploy to Staging
needs: ci
if: ${{ !inputs.skip_staging }}
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.STAGING_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
# Pull new image
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ needs.ci.outputs.image_tag }}
# Stop old container gracefully
docker stop app-staging --time 30 || true
docker rm app-staging || true
# Start new container
docker run -d \
--name app-staging \
--restart unless-stopped \
--env-file /opt/app/staging.env \
-p 8080:8000 \
${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ needs.ci.outputs.image_tag }}
# Wait for health check
for i in $(seq 1 30); do
if curl -sf http://localhost:8080/health; then
echo "Health check passed"
exit 0
fi
sleep 2
done
echo "Health check failed"
exit 1
- name: Run smoke tests
run: |
npm run test:smoke -- --base-url=${{ secrets.STAGING_URL }}
CD Stage: Production Deploy with Rollback
deploy-production:
name: Deploy to Production
needs: [ci, deploy-staging]
runs-on: ubuntu-latest
environment:
name: production
steps:
- name: Save current version for rollback
id: current
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: docker inspect app --format "{{.Config.Image}}" 2>/dev/null || echo "none"
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ needs.ci.outputs.image_tag }}
docker stop app --time 30 || true
docker rm app || true
docker run -d \
--name app \
--restart unless-stopped \
--env-file /opt/app/production.env \
-p 8080:8000 \
${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ needs.ci.outputs.image_tag }}
# Verify deployment
for i in $(seq 1 30); do
if curl -sf http://localhost:8080/health; then
echo "Production deploy successful"
exit 0
fi
sleep 2
done
echo "Production deploy FAILED"
exit 1
- name: Auto-rollback on failure
if: failure()
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
echo "Rolling back to previous version"
PREV_IMAGE="${{ steps.current.outputs.stdout }}"
if [ "$PREV_IMAGE" != "none" ]; then
docker stop app --time 30 || true
docker rm app || true
docker run -d --name app --restart unless-stopped \
--env-file /opt/app/production.env \
-p 8080:8000 "$PREV_IMAGE"
fi
- name: Notify team
if: always()
run: |
STATUS="${{ job.status }}"
curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-d "{\"text\":\"Production deploy $STATUS: ${{ needs.ci.outputs.image_tag }}\"}"
Branch and Environment Strategy
| Branch | Trigger | Target | Approval |
| Feature branches | PR opened | CI checks only | PR review |
| `main` | PR merged | Staging auto-deploy | None |
| `main` (continued) | Staging smoke pass | Production | Manual approval in GitHub |
Implementation Checklist
| Step | Action | Time Estimate |
| 1 | Define deploy branch strategy | 30 minutes |
| 2 | Add CI checks (lint, test, audit) | 1-2 hours |
| 3 | Create Dockerfile with multi-stage build | 1 hour |
| 4 | Configure staging + production environments in GitHub | 30 minutes |
| 5 | Set up SSH deploy keys and secrets | 30 minutes |
| 6 | Write smoke test suite | 2-3 hours |
| 7 | Add rollback mechanism | 1 hour |
| 8 | Add Slack/email notifications | 30 minutes |
| 9 | Document runbook for manual intervention | 1 hour |
| 10 | Test full pipeline end-to-end | 1-2 hours |
Platform docs are available in the Documentation section. Review deployment options on the Services Overview page. Compare delivery tooling on DevOps Tools. Model costs on the Pricing page.
Frequently Asked Questions
Can I deploy to a VPS without Docker?
Yes. Replace the Docker commands with rsync for file deployment and systemd for process management. However, Docker provides reproducible environments and simpler rollbacks (just restart the previous container). For most applications, Docker adds minimal overhead and significant operational benefits.
How do I handle database migrations during deployment?
Run migrations as a separate step before deploying the new application version. Ensure migrations are backward-compatible (the old application version should work with the new schema). This allows the rolling update to proceed without breaking the currently running application instances.
Should I use GitHub Actions or a separate CI/CD tool?
GitHub Actions is the simplest choice when your code is on GitHub. It eliminates the need to manage a separate CI/CD server, handles secrets securely, and integrates directly with GitHub environments and approvals. Consider external tools (Jenkins, GitLab CI) only if you need features GitHub Actions does not provide, such as self-hosted runners with specialized hardware.
How do I set up preview environments for pull requests?
Deploy each PR to a temporary environment with a unique URL. Use a GitHub Actions workflow triggered on `pull_request` that deploys to a subdomain (e.g., `pr-42.staging.example.com`). Clean up the environment when the PR is merged or closed. This adds complexity but significantly improves review quality for frontend changes.
What if the deploy fails after the rollback version is also broken?
This is why you should always keep the last 3-5 known-good image tags. If the immediate previous version is also broken, deploy an older known-good version. Maintain a `LAST_KNOWN_GOOD` tag or version file that is only updated after a deployment passes all health checks and runs successfully for a stability window.
Final Takeaway
A one-click GitHub deploy workflow helps teams ship faster without sacrificing reliability. Start simple, standardize the pipeline, and iterate based on real deployment data. The pipeline should make deploying boring, because boring deployments are reliable deployments.