GitHub Actions can run multiple services alongside your main job, making it a powerful CI/CD tool for applications that depend on external databases or caches.
Imagine you’re testing a web application that relies on both a PostgreSQL database and a Redis cache. Your GitHub Actions workflow needs to spin up these dependencies, run your tests against them, and then shut them down cleanly. Here’s how you’d set that up:
name: Test with Dependencies
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:6
ports:
- 6379:6379
options: >-
--health-cmd redis-cli ping
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
env:
DATABASE_URL: postgresql://testuser:testpassword@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
run: pytest
In this example, the services block defines two containers: postgres and redis. Each service is specified with an image, environment variables (env), and the ports it exposes (ports). The options field is crucial for defining health checks. GitHub Actions waits for these health checks to pass before considering a service ready, preventing your job from trying to connect to an uninitialized dependency.
The postgres service uses a standard PostgreSQL Docker image. We set POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB to ensure the database is initialized with a known configuration. The ports map 5432 on the host to 5432 in the container. The health check command pg_isready is a utility provided by PostgreSQL to check if the server is ready to accept connections.
The redis service uses a Redis Docker image. Similar to PostgreSQL, we expose port 6379. The health check command redis-cli ping is used to verify Redis responsiveness.
Inside your job steps, you can then refer to these services using localhost and the specified port. For instance, the DATABASE_URL and REDIS_URL environment variables in the "Run tests" step are configured to connect to these running services. Your application code, or test suite, would then use these URLs to interact with PostgreSQL and Redis.
The options block for health checks is highly configurable. You can adjust health-interval, health-timeout, and health-retries to suit the startup time and stability requirements of your services. For more complex services, you might need to run a script within the container that performs a more thorough readiness check.
The "magic" here is that GitHub Actions, when using the services keyword, provisions these Docker containers on the runner before your job steps begin. It monitors their health and only proceeds once they are deemed ready. When the job completes (or fails), these containers are automatically stopped and cleaned up. This isolation ensures that each workflow run starts with a clean slate, free from the state of previous runs.
Most people don’t realize that the services block also supports setting volumes for services, allowing you to persist data between runs of the same job if needed, though for typical CI testing, this is usually undesirable. You can also define multiple instances of the same service type if your application requires it, by giving them distinct names in the services block.
This setup allows you to run virtually any service that can be containerized, from databases like MySQL and MongoDB to message queues like RabbitMQ and Kafka, directly within your GitHub Actions workflows, significantly enhancing your testing capabilities.
The next hurdle you’ll likely encounter is managing more complex network configurations or dealing with services that require specific initialization scripts beyond simple environment variables.