Modern software delivery pipelines have evolved from manual, error-prone processes to highly automated, repeatable workflows. At the heart of this transformation for containerized applications is the integration of GitHub Actions with Docker Compose. This architecture allows developers to define infrastructure and application services in code, triggering automated builds, security scans, and remote server deployments through a single, unified workflow. By leveraging official Docker GitHub Actions and secure SSH connections to remote Linux servers, teams can achieve a robust continuous integration and continuous deployment (CI/CD) system. This system ensures that applications are always running the latest code with minimal manual intervention, providing a reliable deployment mechanism for a broad range of web applications.
The core philosophy behind this approach is the separation of build and deployment concerns while maintaining a tight coupling between the source code and the running environment. When a developer pushes code to a main branch, creates a release, or manually triggers a workflow, the pipeline responds by building a new Docker image, pushing it to a registry, and then orchestrating the update on the target server. This process is not exclusive to a specific kind of application; the versatility of Docker allows this pipeline to adapt to any web application that can be containerized.
Foundational Components and Docker GitHub Actions
To establish a robust deployment pipeline, one must first understand the tools provided by Docker for integrating with GitHub Actions. Docker offers a suite of official GitHub Actions that serve as reusable, easy-to-use components for building, annotating, and pushing images. These actions provide a user-friendly interface while retaining the flexibility necessary for customizing build parameters.
The available actions include tools for every stage of the container lifecycle. The Build and push Docker images action utilizes BuildKit to efficiently construct and upload images. For more complex build scenarios, Docker Buildx Bake enables the use of high-level builds. Authentication is handled securely via Docker Login, which signs in to a Docker registry. To support advanced build features, Docker Setup Buildx creates and boots a BuildKit builder, while Docker Setup QEMU installs static binaries required for multi-platform builds. Metadata extraction is managed by the Docker Metadata action, which generates tags, labels, and annotations based on Git references and GitHub events. For local or remote orchestration, Docker Setup Compose installs and configures Compose, and Docker Setup Docker installs the Docker Engine itself. Finally, Docker Scout can be integrated to analyze Docker images for security vulnerabilities before they are deployed.
These actions form the backbone of the initial phase of the deployment workflow, where the application source code is transformed into a deployable artifact.
Prerequisites and Environment Setup
Before initiating the automation pipeline, the target environment and repository must be properly configured. The target server, typically a Linux machine, must have both Docker and Docker Compose installed. Without these, the server cannot interpret or execute the deployment commands sent from GitHub.
On the repository side, a GitHub repository must be set up containing the necessary Docker Compose files. This repository serves as the single source of truth for the application's infrastructure. Access to the target server with appropriate permissions is also a critical prerequisite. The GitHub Action runner will need the ability to log into this server and execute commands, which necessitates a secure authentication method.
Secure Access via SSH Key Generation
Security is paramount in CI/CD pipelines. To allow the GitHub Action runner to connect to the remote server, an SSH key pair must be generated. This key pair acts as the digital identity for the automation process, enabling secure, passwordless authentication.
The generation of this key pair is performed using the ssh-keygen utility. The recommended algorithm for modern systems is Ed25519, which provides strong security with compact key sizes. The command to generate the key pair is as follows:
bash
ssh-keygen -t ed25519 -f github_action_runner
When executed, this command initiates the creation of a public and private key. The system prompts for a passphrase, which can be left empty for automated processes, though encryption is recommended for higher security contexts. The output confirms the successful generation of the keys:
text
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in github_action_runner
Your public key has been saved in github_action_runner.pub
The key fingerprint is:
SHA256:1m2zuFb9MLPT7FaApQeRNtpmvtmrPm2tGiafmmvEZj0 [email protected]
The key's randomart image is:
+--[ED25519 256]--+
| .o |
| = . |
| + * |
| ...* o |
| S..=+o . |
| |
The private key (github_action_runner) must be stored securely as a GitHub Secret, while the public key (github_action_runner.pub) is added to the authorized_keys file on the target server. This setup ensures that only the GitHub Action runner can initiate SSH sessions to the server.
Configuration Files: docker-compose.yml and Overrides
The deployment logic relies heavily on Docker Compose configuration files. The primary file, docker-compose.yml, defines the services that make up the application, allowing them to run together in an isolated environment. In a typical .NET application deployment using ServiceStack, this file might define two key services: app and app-migration.
The app service represents the main web application. It is configured to build from the current directory, restart always, and expose port 5000 on the host to port 80 in the container. It also utilizes environment variables for domain configuration and Let's Encrypt integration.
yaml
version: "3.9"
services:
app:
build: .
restart: always
ports:
- "5000:80"
environment:
VIRTUAL_HOST: ${HOST_DOMAIN}
LETSENCRYPT_HOST: ${HOST_DOMAIN}
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
volumes:
- app-mydb:/app/App_Data
The app-migration service is a critical component for database management. It is designed as a one-off task that runs database migrations using ServiceStack AppTasks. This service uses the same build context but overrides the command to execute --AppTasks=migrate. It is assigned to a specific profile named migration to allow selective execution during the deployment process.
yaml
app-migration:
build: .
restart: "no"
profiles:
- migration
command: --AppTasks=migrate
volumes:
- app-mydb:/app/App_Data
For production environments, a separate docker-compose.prod.yml file is often used. This file tailors the configuration specifically for production, potentially altering resource limits, network settings, or image sources. During the deployment workflow, both the base docker-compose.yml and the production override file are transferred to the server.
Managing Secrets and Registry Authentication
GitHub Secrets are encrypted environment variables stored in the repository settings. They provide a secure method for storing sensitive information such as credentials, SSH keys, tokens, and registry passwords. For the deployment pipeline to function, several secrets must be configured:
DEPLOY_HOST: The IP address or domain name of the target Linux server.DEPLOY_USERNAME: The user account on the remote server with SSH access.DEPLOY_KEY: The private SSH key generated earlier.LETSENCRYPT_EMAIL: The email address for SSL certificate management.
These secrets are retrieved by the GitHub Action at runtime, ensuring that sensitive data is never exposed in the code or logs. Additionally, authentication to the GitHub Container Registry (GHCR) requires a personal access token or deploy key, which is also stored as a secret.
The Deployment Workflow Architecture
The deployment process is orchestrated by a GitHub Actions workflow, typically defined in a file like release.yml. This workflow is triggered by specific events: a new GitHub release, the completion of a build on the main or master branch, or a manual trigger for rollback or redeployment.
The workflow is divided into two primary jobs: push_to_registry and deploy_via_ssh.
- pushtoregistry: This job is responsible for building the Docker image from the repository's source code and pushing it to GitHub's container registry. It utilizes the Docker Login action to authenticate with GHCR and the Build and Push action to create the artifact.
- deployviassh: This job handles the remote deployment. It uses Secure Copy Protocol (SCP) to transfer the
docker-compose.yml,docker-compose.prod.yml, and.envfiles to the target Linux server.
Once the files are transferred, the GitHub Action logs into the remote server via SSH. It first executes the database migrations service defined in the Compose file. This step ensures that the database schema is updated to match the new version of the application before the application itself is restarted.
```bash
Conceptual step executed on remote server
docker compose --profile migration run --rm app-migration
```
After migrations are complete, the GitHub Action instructs the server to pull the new Docker image from GHCR and start the application using Docker Compose. This ensures that the running container is updated to the latest version.
```bash
Conceptual step executed on remote server
docker compose pull
docker compose up -d
```
This series of steps repeats each time a change triggers the GitHub Action, ensuring continuous integration and deployment. The process is designed to be flexible and repeatable, creating necessary directories and copying files automatically for subsequent deployments.
Monitoring and Maintenance
Post-deployment, monitoring the health of the containers is essential. Tools like LazyDocker provide a terminal user interface for both Docker and Docker Compose, allowing administrators to view logs, resource usage, and container status in real-time. This visibility is crucial for troubleshooting any issues that may arise during or after the automated deployment.
The entire pipeline—from code push to registry push to remote server execution—is designed to be robust. By leveraging GitHub Actions for automation, GitHub Secrets for security, and the GitHub Container Registry for storage, teams can create a powerful, end-to-end solution. This approach not only reduces manual effort but also increases reliability, as every deployment follows the exact same validated procedure.
Conclusion
The integration of GitHub Actions with Docker Compose via SSH represents a significant advancement in deployment automation. It bridges the gap between development and operations by providing a seamless, secure, and repeatable path from code commit to live production. By utilizing official Docker actions for building and pushing images, and SSH for remote orchestration, organizations can deploy any containerized web application with confidence. The use of separate migration services ensures database integrity, while the reliance on GitHub Secrets protects sensitive credentials. This architecture not only simplifies the deployment process but also enhances reliability, making it a cornerstone of modern DevOps practices. As containerization continues to evolve, such automated pipelines will remain essential for maintaining high-velocity, stable software delivery.