One-Click Deploy from GitHub to Cloud/VPS: A Practical CI/CD Workflow for Fast-Moving Teams

Published 9 min read
one-click deploy from GitHubGitHub deploy VPSdeploy app from GitHubCI/CDrelease automation

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

BranchTriggerTargetApproval

Feature branchesPR openedCI checks onlyPR review
`main`PR mergedStaging auto-deployNone
`main` (continued)Staging smoke passProductionManual approval in GitHub

Implementation Checklist

StepActionTime Estimate

1Define deploy branch strategy30 minutes
2Add CI checks (lint, test, audit)1-2 hours
3Create Dockerfile with multi-stage build1 hour
4Configure staging + production environments in GitHub30 minutes
5Set up SSH deploy keys and secrets30 minutes
6Write smoke test suite2-3 hours
7Add rollback mechanism1 hour
8Add Slack/email notifications30 minutes
9Document runbook for manual intervention1 hour
10Test full pipeline end-to-end1-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.