# When the customer's servers have no internet, your pipeline has to end in a tar file

> You need four things to get your build into a customer's isolated network: docker save, docker load, an internal registry and someone to sign off.

Original: https://fdetimes.net/en/guides/air-gapped-docker-deployment-tar-pipeline/

Your pipeline is green and the image is in the registry. Then the customer's operations engineer sends one line: the servers here can't reach the internet. The `build → push → pull` chain you rely on breaks at the last link.

Picture a deployment at a bank or a factory that runs an isolated internal network. GitLab's offline documentation says plainly that its guidance is for networks that are physically disconnected. At sites like these, there is no proxy to request and no firewall exception to ask for.

This guide shows you how to rebuild that last link on a laptop. You will package the image as a file, carry it into the isolated network, load it again inside and put an approval gate in the right place. At the end you will have a procedure you can take with you to a customer site.

## What you will build, and what you need

The result is a four-step procedure. You build the image on a connected machine, export it to a tar file, move that file into the customer's network and load it into a Docker daemon or an internal registry. To simulate "carrying it inside", you delete the image from your machine and load it back from the file.

You need Docker installed and an image to test with. The image for the application you are working on is best. If you want to try the runner section, you also need a GitHub repository. You don't need a cluster to follow along.

This works because of what a container is. As Docker describes it, a container packages code together with everything it depends on, so the application runs the same way whatever infrastructure sits underneath.

If you can't install anything from the internet at the customer's site, having everything inside the image stops being a convenience. You can't deploy without it.

## Step 1: Where should the pipeline stop?

IBM separates two concepts that are often treated as one. Continuous delivery automates packaging and handover once a change has passed integration testing. Continuous deployment goes further: every approved change goes to production automatically, and nobody has to step in.

At a customer site, especially one cut off from the internet, you will almost always stop at continuous delivery. Automate everything up to a clean artifact with a clear version. Putting that artifact into production is a decision someone signs off, usually someone on the customer's side.

**Key point:** On a customer's network, the pipeline produces the artifact automatically. A person decides when that artifact runs.

The check after this step is simple. You can point to exactly one place in the process where someone has to approve a release, and you know that person's name.

## Step 2: Package the image as a tar file

GitLab's offline procedure starts by packaging Docker images as tar files. Elastic uses the same pattern for offline installs of Elastic Cloud Enterprise: run `docker save -o` on a connected machine and name the tar file with the image's version number.

For your own application, the file name should carry the same version as the image. The example below uses a hypothetical name:

```bash
docker save -o myapp.1.0.0.tar myapp:1.0.0
```

Check that the tar file appears in the directory and that its size roughly matches the image. Put the version in both the tag and the file name. It is a small detail that matters: on the inside, the operator sees only the file name, not your commit history.

## Step 3: Carry it into the isolated network and load it

GitLab explains that these tar files can be moved elsewhere and loaded into a Docker daemon. On a laptop, you simulate this by deleting the local image and loading it again from the file. The syntax below is the common form, but check it against `docker load --help` on your machine:

```bash
docker load -i myapp.1.0.0.tar
```

Check that `myapp:1.0.0` appears in the image list again and that the container starts without the machine pulling anything from the network. To be more certain, turn off wifi before you run the container. If it still starts, your image really does not depend on the network.

## Step 4: Load onto each host, or into a registry?

Many people only face this architecture decision once they are already on site. Elastic states that without a private registry you have to put the image on every host, one by one. GitLab ends its procedure by loading images into an offline registry, so the internal registry becomes the point of distribution.

Imagine a customer with four hosts running the application. Without a registry, every release means copying the tar file to all four machines and running `docker load` four times. Miss one machine and your four hosts are running two different versions. With an internal registry, you load once and all four hosts pull from the same source.

| Aspect | No internal registry | Offline registry |
|---|---|---|
| Loads per release | One per host | One into the registry |
| Risk of version drift | High, easy to miss a host | Low, single source |
| Suits | Few hosts, early trials | Many hosts, regular releases |

With a registry, the work inside the customer's network usually takes three operations: load the tar file onto a staging machine, retag the image with the internal registry's address, then push it.

The example below is simplified, and the address `registry.internal:5000` is hypothetical. Check `docker tag --help` and `docker push --help`, and ask the customer for the real registry's address and authentication method:

```bash
# On the staging machine inside the customer's network (simplified)
docker load -i myapp.1.0.0.tar
docker tag myapp:1.0.0 registry.internal:5000/myapp:1.0.0
docker push registry.internal:5000/myapp:1.0.0
```

After that, each host pulls the image from the internal registry rather than receiving its own tar file:

```bash
# On each host (simplified)
docker pull registry.internal:5000/myapp:1.0.0
```

Check that all four hosts report the same `1.0.0` tag when you list images, and that none of them needs the tar file on its own disk. At your first working session with the customer, ask straight away whether they have an internal registry. The answer determines how you write the runbook.

## When to put a runner inside the customer's network

Not every site is completely cut off. At some, internal machines can still reach certain services. In that case, GitHub Actions lets you host a runner on your own machine and customise the environment its jobs run in. Jobs can then run on a machine that already sits inside the customer's network, while the workflow is still managed in the repository.

The skeleton below is simplified to illustrate the idea. It is not a complete configuration, so check the syntax against the GitHub Actions documentation before using it:

```yaml
# Simplified: job runs on a self-hosted runner inside the customer's network
jobs:
  load-release:
    runs-on: self-hosted
    steps:
      - run: docker load -i myapp.1.0.0.tar
```

On a physically disconnected network, the runner cannot reach GitHub either, so you are back to steps 2 and 3.

## Common mistakes

The first mistake is using the `latest` tag. Once the tar file is inside the isolated network, nobody can tell how this week's `latest` differs from last week's. Always use a specific version.

The second is an image that still depends on the network, such as a container that downloads models, packages or fonts from the internet at startup. The wifi-off test in step 3 is there to catch this, and you should run it before you leave for the airport, not after you arrive.

The third is forgetting a host. If the customer has no registry, list the hosts in the runbook and tick off each machine once it has been loaded.

## How to put this skill on your CV

When you read job descriptions for FDE roles, look for words such as "on-prem", "air-gapped", "customer environment" or "self-hosted". They are a fairly clear sign that the company sells into places where the procedure above is not theoretical.

On your CV, don't just write "experienced with CI/CD". Describe one specific occasion: what you packaged, where you moved it, how you loaded it and how you checked it afterwards. A story like that proves something a list of tools cannot: you have got software running on infrastructure you don't control.

Even the best pipeline on GitHub is only worth something if the build runs on the customer's machines. At sites without internet, a correctly named tar file, a list of hosts and someone to sign off often matter more than any YAML file.

**Try this week:**

- Pick an image you use, run docker save -o to export it to a tar file, delete the image from your machine, then docker load it again and start a container.
- Write a one-page runbook for moving a build into an offline network: file name, version, who approves it, and which hosts or registry it is loaded into.
- Add a line to your CV describing a specific time you delivered software into an environment without internet access, including how you checked it after loading.

## Sources

- [What is CI/CD? (IBM)](https://www.ibm.com/think/topics/ci-cd)

- [What is a Container? (Docker)](https://www.docker.com/resources/what-container/)

- [GitHub Actions documentation](https://docs.github.com/en/actions)

- [Offline environments (GitLab Docs)](https://docs.gitlab.com/user/application_security/offline_deployments/)

- [Install ECE offline without a private Docker registry (Elastic)](https://www.elastic.co/docs/deploy-manage/deploy/cloud-enterprise/ece-install-offline-no-registry)
