Automate Dokploy Deployments with GitHub Actions

Build a push-to-deploy pipeline that automates staging and production deployments.

Introduction

30 Second Summary

Every time you update your code, you have to manually log into your server, edit files, and hit redeploy. It's repetitive, error-prone, and eats into time you could spend building features.

In this project, you will build an automated deployment pipeline using GitHub Actions that deploys your code to Dokploy every time you push to GitHub.

What You'll Build

A fully automated CI/CD pipeline that eliminates manual docker-compose updates on Dokploy by triggering deployments automatically when you push code.

By the end of this project, you'll have:

  • A staging deployment workflow that automatically deploys your backend and frontend when you push to the staging branch.
  • A production deployment workflow with an approval gate so nothing goes live without your sign-off.
  • GitHub environments configured with separate secrets for staging and production.
  • Secret Mission: Add health checks and Discord notifications to verify your deployments automatically.

Are there any prerequisites?

You need a GitHub account with existing code, a Dokploy server accessible from the internet, and basic Git knowledge (push, pull, branches). You should already have a Docker Compose backend and a separate frontend running on Dokploy.

Connect Your Dokploy Compose to GitHub

Right now, every time you want to update your staging environment, you log into Dokploy and manually edit your docker-compose file or re-paste code.

In this step, you will connect your Dokploy backend Compose service to your GitHub repository, generate an API token, and verify the connection works from your terminal.

In this step, get ready to:
  • Switch your backend Compose service source from Raw to GitHub.
  • Generate a Dokploy API token and locate your service IDs.
  • Verify the API connection from your terminal using curl.
Switch your Dokploy backend service to use GitHub as its source

Dokploy supports three source types for Compose services: Raw (paste directly in the UI), Git, and GitHub. Switching to GitHub tells Dokploy where to pull your docker-compose.yml from when a deployment is triggered.

  • Open your Dokploy panel in your browser and sign in.
  • Navigate to your backend Compose service.
  • Click the General tab (or press g as a keyboard shortcut).
  • Under the source configuration section, look for the provider dropdown.
  • Select GitHub as the source type.

Haven't connected GitHub to Dokploy yet?

If this is your first time using GitHub as a source, you need to set up the integration first.

  • Go to Git in the Dokploy sidebar and select GitHub.
  • Click Create Github App and give it a unique name.
  • After creating the app, click Install and select which repositories Dokploy can access.
  • Click Install & Authorize to complete the setup.
  • Select your repository from the repository dropdown.
  • Set the branch to staging.
  • Set the Compose Path to ./docker-compose.yml (or wherever your compose file lives in the repo).
  • Click Save.

What does this change?

Dokploy will now pull your docker-compose.yml from the staging branch of your GitHub repo whenever a deployment is triggered.

Your environment variables stay in Dokploy's environment editor. They are saved as a .env file alongside the compose file during deployment, so you do not need to manage them through GitHub.

Don't see your repository in the dropdown?

Make sure you granted Dokploy access to the correct repository during the GitHub App installation step. You can update repository access in your GitHub account under Settings > Applications > your Dokploy GitHub App > Configure.

Help me connect my Dokploy Compose service to GitHub. I don't see my repository in the dropdown.

Generate a Dokploy API key

GitHub Actions will need a way to tell Dokploy to redeploy your services remotely. Dokploy's API uses an API key for authentication, passed in the x-api-key header.

  • In Dokploy, click Settings in the sidebar.
  • Click Profile.
  • Scroll down to the API/CLI section.
  • Click the button to generate a new API token.
  • Copy the token and save it somewhere safe (you will not be able to see it again).

Why not just use Dokploy's built-in auto-deploy?

Dokploy already auto-deploys on every push when connected to GitHub. However, triggering deployments through GitHub Actions gives you control over deployment order (backend before frontend), approval gates for production, and the ability to run health checks and notifications as part of the pipeline.

The API key is what lets GitHub Actions trigger deployments programmatically instead of relying on Dokploy's default auto-deploy.

Next, you need to find the IDs for your backend Compose service and frontend Application. These IDs tell the API which service to deploy.

  • Navigate to your backend Compose service in Dokploy.
  • Look at the URL in your browser's address bar. The Compose ID is the string after /compose/ in the URL.
  • Copy this ID and save it alongside your API token.
  • Navigate to your frontend Application in Dokploy.
  • Look at the URL in your browser's address bar. The Application ID is the string after /application/ in the URL.
  • Copy this ID and save it as well.

Keep these four values handy

You now have four pieces of information you will use throughout this project: your Dokploy URL, your API token, your backend Compose ID, and your frontend Application ID.

Test the API connection from your terminal

Before building the full pipeline, you want to confirm that your API key works and that your terminal can reach Dokploy's API. This is the same request GitHub Actions will make later.

  • Open Terminal by pressing Cmd+Space (macOS) and typing Terminal.
  • Test the connection by running this command (replace the placeholder values with your actual Dokploy URL and API key):
curl -X GET "[[DOKPLOY_URL="https://your-dokploy-domain.com"]]/api/project.all" \
  -H "accept: application/json" \
  -H "x-api-key: your-api-key-here"

What does this command do?

  • curl -X GET sends a GET request to the Dokploy API.
  • /api/project.all is a read-only endpoint that lists all your projects. It is safe to call without triggering any deployments.
  • x-api-key is the authentication header Dokploy expects.
  • You should see a JSON response listing your projects and their services (compose services, applications, databases).

Getting an authorization error or empty response?

  • Double-check that your API token is pasted correctly with no extra spaces.
  • Make sure your Dokploy URL does not have a trailing slash (e.g. https://dokploy.example.com not https://dokploy.example.com/).
  • Verify your Dokploy server is accessible from your network. Try opening the URL in your browser.

Help me troubleshoot my Dokploy API connection. I'm running curl but getting an error.

Your Dokploy server is now connected to GitHub and you have confirmed the API responds to your key. Next up, you will create a GitHub Actions workflow that uses this API to deploy automatically on every push.

Create Your First GitHub Actions Workflow

Your Dokploy server is connected to GitHub and ready to accept deployment triggers via its API. Now it's time to automate those triggers so you never have to run them manually again.

GitHub Actions is a CI/CD platform built directly into GitHub. You define a workflow in a YAML file, and GitHub runs it automatically whenever a specific event happens (like pushing code). In this step, you'll create your first workflow that triggers a Dokploy deployment every time you push to your staging branch.

In this step, get ready to:
  • Create the workflow directory and file in your repository.
  • Write a deployment workflow that calls the Dokploy API.
  • Push the workflow to GitHub and watch it run in the Actions tab.
Create the workflow directory and file

GitHub Actions looks for workflow files in a specific location: the .github/workflows/ directory at the root of your repository. Any .yml file placed there becomes a workflow that GitHub can run.

  • Return to Terminal in your repository's root directory.
  • Create the workflow directory by running this command:
mkdir -p .github/workflows
  • Create the workflow file by running:
touch .github/workflows/deploy-staging.yml
  • Confirm the file exists by running:
ls .github/workflows/

You should see deploy-staging.yml in the output.

Don't see the file?

Make sure you ran mkdir -p (with the -p flag) to create nested directories. Double-check you're in your repository's root folder, not a subdirectory.

help me create the .github/workflows directory

Write the staging deployment workflow

This workflow defines the core of your CI/CD pipeline: an event (pushing code) triggers an action (calling the Dokploy API to redeploy). The workflow uses GitHub Secrets to keep your credentials safe. You'll configure those secrets in the next step.

  • Open .github/workflows/deploy-staging.yml in your code editor and paste the following:
name: Deploy to Staging

on:
  push:
    branches: ["staging"]
  workflow_dispatch:

jobs:
  deploy-backend:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Trigger Backend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/compose.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"composeId": "${{ secrets.BACKEND_COMPOSE_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Backend deployment failed with status: $response"
            exit 1
          fi
          echo "Backend deployment triggered successfully!"

What does this workflow do?

  • on: push: branches: ["staging"] tells GitHub to run this workflow every time code is pushed to the staging branch.
  • workflow_dispatch adds a manual "Run workflow" button in the GitHub Actions UI, so you can trigger it on demand.
  • runs-on: ubuntu-latest tells GitHub to run your job on a fresh Linux server.
  • environment: staging scopes this job to a GitHub environment called "staging" (you'll create this in the next step).
  • The run block uses curl to call Dokploy's /api/compose.deploy endpoint and checks for a 200 response. If it fails, the workflow exits with an error.
  • Save deploy-staging.yml.

Why use secrets instead of hardcoded values?

The ${{ secrets.DOKPLOY_URL }} syntax references encrypted values stored in GitHub. Your API key and server URL never appear in your code, so anyone reading your repository can't see them. You'll add these secret values in the next step.

Seeing YAML syntax errors in your editor?

YAML is sensitive to indentation. Make sure you're using spaces (not tabs) and that each level is indented by exactly 2 spaces. The run: | block must have its content indented further than the run key itself.

help me fix YAML indentation in my GitHub Actions workflow

Push your workflow to GitHub and watch it run

With the workflow file saved, you need to push it to your staging branch on GitHub. Once GitHub receives the push, it automatically detects the workflow file and starts running it.

  • Switch to your staging branch (or create it if it doesn't exist yet) by running:
git checkout staging || git checkout -b staging

What does this command do?

This tries to switch to an existing staging branch. If it doesn't exist, the || operator runs the second command to create it from your current branch.

  • Stage, commit, and push the workflow file by running these commands:
git add .github/workflows/deploy-staging.yml
git commit -m "Add staging deployment workflow"
git push origin staging
  • Open your GitHub repository in your browser.
  • Click the Actions tab in the navigation bar at the top of the repository.

You should see a workflow run called "Deploy to Staging" appear in the list. It will show as in-progress or failed.

Why does the workflow fail?

The workflow triggered successfully, which proves GitHub Actions recognized your file and responded to the push event. The actual deployment step fails because the secrets (DOKPLOY_URL, DOKPLOY_API_KEY, BACKEND_COMPOSE_ID) haven't been configured in GitHub yet.

You'll add those secrets in the next step, and the workflow will succeed on its own from that point forward.

Don't see the Actions tab?

GitHub Actions is enabled by default on all repositories. If you don't see the tab, go to your repository's Settings > Actions > General and make sure actions are allowed to run.

If your push failed, check that you have write access to the repository and that your Git credentials are set up.

help me troubleshoot why my GitHub Actions workflow isn't appearing

✔️ Awesome, I've got everything!

Great. Double check you've saved deploy-staging.yml and that your workflow appeared in the Actions tab.

ⓧ I'd like to double check the full code

Here's the complete .github/workflows/deploy-staging.yml file:

name: Deploy to Staging

on:
  push:
    branches: ["staging"]
  workflow_dispatch:

jobs:
  deploy-backend:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Trigger Backend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/compose.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"composeId": "${{ secrets.BACKEND_COMPOSE_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Backend deployment failed with status: $response"
            exit 1
          fi
          echo "Backend deployment triggered successfully!"

Your first CI/CD pipeline is live. Next up, you'll configure GitHub Secrets and Environments so the workflow can actually reach Dokploy and trigger a real deployment.

Secure Your Secrets with GitHub Environments

Your workflow currently references secrets like ${{ secrets.DOKPLOY_URL }} and ${{ secrets.DOKPLOY_API_KEY }}. Right now, those secrets don't exist anywhere in GitHub, which is why your workflow reads empty values.

GitHub provides a dedicated secrets vault that encrypts your credentials and injects them into workflows at runtime. On top of that, GitHub Environments let you scope secrets to a specific deployment target. This means your staging secrets stay completely separate from production secrets.

In this step, get ready to:
  • Add your Dokploy secrets to your GitHub repository.
  • Create a staging environment with scoped secrets.
  • Create a production environment with an approval gate.
Add your Dokploy secrets to GitHub

Your workflow needs four pieces of information to trigger deployments: your Dokploy server URL, API key, backend Compose ID, and frontend Application ID. Storing these as GitHub secrets keeps them encrypted and out of your code.

  • In your GitHub repository, click Settings in the top navigation bar.
  • In the left sidebar, expand Secrets and variables.
  • Click Actions.
  • Click New repository secret.
  • In the Name field, enter DOKPLOY_URL.
  • In the Secret field, paste your Dokploy server URL (e.g. https://dokploy.yourdomain.com). Do not include a trailing slash.
  • Click Add secret.

Repeat the process three more times to add the remaining secrets:

  • Name: DOKPLOY_API_KEY — paste the API token you generated in Dokploy.
  • Name: BACKEND_COMPOSE_ID — paste the Compose ID for your backend service.
  • Name: FRONTEND_APP_ID — paste the Application ID for your frontend.

You should now see four secrets listed under Repository secrets.

Why use secrets instead of hardcoding values?

GitHub encrypts each secret at rest and only exposes the value inside a running workflow. The values never appear in logs (GitHub masks them automatically). This means you can share your repository publicly without leaking credentials.

Don't see the Settings tab?

Only repository admins can access Settings. Make sure you own the repository or have admin permissions. If you forked the repo, navigate to your fork (not the original).

Help me find the secrets settings in my GitHub repository.

Create a staging environment in GitHub

Repository-level secrets work, but they're available to every workflow and every environment. Environments let you scope secrets so that only workflows running in that specific environment can access them.

  • In your repository's Settings, click Environments in the left sidebar.
  • Click New environment.
  • Enter staging as the name.
  • Click Configure environment.

Now move your secrets from the repository level into this environment.

  • Scroll down to the Environment secrets section.
  • Click Add environment secret.
  • Add each of your four secrets: DOKPLOY_URL, DOKPLOY_API_KEY, BACKEND_COMPOSE_ID, and FRONTEND_APP_ID. Use your staging Dokploy values.

Remove the repository-level secrets

Once your secrets exist in the staging environment, go back to Settings > Secrets and variables > Actions and delete the four repository-level secrets you created earlier. This avoids confusion about which secrets your workflow actually uses.

Your workflow already has environment: staging in the job definition. This is the line that tells GitHub Actions to use secrets from the staging environment you just created.

How does the workflow know which environment to use?

The environment: staging key in your workflow's job definition links that job to the GitHub Environment called staging. When the job runs, GitHub injects only the secrets scoped to that environment. Repository-level secrets are also available unless you've removed them.

Secrets not being injected into your workflow?

Make sure the environment: value in your workflow YAML matches the environment name exactly (case-sensitive). If your environment is called staging but your YAML says Staging, the secrets won't load.

Help me figure out why my GitHub Actions secrets aren't being injected.

Create a production environment with an approval gate

Production deployments need an extra safety layer. You don't want code going live just because someone accidentally pushed to main. GitHub Environments support required reviewers, which means a human must click "Approve" before the deployment job runs.

  • Go back to Settings > Environments.
  • Click New environment.
  • Enter production as the name.
  • Click Configure environment.

First, add the approval gate.

  • Under Deployment protection rules, check the box for Required reviewers.
  • In the search field that appears, type your own GitHub username and select yourself.
  • Click Save protection rules.

Now add your production secrets.

  • Scroll down to Environment secrets and click Add environment secret.
  • Add the same four secrets: DOKPLOY_URL, DOKPLOY_API_KEY, BACKEND_COMPOSE_ID, and FRONTEND_APP_ID. Use your production Dokploy values (these may differ from staging).

What if my staging and production use the same Dokploy server?

That's fine. Your DOKPLOY_URL and DOKPLOY_API_KEY might be identical across both environments. The important difference is the Compose ID and Application ID, which point to different services on your Dokploy server.

If you only have one environment in Dokploy right now, use the same values for both. The environment separation in GitHub still gives you the approval gate for production.

You should now see two environments listed under Settings > Environments: staging and production. The production environment displays a shield icon indicating protection rules are active.

Why separate environments instead of one set of secrets?

Environment-scoped secrets guarantee that a workflow triggered by a push to staging can never accidentally use production credentials. Each job declares which environment it belongs to, and GitHub only exposes the matching secrets.

The approval gate adds a human checkpoint. When someone pushes to main, the workflow pauses and waits for you (the required reviewer) to approve before deploying to production.

Your secrets are locked down and your environments are ready. Next up, you'll expand your workflow to deploy both the backend and frontend together, then create the production workflow that uses the approval gate you just configured.

Deploy Backend and Frontend Together

Your staging workflow currently deploys only the backend. In a real application, you need both services updated together in the right order.

In this step, you will expand your staging workflow to also deploy the frontend (after the backend finishes). Then you will create a separate production workflow that requires your approval before running.

In this step, get ready to:
  • Add a frontend deployment job to your staging workflow.
  • Create a production deployment workflow triggered by pushes to main.
  • Test the full pipeline end-to-end on both environments.
Add a frontend deployment job to your staging workflow

Your backend and frontend are separate services on Dokploy. The backend is a Docker Compose service, and the frontend is a standalone application. They use different API endpoints to trigger deployments.

Order matters here. You want the backend to finish deploying before the frontend starts, so the frontend always connects to an up-to-date API.

  • Open .github/workflows/deploy-staging.yml in your code editor.
  • Add the following deploy-frontend job below the existing deploy-backend job:
  deploy-frontend:
    runs-on: ubuntu-latest
    environment: staging
    needs: deploy-backend
    steps:
      - name: Trigger Frontend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/application.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"applicationId": "${{ secrets.FRONTEND_APP_ID }}"}') 
          if [ "$response" != "200" ]; then
            echo "Frontend deployment failed with status: $response"
            exit 1
          fi
          echo "Frontend deployment triggered successfully!"

What does this code do?

This job mirrors the backend job but targets your frontend application instead of the compose service.

  • needs: deploy-backend tells GitHub Actions to wait until deploy-backend completes successfully before starting this job. If the backend fails, the frontend job is automatically skipped.
  • /api/application.deploy is the Dokploy endpoint for triggering application deployments (as opposed to /api/compose.deploy for compose services).
  • FRONTEND_APP_ID is pulled from your staging environment secrets, keeping the workflow file free of hardcoded IDs.
  • Save deploy-staging.yml.

YAML indentation error?

The deploy-frontend job must be at the same indentation level as deploy-backend (two spaces in from jobs:). YAML is whitespace-sensitive, so an extra space will cause a parse error.

help me fix a YAML indentation error in my GitHub Actions workflow

✔️ Awesome, I've got everything!

Great. Double check you have saved deploy-staging.yml before moving on.

ⓧ I'd like to double check the full code

Here is the complete deploy-staging.yml file at this point:

name: Deploy to Staging

on:
  push:
    branches: ["staging"]
  workflow_dispatch:

jobs:
  deploy-backend:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Trigger Backend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/compose.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"composeId": "${{ secrets.BACKEND_COMPOSE_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Backend deployment failed with status: $response"
            exit 1
          fi
          echo "Backend deployment triggered successfully!"

  deploy-frontend:
    runs-on: ubuntu-latest
    environment: staging
    needs: deploy-backend
    steps:
      - name: Trigger Frontend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/application.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"applicationId": "${{ secrets.FRONTEND_APP_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Frontend deployment failed with status: $response"
            exit 1
          fi
          echo "Frontend deployment triggered successfully!"
Create the production deployment workflow

Your production workflow is almost identical to staging, but it triggers on pushes to main and uses the production environment. The approval gate you configured earlier kicks in automatically because GitHub recognizes the environment name.

  • Create a new file at .github/workflows/deploy-production.yml by running:
touch .github/workflows/deploy-production.yml
  • Paste the following workflow into deploy-production.yml:
name: Deploy to Production

on:
  push:
    branches: ["main"]
  workflow_dispatch:

jobs:
  deploy-backend:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Trigger Backend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/compose.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"composeId": "${{ secrets.BACKEND_COMPOSE_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Backend deployment failed with status: $response"
            exit 1
          fi
          echo "Backend deployment triggered successfully!"

  deploy-frontend:
    runs-on: ubuntu-latest
    environment: production
    needs: deploy-backend
    steps:
      - name: Trigger Frontend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/application.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"applicationId": "${{ secrets.FRONTEND_APP_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Frontend deployment failed with status: $response"
            exit 1
          fi
          echo "Frontend deployment triggered successfully!"

What is different about this workflow?

Two key differences separate this from the staging workflow:

  • branches: ["main"] means this workflow only fires when code is pushed to main (or triggered manually via workflow_dispatch).
  • environment: production causes GitHub to pause and wait for your manual approval before running each job, because you enabled "Required reviewers" on the production environment in the previous step.
  • Save deploy-production.yml.
  • Confirm both workflow files exist by running:
ls .github/workflows/

You should see deploy-staging.yml and deploy-production.yml listed in the output.

Only seeing one file?

Make sure you created deploy-production.yml inside the same .github/workflows/ directory where deploy-staging.yml lives. If you accidentally created it in the repository root, move it into the correct folder.

help me find where my workflow file ended up in my repository

✔️ Awesome, I've got everything!

Great. Double check you have saved both workflow files before continuing.

ⓧ I'd like to double check the full code

Here is the complete deploy-production.yml:

name: Deploy to Production

on:
  push:
    branches: ["main"]
  workflow_dispatch:

jobs:
  deploy-backend:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Trigger Backend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/compose.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"composeId": "${{ secrets.BACKEND_COMPOSE_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Backend deployment failed with status: $response"
            exit 1
          fi
          echo "Backend deployment triggered successfully!"

  deploy-frontend:
    runs-on: ubuntu-latest
    environment: production
    needs: deploy-backend
    steps:
      - name: Trigger Frontend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/application.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"applicationId": "${{ secrets.FRONTEND_APP_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Frontend deployment failed with status: $response"
            exit 1
          fi
          echo "Frontend deployment triggered successfully!"
Test the full pipeline end-to-end

With both workflows in place, it is time to push code and watch the automation happen. You will test staging first, then promote to production.

  • Make sure you are on the staging branch by running:
git checkout staging
  • Commit and push both workflow files by running:
git add .github/workflows/deploy-staging.yml .github/workflows/deploy-production.yml
git commit -m "Add frontend deployment and production workflow"
git push origin staging
  • Open your repository on GitHub.
  • Click the Actions tab.
  • Click on the running workflow (titled Deploy to Staging).

You should see two jobs listed: deploy-backend and deploy-frontend. The frontend job waits for the backend to finish before starting.

What should I see?

In the Actions tab, the workflow run shows deploy-backend completing first (green checkmark), then deploy-frontend starting and completing. Both jobs should show green checkmarks when finished.

Over in your Dokploy panel, both your backend compose service and frontend application should reflect new deployments.

  • Now merge your staging branch into main to trigger the production workflow:
git checkout main
git merge staging
git push origin main
  • Return to the Actions tab on GitHub.
  • Click on the Deploy to Production workflow run.
  • You will see a yellow "Waiting for approval" banner. Click Review deployments.
  • Check the production environment checkbox.
  • Click Approve and deploy.

Both production jobs now run in sequence. Your backend deploys first, then your frontend follows.

Why is the approval gate valuable?

The approval gate prevents accidental production deployments. If someone pushes directly to main by mistake, the workflow pauses and waits for a human to confirm before any production service is touched.

This is a standard safety pattern in professional CI/CD pipelines. Staging deploys automatically for fast feedback. Production deploys only when a reviewer says "go".

Production workflow not appearing?

Make sure deploy-production.yml was merged into the main branch. GitHub Actions only recognizes workflow files that exist on the branch being pushed to. If the file only exists on staging, it will not trigger on a push to main.

help me troubleshoot why my production workflow is not triggering

You now have a complete two-environment pipeline. Staging deploys automatically on every push. Production deploys only when you approve it. Next up, you will explore what else GitHub Actions can automate beyond deployments.

Understand What Else GitHub Actions Can Do

Your pipeline is fully operational. Code pushed to staging deploys automatically, and code merged to main requires your approval before going live.

Deployments are just the beginning of what GitHub Actions can automate. In this step, you'll explore other trigger types, learn how to run arbitrary scripts inside your pipeline, and solidify the mental model behind everything you've built.

In this step, get ready to:
  • Explore workflow triggers beyond push (schedules, manual dispatch, pull requests).
  • Understand how to run shell scripts in your pipeline.
  • Review the key CI/CD mental model you've learned.
Explore workflow triggers beyond push

Your workflows currently respond to two events: a push to a specific branch, and manual dispatch. Look at the top of your deploy-staging.yml file.

  • In your repository on GitHub, navigate to .github/workflows/deploy-staging.yml and find the on: section at the top:
on:
  push:
    branches: ["staging"]
  workflow_dispatch:

What do these triggers mean?

  • push: branches: ["staging"] fires the workflow every time someone pushes a commit to the staging branch.
  • workflow_dispatch: adds a Run workflow button to the Actions tab. This lets you manually re-trigger a deployment without pushing new code.
  • Switch to the Actions tab in your GitHub repository.
  • Click Deploy to Staging in the left sidebar.
  • Click the Run workflow button above the list of workflow runs.
  • Select your staging branch from the dropdown, then click Run workflow.

You should see a new workflow run appear and execute successfully. This confirms your manual "redeploy" button works.

What other triggers exist?

GitHub Actions supports many event types beyond push and workflow_dispatch. Here are the most useful ones for CI/CD:

  • schedule — triggers on a cron schedule. Uses POSIX cron syntax (five fields: minute, hour, day of month, month, day of week). Useful for nightly builds or periodic tasks.
  • pull_request — triggers when a pull request is opened, updated, or merged. Perfect for running tests before code reaches your main branches.
  • repository_dispatch — triggers from an external webhook. Useful when another service needs to kick off your pipeline.

You could combine multiple triggers on the same workflow. For example, adding a nightly redeploy schedule alongside your push trigger.

Run shell scripts in your pipeline

Your current workflow runs inline shell commands directly in the YAML. But GitHub Actions can also execute full scripts from your repository. This opens up possibilities like database migrations, cache clearing, test suites, or pre-deploy validation.

The pattern requires two pieces: checking out your repository code (so the runner has access to your files), and then executing the script.

How does running a script work?

By default, a GitHub Actions runner starts with an empty workspace. It does not have your repository files. To access files in your repo, you need the actions/checkout@v4 action, which clones your repository onto the runner.

Once your code is checked out, you can run any script in your repo. The pattern looks like this:

  • Add a step that uses actions/checkout@v4 to clone your repository onto the runner.
  • Add a step that makes the script executable with chmod +x and then runs it.

In the Secret Mission, you'll implement this by creating a scripts/pre-deploy.sh file and wiring it into your staging workflow.

Anything that runs in bash runs in Actions

If you can run it in a terminal, you can run it in GitHub Actions. Database migrations, linting, security scans, notification scripts, integration tests. The runner is a full Ubuntu machine with network access.

Review the key CI/CD concepts you've learned

You've built a real CI/CD pipeline from scratch. Let's name the mental model that connects everything.

The GitHub Actions mental model

Everything in GitHub Actions follows a four-level hierarchy:

  • Events trigger Workflows. An event is a push, a schedule, a manual click, or a pull request.
  • Workflows contain Jobs. A workflow is your .yml file in .github/workflows/.
  • Jobs contain Steps. Each job runs on its own runner (a fresh Ubuntu machine). Jobs can depend on each other using needs:.
  • Steps are individual commands or actions. Each step runs sequentially inside its job.

Secrets keep credentials safe by injecting them at runtime without exposing them in your code. Environments separate staging from production and add approval gates.

Where else does this pattern apply?

This mental model transfers to every CI/CD tool. Jenkins uses Pipelines with Stages and Steps. GitLab CI uses Pipelines with Jobs and Stages. CircleCI uses Workflows with Jobs. The naming differs, but the hierarchy is the same everywhere.

Here's what you've configured using this model:

  • Event: a push to staging or main (plus manual dispatch).
  • Workflow: deploy-staging.yml and deploy-production.yml.
  • Jobs: deploy-backend and deploy-frontend (frontend depends on backend via needs:).
  • Steps: the curl commands that call Dokploy's API.
  • Secrets: your Dokploy URL, API key, and service IDs stored securely in GitHub.
  • Environments: staging (auto-deploy) and production (approval required).

✔️ Awesome, I've got everything!

Great. Double check you've saved both workflow files if you made any edits.

ⓧ I'd like to double check the full code

Here is the full deploy-staging.yml file:

name: Deploy to Staging

on:
  push:
    branches: ["staging"]
  workflow_dispatch:

jobs:
  deploy-backend:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Trigger Backend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/compose.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"composeId": "${{ secrets.BACKEND_COMPOSE_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Backend deployment failed with status: $response"
            exit 1
          fi
          echo "Backend deployment triggered successfully!"

  deploy-frontend:
    runs-on: ubuntu-latest
    environment: staging
    needs: deploy-backend
    steps:
      - name: Trigger Frontend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/application.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"applicationId": "${{ secrets.FRONTEND_APP_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Frontend deployment failed with status: $response"
            exit 1
          fi
          echo "Frontend deployment triggered successfully!"

And here is the full deploy-production.yml file:

name: Deploy to Production

on:
  push:
    branches: ["main"]
  workflow_dispatch:

jobs:
  deploy-backend:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Trigger Backend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/compose.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"composeId": "${{ secrets.BACKEND_COMPOSE_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Backend deployment failed with status: $response"
            exit 1
          fi
          echo "Backend deployment triggered successfully!"

  deploy-frontend:
    runs-on: ubuntu-latest
    environment: production
    needs: deploy-backend
    steps:
      - name: Trigger Frontend Deployment on Dokploy
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
            "${{ secrets.DOKPLOY_URL }}/api/application.deploy" \
            -H "Content-Type: application/json" \
            -H "x-api-key: ${{ secrets.DOKPLOY_API_KEY }}" \
            -d '{"applicationId": "${{ secrets.FRONTEND_APP_ID }}"}')
          if [ "$response" != "200" ]; then
            echo "Frontend deployment failed with status: $response"
            exit 1
          fi
          echo "Frontend deployment triggered successfully!"

Secret mission

Add Health Checks and Notifications

Your pipeline deploys code, but how do you know your services are actually running? Right now, a deployment could succeed (Dokploy pulled the code) but your app could crash on startup, and you would never know until someone reports it. In this secret mission, you will add a health check that verifies your services are alive after every deployment, a Discord notification that tells you the result, and a pre-deploy script that validates your repository before deployment begins.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely within GitHub Actions and your existing Dokploy server, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • GitHub Actions workflow files (.github/workflows/deploy-staging.yml and .github/workflows/deploy-production.yml).
  • Pre-deploy script (scripts/pre-deploy.sh).
  • GitHub Environments (staging and production) with secrets and variables.
  • Dokploy API token.
  • Discord webhook (Secret Mission only).

Keep everything running

No action needed. Choose this if you're still actively building or want your deployments to keep running automatically on every push.

  • Your GitHub Actions workflows will continue triggering on pushes to staging and main.
  • Your Dokploy API token remains active.
  • Your Discord webhook continues sending notifications.

Pause - I'll come back to this later

Disable your workflows so they stop triggering, but keep all files and configuration intact for when you return.

  • In your GitHub repository, click the **Actions** tab.
  • Click on each workflow name (Deploy to Staging and Deploy to Production).
  • Click the three-dot menu (...) and select Disable workflow.
  • Your workflow files, secrets, and environments stay in place. Re-enable the workflows when you're ready to resume.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Delete the workflow files and script from your repository by running:
git rm -r .github/workflows/deploy-staging.yml .github/workflows/deploy-production.yml scripts/pre-deploy.sh
git commit -m "Remove CI/CD workflow files"
git push
  • Delete the GitHub Environments. In your repository, go to Settings > Environments.
  • Click the trash icon next to staging and confirm by clicking I understand, delete this environment.
  • Repeat for production. This removes all associated secrets and protection rules.
  • Revoke your Dokploy API token. In your Dokploy panel, go to Settings > Profile > API/CLI section and delete the token you generated.
  • Delete the Discord webhook. In your Discord server, go to Server Settings > Integrations > Webhooks and delete the webhook you created.

That's a Wrap!

That's a Wrap!

Nice work! You've gone from manually editing docker-compose files in Dokploy to having a fully automated push-to-deploy pipeline powered by GitHub Actions.

You've learned how to:

  • Created GitHub Actions workflows that automatically deploy your backend and frontend to Dokploy on every push, with ordered job dependencies and error handling.
  • Set up separate staging and production environments with scoped secrets, an approval gate for production, and the core CI/CD mental model of Events, Workflows, Jobs, and Steps.
  • Extended your pipeline beyond deployments by running custom shell scripts, adding manual triggers, and discovering schedule-based automation.
  • Added health checks and Discord notifications that verify your services are alive after deployment and alert you to failures automatically.

Ready to quiz yourself?