Task: Set up a local docker registry on the server

Set up a local docker registry on the server

10.06.2026

Building the app container requires 3+ gigs of RAM, and in general, it takes quite a long time to complete.

We need to set it up so that the image can be built locally on the computer and pushed to the registry.

Ворклоги

Running our own registry on the server

This will allow us to have a local image repository on the server so that we can push our own images to it and create and run final containers from them.


docker run -d \
  -p 127.0.0.1:5000:5000 \
  --restart always \
  --name registry \
  registry:2

127.0.0.1:5000:5000 is so that it listens on port 5000 for local connections only; otherwise, regardless of the firewall, the port would be exposed to the outside, and we really don't want to bother with access settings unnecessarily. We will use a reverse proxy here, so access will be local only.

--restart always ensures that the container automatically restarts, meaning it runs continuously.

--name registry is simply a name to make it easier to see in the container list using docker ps.

registry:2 is the image name from Docker Hub.

Building our image locally

Of course, final docker-compose projects can vary greatly, so the image build command might differ, but my task concerns my agent https://github.com/haih-net/agent/tree/main/docker and I have several configs there. For production, these two configs are needed:

docker compose -f docker-compose.yml -f docker-compose.prod.yml build app

If everything went OK and the image built successfully, you should see something like this at the end:

[+] Building 2/2lt c6cfb7caa875
 ✔ Service app  Built                                                                                                   269.2s 
 ✔ app          Built    

What's important for us here is the hash c6cfb7caa875. Let's look at some information about it.

docker inspect c6cfb7caa875 | head -10
[
    {
        "Id": "sha256:c6cfb7caa875cf984db7fcf13bc6a886e92ddbe091f1c824a5aa871ab711bd59",
        "RepoTags": [
            "fi1osof-ru-agent-app:latest"
        ],
        "RepoDigests": [],
        "Parent": "sha256:96a5516e1319785787c218b1f04415763e20ea4b135e906d1b4138d0b7d82ae9",
        "Comment": "",
        "Created": "2026-06-10T21:41:51.224372504Z",

Right here we have RepoTags fi1osof-ru-agent-app:latest, which is what I will use and commit.

docker tag fi1osof-ru-agent-app:latest localhost:5000/fi1osof-agent-app:latest

This is necessary so Docker knows where to push the image. But I'm not 100% sure. I followed the instructions and don't fully understand when certain things are required, but logically it needs this.

Pushing the image from the local machine to the server registry

This is where a bit of magic begins, because our push command is:

docker push localhost:5000/fi1osof-agent-app:latest

But right now, if we run it, we get this:

The push refers to repository [localhost:5000/fi1osof-agent-app]
Get "http://localhost:5000/v2/": dial tcp 127.0.0.1:5000: connect: connection refused

This is because we are pushing to a local port, and naturally, we don't have the registry running here—it's on the server. Why is that, and what can we do to fix it? As I mentioned above, I don't want to mess with public access, so a simple hack is used—an SSH tunnel. This is a connection where a local port is linked to a target server port via the SSH protocol. Essentially, once such a connection is established, our local port becomes a window into the target server's port.

Here is the command. Just run it in a separate terminal tab, as it won't be a background process and will occupy the tab.

ssh -L 5000:localhost:5000 root@example.com

Instead of example.com, of course, specify your server or IP, and of course, SSH access must be enabled on the server.

If everything is fine, you will see that you have logged into the server via SSH, just like normal. But the difference is that you should now have port 5000 accessible locally. Let's check that it works and the registry is responding.

ssh -L 5000:localhost:5000 root@fi1osof.ru

If everything is OK, at least an empty object {} will be returned.

Now you can try pushing your image again. If all is well, requests will start flowing :-). Of course, this will take some time, especially if the image is several gigabytes large.

Downloading the image on the server

The easiest way is to add the image source to the compose file.

# git diff
diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml
index 72c18f8..9b8c69f 100644
--- a/docker/docker-compose.prod.yml
+++ b/docker/docker-compose.prod.yml
@@ -8,6 +8,7 @@ services:
       - 443:443
 
   app:
+    image: localhost:5000/fi1osof-agent-app:latest
     restart: unless-stopped
     build:
       args:

Right now this is hardcoded on the server, I'll need to think about the proper way to do it later.

After that, you can download it directly via compose.

docker compose -f docker-compose.yml -f docker-compose.prod.yml pull app

By the way, a nice side benefit is that updates build and roll out very quickly afterward, because there is a build cache, and some of the layers are already in the registry, so only a part of the project is uploaded.

docker push localhost:5000/fi1osof-agent-app:latest
The push refers to repository [localhost:5000/fi1osof-agent-app]
e9a428849d13: Pushed 
dc967676af36: Pushed 
3ad5267c7022: Pushed 
c74007ef1379: Layer already exists 
4aac218d60da: Layer already exists 
e57f06c69e56: Layer already exists 
aa40e465aa3b: Layer already exists 
f89beb6cb85b: Layer already exists 
4015bc6fbe7e: Layer already exists 
b06d6a345ba0: Layer already exists 
e75204ff1bca: Layer already exists 
f9093a7aaa16: Layer already exists 
1c49688bd8eb: Layer already exists 
f5b8fb1def00: Layer already exists 
8f003894a7ef: Layer already exists 
latest: digest: sha256:6c8820f4c3301f4160748cdb38e911e2f68f7fa701864048c605c8c4384f1949 size: 3483