Integrating Docker Compose into GitHub Actions workflows represents a significant leap in CI/CD maturity, particularly for applications that rely on multi-service architectures. Traditionally, continuous integration pipelines struggled with the complexity of spinning up dependent services like databases, caches, and message queues. Docker Compose resolves this by allowing developers to define the entire application stack in a declarative format. When embedded within GitHub Actions, this capability enables the exact replication of local development environments within the ephemeral runners provided by GitHub. This approach ensures that integration tests, end-to-end (E2E) tests, and deployment scripts operate against a consistent, isolated infrastructure, eliminating the "it works on my machine" paradox. The ecosystem supports this through dedicated actions that handle setup, execution, and cleanup, as well as native shell commands that offer granular control over the container lifecycle.
Native Execution via Shell Commands
For developers who require maximum control or are migrating legacy workflows, executing Docker Compose directly through shell commands remains a robust and straightforward strategy. This method relies on the runner having the Docker Compose plugin installed, which is standard on GitHub's Ubuntu-based runners. A common pattern involves defining a workflow triggered by pushes to specific branches, such as main, features/**, or dependabot/**, as well as pull requests targeting the main branch.
In this configuration, the workflow begins by checking out the repository code. The critical step involves invoking the docker-compose command directly. To ensure the environment is ready for testing, the command typically includes flags to build the images from the current source code and start the containers in detached mode. This allows subsequent steps, such as installing Node.js dependencies or running test suites, to execute while the background services remain active.
To ensure resource efficiency and prevent runner saturation, it is imperative to tear down the containers after the tests conclude. This is achieved by running the docker-compose down command. Crucially, this cleanup step must be conditional on the job's completion status. By using the if: always() directive, the workflow guarantees that containers are stopped even if a previous test step fails. This prevents orphaned containers from lingering on the runner and ensures that the next job in the queue starts with a clean slate.
```yaml
name: Test
on:
push:
branches:
- main
- features/*
- dependabot/*
pull_request:
branches:
- main
jobs:
docker:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Start containers
run: docker-compose -f "docker-compose.yml" up -d --build
- name: Install node
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Install dependencies
run: npm install
- name: Run tests
run: npm run test
- name: Stop containers
if: always()
run: docker-compose -f "docker-compose.yml" down
```
This method is particularly effective for non-trivial applications, such as Django projects, where end-to-end testing requires a fully operational backend with associated databases. By committing a separate environment file, such as .env.github, developers can inject test-specific configuration variables into the container. This allows the CI environment to use private containers and replicate the exact service topology used in local development, providing a reliable testing ground without the overhead of complex build scripts.
Automated Setup with docker/setup-compose-action
To streamline the initialization phase, the docker/setup-compose-action provides a standardized way to install Docker Compose on the runner. This action checks the runner's existing installation; if Docker Compose is already present, it skips the download to save time. If it is missing, the action downloads and installs the latest stable version available on GitHub. This ensures that the workflow does not fail due to version mismatches or missing binaries.
Developers can force the installation of the latest version by specifying the version input as latest. Additionally, the action supports caching the compose binary to the GitHub Actions cache backend. This optimization reduces the download time in subsequent runs, accelerating the CI pipeline. The action is typically added as the first step in a workflow, preceding the execution of compose commands.
```yaml
name: ci
on:
push:
jobs:
compose:
runs-on: ubuntu-latest
steps:
- name: Set up Docker Compose
uses: docker/setup-compose-action@v2
- name: Set up Docker Compose Latest
uses: docker/setup-compose-action@v2
with:
version: latest
```
The configuration options for this action include the version string, which accepts specific version numbers like v2.32.4 or the keyword latest, and the cache-binary boolean, which defaults to true. By leveraging this action, teams can ensure a consistent Docker Compose version across all jobs, regardless of the underlying runner image updates.
Lifecycle Management with hoverkraft-tech/compose-action
For more advanced use cases, the hoverkraft-tech/compose-action offers a comprehensive solution that manages the entire lifecycle of the Docker Compose services within a single step. This action not only starts the services defined in the compose file but also handles the cleanup automatically when the step completes. This post-hook mechanism runs docker compose down to stop and remove the containers, ensuring that no resources are left hanging.
The action provides several inputs to customize its behavior. The compose-file input specifies the path to the compose file(s), supporting lists of files for complex configurations. Developers can pass additional options to the docker compose up command via the up-flags input. For instance, the --build flag can be passed to force a rebuild of the images, while the --remove-orphans flag might be useful for cleanup. Similarly, the down-flags input allows customization of the teardown process, such as adding flags to delete persistent volumes.
One of the most valuable features of this action is its logging capability. It captures the logs of the Docker Compose services and outputs them to the GitHub Actions log using the core.ts API. This is critical for debugging test failures, as it provides visibility into the output of background services. The log level can be adjusted using the services-log-level input, which defaults to debug. In debug mode, logs are printed only if debug mode is enabled in the runner environment.
```yaml
name: Docker Compose Action
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- name: Run docker compose
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
with:
compose-file: "./docker/docker-compose.yml"
- name: Execute tests in the running services
run: |
docker compose exec test-app pytest
```
The action also allows for targeted service execution. Instead of starting all services defined in the compose file, developers can specify a subset of services using the services input. This is useful when only a few services are required for a specific test suite. Additionally, environment variables can be injected into the step using the env context, making them available to the compose file during execution.
```yaml
steps:
- uses: actions/checkout@v3
- uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
with:
compose-file: "./docker/docker-compose.yml"
services: |
helloworld2
helloworld3
up-flags: "--build"
down-flags: "--volumes"
services-log-level: "info"
env:
CUSTOM_VARIABLE: "test"
```
Remote Deployment via SSH and SCP
Beyond testing, Docker Compose workflows in GitHub Actions can facilitate continuous deployment to remote servers. This process involves building the application image, pushing it to a container registry, and then updating the remote server to pull and run the new image. A typical deployment workflow begins when a user pushes to the main or master branch, creates a release, or manually triggers a dispatch.
The GitHub Action first retrieves necessary secrets, such as credentials for the GitHub Container Registry (GHCR). It then logs in to GHCR, builds the Docker image from the source code, and pushes it to the registry. Once the image is securely stored, the workflow uses the Secure Copy Protocol (SCP) to transfer updated configuration files, such as docker-compose.yml, docker-compose.prod.yml, and .env, to the remote Linux server.
Following the file transfer, the action establishes an SSH connection to the remote server. It then executes commands to update the application. This often involves running a specific service defined in the compose file, such as a migration service named theapp-migrate, to update the database schema. Finally, the action instructs the server to pull the new Docker image from GHCR and restart the application services. This sequence ensures that the deployment is atomic and repeatable, minimizing downtime and reducing the risk of human error.
Monitoring the health of these deployed containers can be facilitated by tools like LazyDocker, a terminal UI for Docker and Docker Compose. While not part of the GitHub Action itself, it serves as a valuable companion for developers managing the remote infrastructure post-deployment.
Conclusion
The integration of Docker Compose into GitHub Actions provides a powerful framework for managing complex application lifecycles. Whether through native shell commands for granular control, the docker/setup-compose-action for streamlined initialization, or the hoverkraft-tech/compose-action for automated lifecycle management, developers have a suite of tools to ensure consistency between local development, continuous integration, and production deployment. The ability to define the entire infrastructure in code, replicate it in ephemeral CI runners, and seamlessly push updates to remote servers via SSH represents a mature approach to modern software engineering. As containerization continues to dominate the landscape, mastering these workflows becomes essential for maintaining reliable, scalable, and maintainable applications.