Skip to main content

Command Palette

Search for a command to run...

Running a Private Docker Registry Behind a Tunnel with TCP Passthrough

Updated
4 min read
Running a Private Docker Registry Behind a Tunnel with TCP Passthrough
S

DevOps & Cloud Engineer — building scalable, automated, and intelligent systems. Developer of sorts | Automator | Innovator

Managing Docker images privately is crucial for projects that need security, control, and faster deployments. In this tutorial, we’ll go through setting up a private Docker registry behind a remote tunnel , using a TCP passthrough for secure communication, and pushing/pulling Docker images with a sample Flask app.

We will use the domain registry.nyzex.in (with port 5000) as our private registry.

1. Environment Overview

  • VM Hosting Docker Registry: Behind a Pangolin tunnel, which exposes internal IPs externally.

  • Problem: Docker registry standard HTTP/HTTPS cannot easily work through HTTP proxy because Docker client uses HTTPS by default, and certificate management is complicated when going through a tunnel.

  • Solution: Use TCP passthrough on the tunnel. This allows Docker to talk directly to the registry over TCP without interference from HTTP-level proxies.

  • Result: You can tag/push images using registry.nyzex.in:5000 even though the VM is behind a tunnel.


2. Sample Flask App

We will use a minimal Flask app for demonstration.

File: app.py

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello from private Docker registry demo!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

3. Dockerfile

Containerize the Flask app:

File: Dockerfile

# Use lightweight Python image
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Install dependencies
RUN pip install --no-cache-dir flask gunicorn

# Copy app into container
COPY app.py .

# Expose the port
EXPOSE 5000

# Run the app with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

4. Run the Private Docker Registry

On the VM behind the Pangolin tunnel:

docker run -d --restart=always --name registry \
  -p 5000:5000 \
  -v /data/registry:/var/lib/registry \
  registry:2

Check the container:

docker ps

You should see something like:

CONTAINER ID   IMAGE        PORTS
d5fb2585a79e   registry:2   0.0.0.0:5000->5000/tcp

5. Configure Pangolin Tunnel

  • Set Protocol: TCP for the domain registry.nyzex.in.

  • Forward external port 5000 to the VM internal IP/port (e.g., 192.168.29.73:5000).

  • Do not use HTTP/HTTPS proxy, as Docker cannot handle HTTPS termination through HTTP proxies easily.

This allows Docker to connect directly through TCP passthrough. I am still working on finding a way to enable HTTPs as I didnt want to registry to have :5000, it didn’t work even when I edited the daemon.json, so we shall get to it on a later post!

To learn how to setup a pangolin tunnel and a VM behind it, you can read my blog:

https://blog.nyzex.in/self-hosting-pangolin-newt-on-your-own-server


6. Build and Tag the Flask App Image

Build the image:

docker build -t sample-app:latest .

Tag it for your private registry:

docker tag sample-app:latest registry.nyzex.in:5000/sample-app:latest

7. Push Image to Private Registry

Push the tagged image:

docker push registry.nyzex.in:5000/sample-app:latest

Expected output:

The push refers to repository [registry.nyzex.in:5000/sample-app]
a93f0efed1fd: Pushed
ed6ad9f11d83: Pushed
...
latest: digest: sha256:2d7a49862a5ebe625225642f809f7f6fbb8593587e7f0861d5256e6640f46b1b size: 856

A possible output could be:

failed to do request: Head "https://registry.nyzex.in:5000/v2/sample-app/blobs/sha256:6e38218f441707f4e338d3de283e3dee507e47bb8818603c5f28d54f221306": http: server gave HTTP response to HTTPS client

In this case we have perform the edit as I mentioned on 5 , Under /etc/docker/daemon.json

{
  "insecure-registries": ["registry.nyzex.in:5000"]
}

On adding this and restarting docker shall fix our issue.


8. Verify the Registry

Check which repositories exist:

curl http://registry.nyzex.in:5000/v2/_catalog

Output:

{"repositories":["sample-app"]}

This confirms that your image is stored in the private registry.


9. Pull and Run the Image

Pull the image on any host that can access the tunnel:

docker pull registry.nyzex.in:5000/sample-app:latest

Run it:

docker run -d -p 5001:5000 registry.nyzex.in:5000/sample-app:latest

Visit http://localhost:5001/ in a browser:

Hello from private Docker registry demo!

10. Notes and Best Practices

  1. TCP Passthrough: Necessary when the registry is behind HTTP proxy/tunnel. HTTP/HTTPS termination can break Docker push/pull operations.

  2. Port 5000: Required by default, but you can expose it via any external port using the tunnel.

  3. SSL Certificates: Ideally, you should secure the registry using TLS. If using HTTP/HTTPS proxy fails, TCP passthrough is the simpler workaround.

  4. Persistent Storage: Mount /var/lib/registry to preserve your images even if the container is recreated.

  5. Firewall: Make sure the VM allows traffic from the tunnel endpoint.


11. Summary

You now have a private Docker registry running behind a Pangolin tunnel, able to push and pull images securely over TCP passthrough, using a simple Flask app as a demo.

This setup is ideal for internal image hosting, CI/CD pipelines, or secure Docker environments where Docker Hub might not be suitable. We are yet to enable authentication for this, but this was a fun practice!

More from this blog

C

CodeOps Studies

39 posts

Simple write-ups on day to day code or devops experiments, tests etc.