This site contains affiliate links — we may earn a commission if you sign up through them.

CI/CD Guide with GitHub Actions to DigitalOcean 2026

A practical guide to building GitHub Actions workflows that deploy code to DigitalOcean Droplets via SSH and to App Platform via doctl, covering secrets management and rollback strategy.

CI/CD Guide with GitHub Actions to DigitalOcean 2026

Integrating GitHub Actions with DigitalOcean allows automatic building, testing, and deployment whenever code is pushed to the main branch, eliminating the need to manually SSH into your server. This article walks you through CI/CD concepts and then shows you how to write production workflows to deploy to Droplets via SSH and to App Platform via doctl, including secrets management and rollback planning when deployments fail.

What is CI/CD and Why It Matters

CI/CD stands for Continuous Integration and Continuous Deployment (some teams use Continuous Delivery for the latter term instead). It's an approach where code changes are automatically built, tested, and deployed to production through predefined steps, rather than requiring developers to manually SSH into a server, pull code, and restart services each time — which is time-consuming and error-prone. Mistakes like forgetting a step or running commands in the wrong environment are common human errors. GitHub Actions is a CI/CD system built directly into GitHub, operating through YAML files placed in the .github/workflows/ folder of your repository, such as .github/workflows/deploy.yml. The system automatically runs workflows when specified events occur, for example on: push: branches: [main] means the workflow triggers every time code is pushed or merged into the main branch. The advantage of GitHub Actions is that you don't need a separate CI server, there's no extra cost for public repositories, and private repositories include free runtime minutes per month based on your GitHub plan. For DigitalOcean users, connecting GitHub Actions to your infrastructure involves two main approaches: deploying to a Droplet that you manage yourself via SSH (which offers maximum flexibility since you control every step), or deploying to App Platform, which is a PaaS service where DigitalOcean handles the infrastructure. Both approaches can work with the same workflow — you just swap the final deployment step. The real value of CI/CD isn't just speed, but consistency: every deployment follows the same process regardless of who pushed the code, reducing issues that plague small teams where everyone deploys differently, like forgetting to run database migrations before restarting services.

Building a Workflow to Deploy to Droplets via SSH

From multiple reviews, deploying to a Droplet via GitHub Actions uses the same principle as developers manually SSHing into a server — the workflow simply runs commands instead. The most popular approach uses the ready-made appleboy/ssh-action action, which takes a host, username, and private key, then runs specified commands on the remote server. Here's an example workflow step: - uses: appleboy/ssh-action@v1 with: host: ${{ secrets.DROPLET_HOST }} username: deploy key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /var/www/myapp git pull origin main docker compose up -d --build Typically, the same file includes earlier jobs for building and testing — checking out code with actions/checkout@v4, installing dependencies, running tests — before advancing to the deploy step only if all tests pass. Use needs: to make the deploy job wait for the test job to complete, preventing code with failing tests from reaching production. For Droplets running containerized apps, consider building and pushing images to DigitalOcean Container Registry first, then having the Droplet simply pull and run the image rather than building on production. This way your production server doesn't need a full build toolchain installed, and deployment time is shorter since you're only swapping images instead of recompiling on the live machine. A starter Droplet with 1 GiB RAM / 1 vCPU at $6/month is sufficient for small to medium applications. For users in Thailand, the sgp1 (Singapore) region is recommended for the lowest latency compared to other DigitalOcean regions.

Building a Workflow to Deploy to App Platform

App Platform already supports native GitHub integration through the control panel, which auto-deploys whenever you push to a configured branch — no workflow needed. However, many teams choose to control deployments through GitHub Actions instead, because they want to run complex test/lint/build steps first before triggering a real deployment, or they need to deploy only specific folders from a monorepo where changes actually occurred. The main tool is doctl via the official GitHub Action digitalocean/action-doctl@v2, which installs doctl in the runner and authenticates using a Personal Access Token stored in secrets. Here's an example step: - uses: digitalocean/action-doctl@v2 with: token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - run: doctl apps create-deployment ${{ secrets.DO_APP_ID }} --wait The command doctl apps create-deployment <app-id> --wait tells App Platform to start a new deployment from the branch linked to that app, and the --wait flag blocks the workflow until deployment completes or fails, rather than returning immediately while deployment continues. This matters if your workflow has verification steps after deployment. For new apps or major configuration changes (adding environment variables, changing instance size), use a YAML spec file stored in your repository, like app.yaml, then run doctl apps create --spec app.yaml for new apps or doctl apps update <app-id> --spec app.yaml for existing ones. This approach keeps your entire app structure as code in git, letting you review infrastructure changes via pull requests just like any other code. App Platform offers container instance plans by size: starting at shared 1 vCPU / 512 MiB RAM / 50 GiB transfer for $5/month, up to shared 2 vCPU / 4 GiB RAM / 250 GiB transfer at $50/month. The free plan supports only static sites (up to 3 apps and 1 GiB transfer per app), perfect for testing workflows before production.

Storing Secrets (SSH Keys/API Tokens) Securely

Sensitive data like SSH private keys and DigitalOcean Personal Access Tokens must never be written directly into workflow files. GitHub has a built-in encrypted secrets system accessible at repository Settings > Secrets and variables > Actions. Stored values are encrypted and never shown in workflow logs, even to the repository owner — you can only create new values, not retrieve old ones. For SSH keys, create a separate key pair solely for CI/CD deployments, not the same key you personally use to SSH into servers. Generate it with ssh-keygen -t ed25519 -C "github-actions-deploy", then add the public key to ~/.ssh/authorized_keys on the Droplet user account (preferably create a restricted "deploy" user with access only to app folders, not root). Copy the entire private key file (including the -----BEGIN OPENSSH PRIVATE KEY----- header/footer) into a secret named SSH_PRIVATE_KEY. For DigitalOcean Personal Access Tokens, create one at cloud.digitalocean.com/account/api/tokens specifically for CI/CD, not your personal token, so you can revoke it immediately if needed without disrupting other tasks. Store it in a secret named DIGITALOCEAN_ACCESS_TOKEN and grant only necessary permissions — for example, if the workflow just triggers existing app deployments, there's no need to grant permissions to create/delete all resources. For teams with multiple environments (staging and production), use GitHub Environments to separate secrets by environment — the same secret name can have different values in staging vs production. You can also set required reviewers so someone must approve before the production deploy job runs, adding an extra safety layer beyond secure secret storage.

Key takeaway: Always use GitHub encrypted secrets (Settings > Secrets and variables > Actions) — never write keys/tokens directly in YAML
  1. Always use GitHub encrypted secrets (Settings > Secrets and variables > Actions) — never write keys/tokens directly in YAML
  2. Create a new SSH key pair specifically for CI/CD with ssh-keygen -t ed25519, separate from your personal key, and use a restricted user (not root)
  3. Create a separate DigitalOcean Personal Access Token at cloud.digitalocean.com/account/api/tokens just for CI/CD so you can revoke it independently
  4. Use GitHub Environments to separate secrets between staging and production, with required reviewers approving production deployments
  5. Follow least-privilege: grant tokens/keys only the permissions they actually need, and rotate them periodically

Rolling Back When Deployments Fail

Plan rollback strategies during workflow design, not after problems occur. For Droplets deploying via SSH, a popular pattern stores multiple releases in separate folders like /var/www/myapp/releases/<timestamp> with a symlink named current pointing to the active release. If a new deployment fails or problems surface afterward, instantly point the symlink back to the previous release with a single command — no rebuild or code re-pull required. For container-based Droplets, rollback is simpler: tag images with commit SHA instead of just latest, like myapp:${{ github.sha }}. When rolling back, change the image tag in your compose file to the SHA of a previously working commit and restart the service. App Platform has built-in deployment history available through doctl apps list-deployments <app-id>, showing each deployment's ID, status, and timestamp. If the latest deployment fails or has problems, you can re-deploy from a prior commit immediately. In some cases, App Platform auto-rolls back if a new deployment fails during build or health checks, keeping the old version running until the new one succeeds. To reduce risk from the start, add a smoke test step at the end of every workflow after deployment succeeds — curl the app's health check endpoint and verify the HTTP status or response content. If this step fails, mark the workflow as failed immediately so the team gets alert before users encounter the problem.

  1. Droplet: use releases/<timestamp> pattern plus a symlink named current pointing to the live release, swap symlink instantly on rollback
  2. Containers: tag images with commit SHA instead of latest only, so rollback is precise — just change the tag and restart
  3. App Platform: doctl apps list-deployments <app-id> shows deployment history; re-deploy from any prior commit

When This Feature Is Worth Using (Real Use Cases)

Investing time to write GitHub Actions workflows pays off most for teams or projects that push code frequently — multiple times per day or week. Every time you save on manual SSH deployments, the value compounds. Projects with multiple team members benefit immediately because everyone uses the same deployment process instead of each doing it their way, eliminating "it works on my machine" problems. For projects with separate environments like staging (for testing before production), automated workflows let you deploy to staging on every merge to the develop branch instantly, without waiting for someone to do it manually, then require a reviewer before production deployments. This lets the team test new features on staging quickly without affecting real users. Conversely, small personal projects you deploy rarely, or single-developer projects where you're already comfortable with manual SSH deployment, might not justify the setup time. For those, plain SSH with a deploy script or App Platform's native auto-deploy (no workflow needed) might get you running faster. Use GitHub Actions when your pipeline needs complex steps beyond basic builds: database migrations before restarting services, integration tests against real databases before deployment, Slack notifications when deploys succeed or fail, or selective deployments from a monorepo that only build services with actual changes. These are difficult or impossible with simple native auto-deploy.

Common Errors and How to Fix Them

A point users often miss: the most common SSH deployment error is "Permission denied (publickey)," usually from one of three causes: the public key isn't correctly in ~/.ssh/authorized_keys on the remote user, the private key in the secret has formatting issues or missing lines (copy the entire file including -----BEGIN OPENSSH PRIVATE KEY----- and -----END OPENSSH PRIVATE KEY-----), or the .ssh folder permissions are wrong — SSH may reject the key if permissions are too loose for security reasons. Another common error with doctl in workflows is 401 Unauthorized despite setting up secrets. Usually the secret name in the workflow file doesn't match what you created in repository settings (check case sensitivity), or the token expired/was revoked. Add a simple verification step early in the workflow like doctl account get to confirm authentication works before the real deployment step. If the workflow reports success but your app doesn't change, you probably forgot the restart step after pulling new code — the old process is still running the old code in memory. Add a restart step like systemctl restart myapp or docker compose up -d --build after pulling code. Another common cause is incorrect workflow trigger conditions, like setting branches: [master] when the repository switched to main — the workflow never runs. For App Platform, "unable to update app: spec is invalid" usually means the app.yaml file has typos or syntax that App Platform doesn't support. Run doctl apps spec validate app.yaml to catch syntax errors before creating or updating the app in your next workflow step.

  1. "Permission denied (publickey)": verify public key in authorized_keys, check private key format has complete header/footer in secret, verify .ssh folder permissions
  2. 401 Unauthorized from doctl: check secret name matches exactly (case-sensitive) and add doctl account get test step to verify auth before real deployment
  3. Workflow succeeds but app unchanged: forgot to restart like systemctl restart myapp or docker compose up -d --build after pulling code
  4. Workflow doesn't trigger at all: check branches: in on.push matches the actual branch name (main vs master)

Best Practices

Writing workflows that stay secure and maintainable long-term requires several good habits from the start. Pin the versions of actions you use clearly, like actions/checkout@v4 instead of @main or floating tags, to prevent new action versions from unexpectedly breaking workflows that used to work. Security-conscious teams may pin down to commit SHA instead of tags, since tags can be moved to point to different commits later. Always separate build/test jobs from deploy jobs using needs: — deploy should run only after tests pass completely. Add a concurrency: group at the workflow level to prevent multiple deployments from running simultaneously if someone pushes twice in quick succession, which could create unpredictable state on production if two deploys race to write the same files. Use GitHub Environments to clearly separate staging from production, always requiring reviewers for production deployments even in small teams — one approval step is a good safeguard against accidental pushes. Keep deployment logs so you can audit history: use doctl apps list-deployments for App Platform or log each successful SSH deployment to a file on the Droplet. Finally, test the entire workflow on staging before using it on production, including testing the rollback path, not just the successful deployment path. Workflows that have never been tested in failure mode often hide bugs that surface exactly when you most need reliability.

Get $200 Free Credit →

Frequently Asked Questions

Must I use GitHub Actions, or can I use other CI/CD systems with DigitalOcean?
You don't have to use GitHub Actions. DigitalOcean isn't locked to any specific CI/CD system — you can use GitLab CI, CircleCI, or others equally well. You just need to store SSH keys or Personal Access Tokens as secrets in that system and call doctl or SSH the same way.
Which is better: deploying to Droplets or App Platform?
It depends. Droplets suit teams wanting full server control, unlimited customization, and lower costs under high load. App Platform suits teams wanting to reduce infrastructure maintenance burden, no patch management, and built-in auto-scaling — you trade flexibility for less operational overhead.
What if a workflow deployment gets stuck mid-run?
You can cancel the workflow run directly from the repository's Actions page. For App Platform using --wait, if deployment hangs unusually long, check the status via doctl apps list-deployments to see if it's the build or health check that's stuck. For Droplets, hanging SSH usually means the remote script is waiting for input or a process won't exit — verify your script always completes without interactive prompts.
Do I pay GitHub for using GitHub Actions to deploy to DigitalOcean?
GitHub Actions includes free runtime minutes per month based on your plan (unlimited for public repos on standard runners). Costs come from DigitalOcean resources you deploy to — Droplets and App Platform at their normal rates. No additional charges for connecting the two systems.
Must I have a staging environment before using CI/CD to deploy to production?
Not required, but strongly recommended for any project with real users. Staging catches problems before they affect users. If you don't have separate staging resources, at minimum add automated smoke tests after every production deployment to catch issues fast — better than finding out from users.
How secure is an SSH private key stored in GitHub Actions?
GitHub's encrypted secrets provide the security GitHub designed them for — values are encrypted and won't appear in logs. Overall security depends on team practices too: use a key only for CI/CD separate from your personal key, limit the remote user's permissions to the minimum necessary, and rotate keys periodically.