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:
- A GitHub repository containing your application code.
- A
Dockerfilein the root of your project. - A Docker Hub account (or access to how to host a private docker registry if you prefer self-hosting).
- A target server with Docker and Docker Compose installed.
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:
DOCKERHUB_USERNAME: Your Docker Hub username.DOCKERHUB_TOKEN: A personal access token from Docker Hub.SSH_PRIVATE_KEY: The private key used to access your VPS.SERVER_IP: The public IP address of your deployment server.
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
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.
- Use Version Tags: Use the GitHub SHA (
${{ github.sha }}) as a tag. This creates an immutable history of every deployment. - Implement Blue-Green Deployment: To eliminate downtime, don’t just stop the container. I highly recommend checking out my blue green deployment github actions tutorial to learn how to switch traffic seamlessly.
- Multi-Site Management: If you’re scaling up, consider hosting multiple sites on one vps using docker with a reverse proxy like Nginx or Traefik to manage your routing.
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.