GitLab CI/CD: Resolving 'Connection Refused' to Service Containers

TechLab Editorial TeamPublished 4 views1 answer
0

Editorial Q&A · This problem and its answer were written by the TechLab editorial team to document a common issue. It isn't a question from a community member. If you've hit a variation of it or know a better approach, add a reply below.

A common challenge for professional developers configuring CI/CD pipelines in GitLab CI involves applications that depend on ephemeral services, such as databases or message brokers. A typical setup utilizes GitLab CI's services keyword to provision these dependencies for testing.

Consider a GitLab CI pipeline running on GitLab Runner version 16.x, where a test job uses a PostgreSQL 15.x service. The application's test suite, written in Python with psycopg2, attempts to establish a connection to the postgres service immediately upon job startup.

A minimal .gitlab-ci.yml configuration:

YAML
test_job:
  image: python:3.10-slim-bullseye
  services:
    - postgres:15-alpine
  script:
    - pip install psycopg2-binary
    - python -c "import psycopg2; conn = psycopg2.connect(host='postgres', user='postgres', password='mysecretpassword', dbname='testdb'); conn.close(); print('Connected successfully!')"

When this pipeline executes, the expected behavior is that the Python script successfully connects to the PostgreSQL service, allowing subsequent tests to run. However, the actual behavior is that the job fails with a connection error. The standard output often includes messages similar to this:

Code
psycopg2.OperationalError: connection to server at "postgres" (172.17.0.x), port 5432 failed: Connection refused

This occurs consistently because the database service container, while started by the GitLab Runner, has not fully initialized its internal PostgreSQL server process and begun listening for connections by the time the script command attempts to connect.

The question: How can a GitLab CI job be configured to reliably wait for a service container, like PostgreSQL, to be fully ready and accepting connections before the main script proceeds?

Advertisement

1 Answer

0

The root cause of "Connection refused" errors when interacting with service containers in GitLab CI pipelines stems from the asynchronous nature of service provisioning. When a job starts, GitLab Runner initiates the service containers (defined under the services keyword) concurrently with the execution of the job's script commands. While the service container itself might be running, its internal processes—like a PostgreSQL server, a Redis instance, or a web server—require additional time to fully initialize, bind to their respective ports, and become ready to accept connections. The application's main script attempts to connect before this internal initialization is complete, leading to the connection failure.

To reliably ensure a service is ready, the job's script must implement a "wait-for-it" mechanism. This involves repeatedly polling the service's network endpoint until it responds or a timeout is reached. A common and effective method is to use a utility like wait-for-it.sh or a simple bash loop employing netcat (nc) to check if the specific port on the service's hostname is open. For database services, an even more robust check involves attempting a lightweight connection and executing a trivial query, confirming not just network connectivity but also database readiness.

Here is the corrected .gitlab-ci.yml that incorporates a readiness check using wait-for-it.sh:

YAML
test_job:
  image: python:3.10-slim-bullseye
  services:
    - postgres:15-alpine
  variables:
    POSTGRES_DB: testdb
    POSTGRES_USER: postgres
    POSTGRES_PASSWORD: mysecretpassword
  before_script:
    - apt-get update && apt-get install -y netcat-traditional # Install netcat for wait-for-it.sh
    - wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh -O /usr/local/bin/wait-for-it.sh
    - chmod +x /usr/local/bin/wait-for-it.sh
    - /usr/local/bin/wait-for-it.sh postgres:5432 --timeout=60 # Wait for Postgres to be ready on port 5432
  script:
    - pip install psycopg2-binary
    - python -c "import psycopg2, os; conn = psycopg2.connect(host='postgres', user=os.getenv('POSTGRES_USER'), password=os.getenv('POSTGRES_PASSWORD'), dbname=os.getenv('POSTGRES_DB')); conn.close(); print('Connected successfully!')"

In this enhanced configuration:

  1. variables: Environment variables for PostgreSQL credentials are set directly in the .gitlab-ci.yml, which postgres:15-alpine uses to initialize the database. These are then accessed by the Python script using os.getenv().
  2. before_script:
    • apt-get install -y netcat-traditional: Installs netcat, a prerequisite for wait-for-it.sh to perform TCP port checks. For alpine images, this would be apk add netcat-traditional. For python:3.10-slim-bullseye, apt-get is correct.
    • wget ... wait-for-it.sh: Downloads the wait-for-it.sh utility script.
    • chmod +x: Makes the script executable.
    • /usr/local/bin/wait-for-it.sh postgres:5432 --timeout=60: This command instructs the job to repeatedly check if port 5432 on the postgres service hostname is open. It will wait for up to 60 seconds before failing the job.

Edge Cases and Pitfalls:

  • Service Hostname: Always use the service name as the hostname (e.g., postgres), not localhost or 127.0.0.1. GitLab Runner configures network aliases for services, making them discoverable by their image name or an explicitly defined alias.
  • Initialization Timeouts: Some services, especially databases or complex applications, might take longer than the default wait-for-it timeout (usually 15-30 seconds). Adjust --timeout accordingly (e.g., --timeout=120) if the service has extensive startup routines, such as restoring from a large backup.
  • Service-Specific Readiness Checks: While netcat checks TCP port availability, it doesn't guarantee the service is fully functional (e.g., a database might be listening but still recovering or running migration scripts). For critical applications, consider a service-specific check, like attempting a simple API call for a web service, or psql -h postgres -U postgres -c "SELECT 1;" for PostgreSQL, which verifies database responsiveness beyond just port listening.

💡 Bonus Tip: For debugging service startup issues or general pipeline problems, enable CI_DEBUG_TRACE: "true" in your .gitlab-ci.yml or as a CI/CD variable. This will output every executed command and its arguments, providing detailed insights into what the runner is doing, helping diagnose why a service might not be starting correctly or why a wait-for-it script might be failing prematurely.

TechLab Editorial TeamAuthorAnswered

Know the answer?

Posting answers needs a TechLab plan. Reading every discussion stays free.