# docker compose pulls every image on every GitHub Actions job: how to stop pulling the same images

> Each GitHub Actions job starts on a new VM with no Docker images, so docker compose up pulls Postgres, Redis and every other service image again. docker save with actions/cache only moves the download, inside GitHub's 10 GB. On a runner that keeps its disk the images are already there. Skipbox is that runner for a flat $39 a month: change one runs-on line.

`docker compose up` pulls every image on every GitHub Actions job because each job on a GitHub-hosted runner starts on a new VM. Postgres, Redis, the message broker, your own service images: none of them is on the disk, so Compose downloads and unpacks them all before the first test runs. The way to stop pulling the same images is to run the job on a machine that still has them.

Skipbox is that machine. Your jobs run on your account's own machine, which keeps Docker's images, layers and build cache between jobs. `docker compose up` finds what it pulled last time and starts the containers. Change one line, `runs-on: skipbox`, for a flat $39 a month with unlimited minutes.

- GitHub: a new VM per job, so every image in your compose file is pulled again, every job
- Skipbox: the images stay on your machine's disk, and only layers that changed are downloaded
- 20,000 minutes a month on 4 vCPU: $222 on GitHub, $39 on Skipbox

**GitHub Actions at a flat price, unlimited minutes** Skipbox runs your GitHub Actions jobs on your own dedicated machine: $39 a month, however many minutes you run. Docker layers and dependency caches stay between jobs, so the next job starts right away.

A 4 vCPU runner, 20,000 minutes a month: GitHub $222 → Skipbox $39. You save $183 every month.

Moving takes one line: `runs-on: skipbox`.

[Start my 7-day free trial](https://app.skipbox.dev/sign-in?ref=g-docker-compose-pull-slow-github-actions-en)

## Why every job pulls

- **A new VM per job.** GitHub's [runner reference](https://docs.github.com/en/actions/reference/runners/github-hosted-runners), read November 14, 2026: "each GitHub-hosted runner is a new virtual machine (VM) hosted by GitHub." Nothing the last job pulled is on it.
- **Compose pulls what's missing.** The default [`pull_policy`](https://docs.docker.com/reference/compose-file/services/) is `missing`: "Compose pulls the image only if it's not available in the platform cache." On a new VM, every image is missing.
- **The pull is download and unpack.** Each layer is downloaded, decompressed and written to the VM's disk. A private repository's `ubuntu-latest` has 2 CPUs and 14 GB of SSD to do it with.

So the time before your first test is the size of your compose file, paid in full on every push.

## What you can do on a GitHub-hosted runner

The options move the download; none removes it.

| Option | What each job still does | Limit |
|---|---|---|
| `docker save` + actions/cache + `docker load` | Downloads the archive, then loads every image into Docker | Counts in GitHub's 10 GB per repository, shared with every other cache |
| Smaller images (`-alpine`, `-slim`) | Pulls them all, faster | Only as far as each image has a small variant |
| Fewer services per job | Pulls fewer images | The tests that need the full stack still pull it |

Each is a way to make a forgetful machine forget less. The pull disappears only when the machine remembers.

## Skipbox keeps the images on your machine

Each plan comes with your account's own machine, kept between jobs:

- **Images stay on the disk.** `docker compose up` finds the images it pulled last time and goes straight to starting containers.
- **Only changes are downloaded.** `docker compose pull` fetches the layers that changed and reuses the rest.
- **Your own service images build from cache.** `docker compose build` reuses the last build's layers, with no `cache-from` or `cache-to`.
- **A clean start every job.** After every job, Skipbox removes every container and deletes the registry login (`~/.docker/config.json`). The next job starts its services fresh, from images already on disk.
- **Room for them.** The disk is 80 GB on S, against 14 GB on GitHub's runner. Nothing is deleted until the disk is nearly full after a job; then dangling images go first, then the build cache used longest ago.

The workflow doesn't change beyond its `runs-on`:

```yaml
jobs:
  integration:
    runs-on: skipbox # was ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker compose up -d --wait
      - run: npm ci && npm run test:integration
```

If you saved images with actions/cache and `docker load`, delete those steps. The first job on Skipbox pulls every image once. The second finds them on the machine.

A job picks its size by label:

| Label | Machine | Disk | Plan |
|---|---|---|---|
| `skipbox` | 4 vCPU · 16 GB | 80 GB | S, $39 a month |
| `skipbox-8x16` | 8 vCPU · 16 GB | 160 GB | M, $79 a month |
| `skipbox-16x32` | 16 vCPU · 32 GB | 320 GB | L, $149 a month |

## The same minutes, a flat price

Integration tests are the long jobs, so they are the ones a per-minute bill grows with. On Skipbox minutes are unlimited:

| Minutes a month (4 vCPU) | GitHub | Skipbox S | You save each month |
|---|---|---|---|
| 10,000 | $102 | $39 | **$63** |
| 20,000 | $222 | $39 | **$183** |
| 30,000 | $342 | $39 | **$303** |

GitHub's figures are after GitHub Team's included usage. Your own minutes go in the [calculator on the home page](https://skipbox.dev/?ref=g-docker-compose-pull-slow-github-actions-en#cost).

## Moving is one line

1. Sign in to Skipbox with GitHub.
2. Install the Skipbox GitHub App and pick the repositories.
3. Change `runs-on` on the integration test job and push.

Building images in the same workflow? [Docker layer cache in GitHub Actions](https://skipbox.dev/guides/docker-layer-cache-github-actions) covers `docker build` and buildx.

## Security

- The machine is your account's alone. No other company's jobs run on it.
- After every job, the runner's credentials, processes and containers are wiped, and so is `~/.docker/config.json`, so a registry login doesn't carry over to the next job. Clean every job gives each job a fresh machine instead.
- Code from people without write access (such as pull requests from forks) runs on a fresh, throwaway machine, never on yours.

The details are in [how jobs are isolated](https://docs.skipbox.dev/isolation).

## Try it on your integration tests

The trial is free for 7 days or 3,000 job-minutes, whichever comes first, and takes no card. Move the job to `runs-on: skipbox`, run it twice, and put the second run's `docker compose up` next to GitHub's.

## FAQ

### Our docker compose based integration tests in GitHub Actions spend most of the time pulling images. How can we avoid pulling the same images every job?

Run the job on a machine that keeps its disk. A GitHub-hosted runner is a new VM for every job, so no image survives and compose pulls them all again. On Skipbox the job runs on your account's own machine, which keeps Docker's images between jobs: docker compose up finds the images it pulled last time and starts the containers. The change is one line, runs-on: skipbox, for a flat $39 a month with unlimited minutes.

### Why does docker compose pull the images again in every GitHub Actions job?

Because Compose pulls an image when it isn't on the machine, and on a GitHub-hosted runner it never is. GitHub's runner reference says each GitHub-hosted runner is a new virtual machine; nothing the last job pulled is on it. Compose's default pull_policy, missing, would skip the pull if the image were there.

### Can I cache docker compose images with actions/cache?

You can, and it only moves the download. docker save writes the images to a tar file, actions/cache stores it, and the next job restores it and runs docker load. Each job still downloads the whole archive and loads it into Docker, and the archive counts in GitHub's 10 GB of cache per repository, shared with your dependency caches. On a machine that keeps its disk there is nothing to save or load.

### Does Skipbox keep Docker images between jobs?

Yes. Each plan comes with your account's own machine, kept between jobs, and it keeps Docker's images, layers and build cache on its disk: 80 GB on S, 160 GB on M and 320 GB on L. After every job the containers are removed and the registry login is deleted; the images stay.

### What happens when an image in my compose file changes?

Only the layers that changed are downloaded. Add docker compose pull before docker compose up when you want each job to check the registry: Docker fetches the new layers and reuses the ones already on the machine's disk. Images you build yourself with docker compose build reuse the last build's layers the same way.

### How much does it cost to try?

Nothing. The trial is 7 days or 3,000 job-minutes, whichever comes first, with no card. Move your integration test job to runs-on: skipbox, run it twice, and compare the second run's docker compose up with GitHub's.

Updated 2026-11-14 · https://skipbox.dev/guides/docker-compose-pull-slow-github-actions
