Stackvora.tech

Deploy to Production via GitHub Actions

Automating production deployments is a critical component of a robust CI/CD strategy, ensuring consistent and repeatable releases. GitHub Actions provides…

Automating production deployments is a critical component of a robust CI/CD strategy, ensuring consistent and repeatable releases. GitHub Actions provides a powerful, integrated platform for defining workflows that can automate everything from testing to deployment. This article details building a secure and efficient GitHub Actions workflow for deploying applications to a production environment, covering common deployment patterns, security considerations, and best practices.

Understanding the Deployment Workflow Structure

A typical GitHub Actions deployment workflow is defined in a YAML file within your repository's .github/workflows/ directory. The workflow responds to specific events, such as pushes to the main branch, and executes a series of jobs, each comprising multiple steps. The provided stub outlines a common pattern: checking out the code, SSHing into a server, and executing deployment commands.

name: Deploy to Production

on:
  push:
    branches:
      - main
  workflow_dispatch: # Allows manual triggering

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: Production # Link to an environment for protection rules and secrets
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Needed for git-based deployment strategies if tags/history are required

      - name: Setup SSH agent and deploy
        uses: webfactory/ssh-agent@v0.8.0 # More robust SSH agent forwarding
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Execute deployment script on remote host
        run: |
          ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
            cd /srv/my-app
            echo "Pulling latest code..."
            git pull origin main
            echo "Rebuilding and restarting services..."
            docker compose -f docker-compose.prod.yml up -d --build --force-recreate
            echo "Deployment complete!"
          EOF

Workflow Triggers and Environments

  • on: push: branches: [main]: This is the primary trigger, initiating a deployment whenever changes are pushed to the main branch.
  • on: workflow_dispatch:: Adding this trigger allows manual execution of the workflow from the GitHub Actions UI, useful for re-deployments or hotfixes.
  • environment: Production: This is crucial for production deployments. GitHub Environments allow you to define protection rules (e.g., manual approval, specific branch requirements) and store environment-specific secrets, enhancing security and control.

SSH-based Deployment Strategies

The core of the deployment involves securely connecting to a remote server and executing commands. The appleboy/ssh-action and webfactory/ssh-agent actions are popular choices.

appleboy/ssh-action@v1

This action simplifies SSH command execution. It handles key management internally. While convenient, some find its execution model (passing the entire script as a string) less flexible for complex multi-line scripts with conditional logic.

  • host, username, key: These parameters are mandatory and should be stored as GitHub Actions secrets for security.
  • script: The commands to be executed on the remote host. Note the use of 'EOF' in the enhanced example to prevent shell variable expansion in the GitHub Actions runner before reaching the remote host.

webfactory/ssh-agent@v0.8.0

This action is generally preferred for more complex scenarios as it sets up an actual SSH agent within the runner, allowing subsequent ssh commands to use the forwarded key. This is particularly useful if your deployment process involves multiple SSH hops or interactions with other SSH-secured services.

  • ssh-private-key: The private key to add to the SSH agent. Again, stored as a secret.
  • Subsequent run step: After the agent is set up, you can use standard ssh commands. The -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null options are commonly used in CI/CD environments to bypass interactive host key verification, but in a production setup, consider pre-populating known_hosts or using a more secure method if possible.

Securing Your Deployment Workflow

Security is paramount for production deployments.

GitHub Secrets

All sensitive information (SSH keys, API tokens, database credentials) MUST be stored as GitHub Secrets. Never hardcode them in your workflow files.

  • Repository Secrets: Accessible by all workflows in the repository.
  • Environment Secrets: Tied to a specific environment (e.g., Production) and only accessible by jobs targeting that environment. This is the recommended approach for production credentials.
  • Organization Secrets: Accessible by all repositories within an organization.

Example secrets:

  • SSH_PRIVATE_KEY: The private part of your SSH key pair (e.g., ~/.ssh/id_rsa content).
  • SSH_USER: The username on your production server (e.g., deployuser).
  • SSH_HOST: The IP address or hostname of your production server.

Generate SSH key pairs specifically for your deployment user. On your production server, add the public key to /home/deployuser/.ssh/authorized_keys. Ensure this user has only the necessary permissions to perform deployments (e.g., read/write access to application directories, permissions to restart services).

GitHub Environments

Environments provide an additional layer of security and control:

  • Deployment Protection Rules: Require manual approval for deployments to production.
  • Environment Secrets: Segregate production secrets from development or staging.
  • Deployment Branch Policies: Restrict which branches can deploy to a specific environment (e.g., only main can deploy to Production).

To configure an environment, go to Repository Settings > Environments > New environment. Name it Production and configure any desired protection rules and secrets.

Deployment Patterns and Best Practices

Docker Compose Deployments

The provided stub uses docker compose up -d --build. This is a common pattern for containerized applications.

  • -f docker-compose.prod.yml: Use a dedicated production Docker Compose file. This allows for environment-specific configurations like optimized image tags, resource limits, and different network settings compared to development.
  • --build: Rebuilds images if their definitions or context have changed. This ensures the latest code is reflected in the new image. Consider pre-building and pushing images to a container registry (e.g., Docker Hub, GitHub Container Registry) in a separate CI job for faster deployments and better caching.
  • --force-recreate: Forces recreation of containers, ensuring new images are used and old containers are replaced.
  • -d: Detached mode, runs containers in the background.

Zero-Downtime Deployments

For critical applications, a simple docker compose up might incur a brief downtime. Consider these advanced strategies:

  • Blue/Green Deployment: Maintain two identical production environments ("Blue" and "Green"). Deploy to the inactive environment (Green), thoroughly test it, and then switch traffic (e.g., via a load balancer update) to the new environment. If issues arise, switch back to Blue.
  • Canary Deployment: Gradually roll out the new version to a small subset of users (canaries), monitor for errors, and then progressively deploy to the rest of the user base.
  • Rolling Updates (Orchestrators): Container orchestrators like Kubernetes or Docker Swarm provide built-in rolling update capabilities, replacing old containers with new ones one by one, ensuring continuous availability.

Implementing these typically requires more sophisticated deployment scripts and infrastructure (load balancers, orchestrators) than a single-server docker compose setup.

Rollback Strategy

Always have a plan for rolling back a failed deployment. For git pull based deployments, a rollback might involve:

# On remote host after failed deployment
cd /srv/my-app
git reset --hard HEAD~1 # Go back to previous commit
docker compose -f docker-compose.prod.yml up -d --build --force-recreate

For containerized applications with versioned images, a rollback means reverting to a previous, known-good image tag.

Common Pitfalls and Troubleshooting

  • SSH Key Permissions: Ensure the private key used in secrets.SSH_PRIVATE_KEY has no passphrase or that the SSH action supports it. On the remote server, /home/deployuser/.ssh and /home/deployuser/.ssh/authorized_keys must have correct permissions (e.g., 700 and 600 respectively) to prevent SSH login failures.
  • Host Key Verification: Using -o StrictHostKeyChecking=no bypasses host key verification. While convenient for CI/CD, it weakens security. A more secure alternative is to pre-populate the GitHub Actions runner's known_hosts file or manually add the server's public key as a secret and configure ssh-agent to use it.
  • Docker Compose File Location/Name: Ensure the docker compose command specifies the correct filename (e.g., docker-compose.prod.yml) and is executed from the directory containing that file.
  • Remote Environment Variables: If your application relies on environment variables, ensure they are correctly set on the remote server (e.g., in a .env file or passed directly to Docker Compose services).
  • Permissions on Remote Server: The deployuser must have sufficient permissions to pull git repositories, build Docker images, and manage Docker containers/services. Misconfigured permissions (e.g., not being in the docker group) are a frequent cause of deployment failures.
  • Caching Issues: If you're encountering old code after a successful deploy, verify that Docker build caches are being invalidated correctly (--no-cache for docker build, or ensuring layers are changed).

Back to the knowledge base · Ask the AI assistant