This feature lets you spin up arbitrary Docker containers that your GitLab CI jobs can then talk to, as if they were part of the same network. Think of it like having a temporary, on-demand database or caching layer specifically for the duration of your CI pipeline.
Imagine you’re running an integration test suite that needs to talk to a PostgreSQL database. Normally, you’d have to set up a PostgreSQL server somewhere, manage its lifecycle, and ensure your CI job can connect to it. With service containers, you declare PostgreSQL as a "service" in your .gitlab-ci.yml file, and GitLab automatically starts a PostgreSQL container for you before your job runs.
Here’s a snippet from a .gitlab-ci.yml demonstrating this:
services:
- postgres:13
test:
image: alpine:latest
script:
- apk add --no-cache postgresql-client
- psql -h postgres "dbname=postgres user=postgres" -c "SELECT 1;"
In this example:
services: - postgres:13tells GitLab to pull and run a Docker container taggedpostgres:13as a service.- The
testjob runs with analpine:latestimage. apk add --no-cache postgresql-clientinstalls the PostgreSQL client tools within the job’s container.psql -h postgres "dbname=postgres user=postgres" -c "SELECT 1;"attempts to connect to the PostgreSQL service. Notice we usepostgresas the hostname. GitLab DNS resolves thispostgreshostname to the IP address of the service container.
GitLab CI orchestrates the lifecycle of these service containers. When your job starts, GitLab spins up all declared services. When the job finishes (either successfully or with failure), GitLab tears down these service containers. This ensures a clean, isolated environment for each pipeline run.
The primary problem this solves is managing dependencies for your CI jobs. Many applications require external services to function, such as databases, caches, message queues, or even other microservices for integration testing. Manually setting these up for every CI run is tedious, error-prone, and inefficient. Service containers abstract this complexity away.
Internally, GitLab uses Docker (or your configured container runtime) to manage these services. It creates a dedicated Docker network for each job. The job’s main container and all its service containers are attached to this same network. This is why they can communicate using hostnames. GitLab sets up DNS entries within this network so that the service name (e.g., postgres) resolves to the correct IP address of the service container.
You can define multiple services. If you need both PostgreSQL and Redis for your tests, you’d list them both under services:
services:
- postgres:13
- redis:6
test:
image: python:3.9
script:
- pip install -r requirements.txt
- python manage.py test
In this case, both postgres and redis would be resolvable hostnames within the job’s network. You can even specify custom aliases for your services using a map syntax, which is useful if the default service name conflicts with something else or if you want to be more explicit:
services:
database:
image: postgres:13
alias: db
cache:
image: redis:6
alias: redis-cache
test:
image: ubuntu:latest
script:
- apt-get update && apt-get install -y postgresql-client redis-tools
- psql -h db "dbname=postgres user=postgres" -c "SELECT 1;"
- redis-cli -h redis-cache ping
Here, the PostgreSQL service is available at the hostname db, and Redis at redis-cache.
The service containers are started before your job’s script commands begin. They are stopped after your script commands complete. This lifecycle management is crucial for ensuring that your tests or build steps have access to these dependencies when they need them. If a service container fails to start, the job will typically fail immediately, indicating a problem with the service definition or the image itself.
A common point of confusion is how to pass credentials or configuration to service containers. While you can’t directly inject environment variables into the service container’s definition within the .gitlab-ci.yml, you can often rely on default credentials provided by the image (like postgres user with no password for postgres:13) or use Docker secrets if your GitLab runner is configured to support them. For more complex setups, you might pre-build a custom Docker image for your service that includes the necessary configurations.
The next step in managing external dependencies for your jobs involves exploring GitLab’s built-in caching mechanisms to speed up repeated downloads and builds, which complements the on-demand nature of service containers.