Manual deployments are the silent killer of developer productivity. In my early projects, I spent hours manually running docker build, tagging images, and SSH-ing into servers to pull new versions. It was tedious and prone to human error. That’s why I switched to github actions for docker deployment, and it completely changed my workflow.

By automating the build and deploy cycle, you ensure that what you see in your local environment is exactly what hits production. Whether you are deploying to a single VPS or a complex cluster, the core logic remains the same: trigger on push, build the image, push to a registry, and signal the server to update.

Prerequisites

Before we dive into the YAML configuration, make sure you have the following ready:

Step 1: Configuring GitHub Secrets

You should never hardcode your passwords or API keys in your workflow files. I always use GitHub Secrets to keep my credentials secure. Navigate to Settings > Secrets and variables > Actions in your repository and add the following:

Step 2: Creating the Workflow File

GitHub Actions looks for YAML files in the .github/workflows directory. Create a file named deploy.yml and paste the following configuration. I’ve optimized this for speed by using the docker/build-push-action which supports advanced caching.

name: Docker Deployment

on:
  push:
    branches: [ "main" ]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{\n            secrets.DOCKERHUB_USERNAME
          }}
          password: ${{\n            secrets.DOCKERHUB_TOKEN
          }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_IP }}
          username: root
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            docker pull ${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 80:80 ${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest
GitHub Actions workflow run showing build and deploy jobs succeeding
GitHub Actions workflow run showing build and deploy jobs succeeding

As shown in the image below, the magic happens in the needs: build-and-push section, which ensures we don’t try to deploy a version of the app that failed to build.

Pro Tips for Production-Ready Deployments

Once you have the basic pipeline working, you’ll realize that :latest tags are dangerous in production. If a deployment fails, rolling back is a nightmare.

Troubleshooting Common Issues

In my experience, most GitHub Actions failures for Docker deployment fall into three categories:

1. SSH Authentication Failures

If the appleboy/ssh-action fails, check if your SSH_PRIVATE_KEY includes the -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- lines. Also, ensure your server’s authorized_keys contains the corresponding public key.

2. Docker Permission Denied

If you aren’t logging in as root, you might see permission errors. You can fix this by adding your user to the docker group on the server: sudo usermod -aG docker $USER.

3. Build Timeouts

If your build takes too long, you’re likely not leveraging Docker layers. Ensure your COPY package.json . happens before COPY . . to cache dependencies.

What’s Next?

Now that you have a working pipeline, you can expand it by adding automated testing steps before the build phase. Adding a npm test or pytest step ensures that broken code never even reaches the registry.

Ready to scale? Start implementing health checks in your Docker Compose files to ensure your app is fully booted before the load balancer sends traffic to it.