Can I Run Docker Compose in GitHub Actions?

Yes, on a Linux runner. Docker Compose v2 ships in the Ubuntu runner images, and stacks fail on runner memory and CPU rather than on compose itself.

Last verified:

Yes. Docker Compose v2 is preinstalled on the Ubuntu runner images (Ubuntu 24.04 image manifest), so docker compose up runs in a job on any Linux GitHub Actions runner with no setup action in front of it. The thing that usually breaks is resource pressure rather than compose itself: five containers and a test process starting at once on a 2 vCPU runner with 8GB of memory exhaust the memory budget or miss their health check deadlines, and both failures read as flaky tests in the report.

Answer

Compose needs a Docker daemon on the runner, which decides the platform question. The cloud runners documentation states that macOS runners do not support nested virtualization and cannot run Docker, so a compose stack has nothing to connect to there. Windows runners carry a daemon that runs Windows containers, which does not help a compose file written against Linux images. Compose work belongs on the Linux labels listed in the Linux x64 runner catalog.

The second decision is compose against the built-in services key. GitHub's service containers start stock images alongside the job and map their ports, which covers a job that wants Postgres and Redis and nothing more. Compose earns its place when the file already exists in the repository for local development, when services depend on each other in an order you have to declare, or when the stack includes your own application image. The service containers guide works through the split in detail.

Here is the shape that works, with the pull timed on its own, startup gated on health rather than on sleep, and teardown in an always() block so a failed suite still releases the volumes:

name: compose
on: [pull_request]

jobs:
  stack:
    runs-on: warp-ubuntu-latest-x64-8x
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v5

      - name: Log in to the registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Pull images
        run: docker compose -f compose.ci.yaml pull --quiet

      - name: Start the stack
        run: docker compose -f compose.ci.yaml up -d --wait --wait-timeout 300

      - name: Run tests
        run: npm run test:integration

      - name: Dump logs on failure
        if: failure()
        run: docker compose -f compose.ci.yaml logs --no-color --tail=200

      - name: Tear down
        if: always()
        run: docker compose -f compose.ci.yaml down -v

--wait blocks until every service reporting a health check is healthy, and --wait-timeout is expressed in seconds (docker compose up reference). Services with no healthcheck block count as ready the moment the container starts, which for Postgres is several seconds before it accepts connections, so each service in the compose file needs its own healthcheck definition for --wait to mean anything.

Detail

Sizing the runner against the stack

Memory is the binding constraint, because every container holds its working set at the same time. Add the peak resident memory of each container, the test process, and headroom for the Docker daemon and the page cache the database depends on, then take the first row that clears the total. Rates come from the pricing page and the cloud runners documentation, checked on 2026-08-13.

Stack sizeTypical shapeRunner labelvCPUMemoryPer minute
1 to 2 servicesOne database, one cachewarp-ubuntu-latest-x64-2x28GB$0.004
3 servicesDatabase, cache, applicationwarp-ubuntu-latest-x64-4x416GB$0.008
5 to 6 servicesAdds a broker and a search nodewarp-ubuntu-latest-x64-8x832GB$0.016
7 or more servicesAdds a browser or a second JVM servicewarp-ubuntu-latest-x64-16x1664GB$0.032
Several stacks in one jobParallel compose projects on separate portswarp-ubuntu-latest-x64-32x32128GB$0.064

Every Linux x64 size carries 150GB SSD, so disk rarely decides the row. Read your own numbers with docker stats --no-stream at the end of a local run instead of trusting the shapes above. The same ladder exists on ARM64 from $0.003 to $0.048 per minute when the stack images are multi-architecture, per the cloud runners documentation.

CPU matters in one specific way. Health checks are processes competing for the same cores as the services they probe, so a saturated 2 vCPU runner produces probe timeouts, compose retries, and a stack that reports unhealthy for reasons that look like application bugs.

What the size costs against GitHub list prices

warp-ubuntu-latest-x64-8x (8 vCPU, 32 GB) costs $0.016 per minute against $0.022 per minute for the 8-core Linux larger runner (8 vCPU, 32 GB), which is 27 percent lower list price. GitHub list price checked on 2026-08-13 in the GitHub Actions billing reference, with shapes from the GitHub-hosted runner specifications.

At 600 pull request runs per month and 9 minutes per run, that is 5,400 minutes: $86.40 on the WarpBuild 8x label against $118.80 at GitHub's 8-core rate.

When the stack builds its own images

A build: section in the compose file turns the job into a build job. Every layer is built cold on a fresh runner, the build competes with the containers already running, and the test suite waits behind it.

The alternative is to keep the image build off the runner. Build and push it on a remote Docker builder, which holds a persistent layer cache across runs, then let compose reference the pushed tag and pull it like any other image (Docker builders documentation):

      - name: Build and push the application image
        uses: Warpbuilds/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}/api:${{ github.sha }}
          profile-name: "compose-stack"

      - name: Start the stack
        env:
          API_TAG: ${{ github.sha }}
        run: docker compose -f compose.ci.yaml up -d --wait --wait-timeout 300

The compose file reads that tag with image: ghcr.io/acme/api:${API_TAG:-main} and drops its build: section entirely. Builder profiles are billed per session on top of the runner, starting at $0.06 per minute for the 16 vCPU, 32GB, 100GB disk profile (Docker builders documentation), so the two resources are counted separately. Docker builds on GitHub Actions covers the builder route end to end.

Teardown that survives a failure

down -v in an always() step removes the containers and the named volumes whatever the suite did. That matters on any runner where the disk outlives the job, and it matters for the failure step ordering above: dump logs before teardown, because the containers stop existing after it. Docker Compose integration tests on GitHub Actions covers seeding, digest pinning, and carrying prepared images between runs.

Does Docker Compose work on macOS or Windows GitHub Actions runners?

Not on macOS. WarpBuild macOS runners do not support nested virtualization and cannot run Docker, so a compose stack has no daemon to talk to (cloud runners documentation). Windows runners have a Docker daemon that runs Windows containers, so a compose file built on Linux images does not start there. Keep compose stacks on the Linux labels in the Linux x64 runner catalog.

Should I use Docker Compose or the GitHub Actions services key?

Use the services key when the job needs a few stock backing services on their own ports and nothing else. Use compose when the file already exists in the repository, when services depend on each other in a defined order, or when developers run the same stack locally and you want one definition for both. The service containers guide has the comparison, and Docker builds on GitHub Actions covers building your own application image once the stack needs one.

Why does my compose stack die with exit code 137?

The kernel out-of-memory killer stopped a container because the stack asked for more memory than the runner has. Sum the peak memory of every container plus the test process, then move up the size ladder until the total clears with headroom. A five service stack near 10GB needs warp-ubuntu-latest-x64-8x with 32GB, priced on the pricing page. Docker Compose integration tests on GitHub Actions covers the seeding and digest pinning patterns that keep a stack like this reproducible.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.