Skip to main content
Crusoe Support Help Center home page
Crusoe

How-To Expose a Private Docker Registry Behind a Bastion Using an SSH Tunnel and Access It Through Crusoe CCR Pull-Through Caching

Tanaya Atmaram Kambli
Tanaya Atmaram Kambli
Updated

Last Updated: Jan 15, 2026

Introduction

This guide describes how to expose a private Docker registry running on a private Crusoe Cloud VM to the public internet through a secure SSH tunnel, using a public proxy VM. This allows you to:

  • Keep your registry fully private (no public IP)

  • Access it from your local machine or external systems

  • Use the registry as a pull-through source for the Crusoe Container Registry (CCR)

By following this guide, you will:

  • Deploy a Docker registry on a private Crusoe VM

  • Access it through a bastion host using SSH ProxyJump

  • Forward port 5000 through a public proxy

  • Persist the tunnel with autossh and systemd

  • Use the proxy as a pull-through provider in CCR

Note on External Private Registries

Although this guide uses a private Docker registry hosted on Crusoe Cloud VMs, the same approach works with registries hosted outside Crusoe, including:

  • AWS, Azure, or GCP VMs

  • On-prem servers

  • Customer datacenters

  • Multi-hop SSH environments

As long as the Crusoe proxy VM can establish an SSH tunnel to the registry (directly or via a bastion), all steps from Crusoe Cloud remain the same. If this is your scenario, please feel free to skip steps 2-4.

 

Prerequisites

Infrastructure:

  • Private VM in Crusoe Cloud running Docker registry (no public IP)

  • Bastion VM in same VPC and subnet as the private VM (has public IP)

  • Proxy VM in Crusoe Cloud with a public IP (for tunnel exposure) in a VPC different from the Private and Bastion VMs. This VM should be in the same region where you plan to create your CCR.

  • Static IPs for Bastion and Proxy (recommended)

Access Requirements:

  • SSH key pair for:

    • Bastion access

    • Private VM access (via Bastion)

  • Firewall rules allowing:

    • Proxy → Bastion: TCP/22

    • Bastion → Private VM: TCP/22

  • Outbound access for proxy (optional for testing via curl)

Software:

  • Docker installed on Private Registry VM

  • autossh installed on Proxy VM

  • systemd installed (default on most Linux distros)

     

Step-by-Step Instructions

  1. Architecture Overview 

    Crusoe Container Registry (CCR) - Pull through cache
        |
        | HTTP (registry access)
        v
    Proxy VM in Network 1 (Public IP; maintains SSH tunnel)
        |
        | ssh -L 5000:localhost:5000
        v
    Bastion Host in Network 2 (Public IP; only SSH port open)
        |
        | access via private IP 
        v
    Private Registry VM in Network 2 (Private IP only; Docker Registry on port 5000)
    

     

  2. Setup/Ensure Firewall Rules for communication within the Private VM and Bastion Subnets, and enable NAT for Private VM subnet 

    • In Crusoe Networking -> VPC Subnets ->  Select the Subnet where you want to create the Private VM -> Flip the 'NAT Gateway' Toggle to 'On' (Note: If you cannot see this Option please Contact Support)
    • In Crusoe Networking -> Firewall Rules section, ensure you have the following rules in place 
      • Ingress: 
        • Allow tcp,udp traffic from Source ports: * ,  Source Resource: VPC where the Private VM and Bastion lie  to  Destination port:  * , Destination Resource: VPC where the Private VM and Bastion lie. This allows tcp, udp ingress traffic to flow between the VPC
        • Allow icmp traffic from Source ports: * ,  Source Resource: VPC where the Private VM and Bastion lie  to  Destination port:  * , Destination Resource: VPC where the Private VM and Bastion lie. This allows icmp ingress traffic to flow between the VPC
      • Egress:  
        • Allow tcp,udp traffic from Source ports: * ,  Source Resource: VPC where the Private VM and Bastion lie  to  Destination port:  * , Destination Resource VPC where the Private VM and Bastion lie. This allows tcp, udp egress traffic to flow between the VPC
        • Allow icmp traffic from Source ports: * ,  Source Resource: VPC where the Private VM and Bastion lie  to  Destination port:  * , Destination Resource: VPC where the Private VM and Bastion lie. This allows icmp egress traffic to flow between the VPC
        • Allow tcp,udp traffic from Source ports: * ,  Source Resource: Subnet where the Private VM and Bastion lie  to  Destination port:  * , Destination CIDR: 0.0.0.0/0. This allows the Private VM to communicate with the outside world. 
    • Note: If this is not hosted on Crusoe Cloud but is on your Network, you can still ensure that the above is in place.

     

  3. Deploy a Private Docker Registry on the Private VM

    • Install Docker and add current User to the Docker group

      $ sudo apt update
      $ sudo apt install -y docker.io
      $ sudo systemctl enable --now docker
      $ sudo usermod -aG docker $USER
      $ newgrp docker
    • Start the registry

      $ docker run -d --name private-registry -p 5000:5000 registry:2
    • Test locally

      $ curl http://localhost:5000/v2/_catalog

     

  4. Push Images to the Private Registry and verify

    • Pull an image and push to the local registry on the Private VM

      $ docker pull alpine
      $ docker tag alpine localhost:5000/alpine
      $ docker push localhost:5000/alpine
    • Verify from the Private VM (local test) 

      $ curl http://localhost:5000/v2/_catalog
    • Verify from the Bastion VM (using private IP or private DNS of the Private VM)

      $ curl http://<private-ip-of-Private-VM>:5000/v2/_catalog
      
      OR 
      
      $ curl http://<private-dns-of-Private-VM>:5000/v2/_catalog
    • The output in both the cases should look as follows:

      {"repositories":["alpine"]}

     

  5. Setup a  Firewall Rule to allow Proxy VM to communicate with the Bastion

    • In Crusoe Networking -> Firewall Rules section, ensure you have the following rule in place 
      • Egress: Allow tcp,udp traffic from Source ports: * ,  Source CIDR: Proxy VM to  Destination port:  * , Destination CIDR: Bastion Public IP. This allows Proxy VM to communicate with the Bastion

     

  6. Configure SSH Access on Proxy VM

    • Place ssh keys of the Bastion and Private VM on the Proxy VM.

      Note: If the Bastion and the Private VM use the same private key, you only need that one key (assumed here). If they use different keys, place both keys on the Proxy VM and ensure the Bastion also has the private key for the Private VM to allow the Proxy to SSH through it.

      $ ls ~/.ssh/
      
      Expected outut: 
       ~/.ssh/id_private
    • Fix key permissions 

      $ chmod 600 ~/.ssh/id_*
    • Add SSH config. 

      Add the following config file at  ~/.ssh/config

      Host bastion
        HostName <BASTION_PUBLIC_IP>
        User ubuntu
        IdentityFile ~/.ssh/id_private
      
      Host private-vm
        HostName <PRIVATE_VM_PRIVATE_IP>
        User ubuntu
        IdentityFile ~/.ssh/id_private
        ProxyJump bastion
    • Test access to the Private VM  form the Proxy VM now

      $ ssh private-vm

     

  7. Create SSH Tunnel (Manual Test) on Proxy VM

    • On the Proxy VM

      $ ssh private-vm -N -L 0.0.0.0:5000:localhost:5000
    • Now test from the Proxy VM using a separate terminal

      $ curl http://localhost:5000/v2/_catalog

     

  8. Persist the Tunnel Using autossh on Proxy VM:

    • Install autossh

      $ sudo apt update && sudo apt install -y autossh

      Test

      $ autossh -M 0 private-vm -N -L 0.0.0.0:5000:localhost:5000
      $ curl http://localhost:5000/v2/_catalog
    • Create systemd service 

      $ sudo nano /etc/systemd/system/registry-tunnel.service
    • Paste the following contents in this 

      [Unit]
      Description=Persistent SSH Tunnel to Private Docker Registry
      After=network.target
      
      [Service]
      User=ubuntu
      ExecStart=/usr/bin/autossh -M 0 private-vm -N -L 0.0.0.0:5000:localhost:5000
      Restart=always
      Environment="AUTOSSH_GATETIME=0"
      
      [Install]
      WantedBy=multi-user.target
    • Enable and start the service

      $ sudo systemctl daemon-reexec
      $ sudo systemctl daemon-reload
      $ sudo systemctl enable registry-tunnel
      $ sudo systemctl start registry-tunnel
    • Verify that the service is running 

      $ sudo systemctl status registry-tunnel
    • Finally, test access of the private registry and verify it shows the correct output 

      $ curl http://localhost:5000/v2/_catalog
      
      Expected outut: 
      
      {"repositories":["alpine"]}

     

  9. Setup Firewall Rules 

    • In Crusoe Networking -> Firewall Rules section, ensure you have the following rule in place 
      • Ingress: Allow tcp,udp traffic from everywhere (Source ports: * , Source CIDRL 0.0.0.0/0) to Destination port 5000 of the Proxy VM (Destination resource). This allows CCR to communicate with the Proxy VM.

     

  10. Use the Proxy as a Pull-Through Source for Crusoe CCR

    • Create a pull-through repo in Crusoe CCR console or via CLI:

      CLI:

      $ crusoe registry repositories create --name <name> --location <region> --mode pull-through-cache --upstream-registry url=http://<Public-IP-or-Public-DNS-of_Proxy-VM>:5000,provider=docker-registry

      Here replace <name> with a name for the repo, <region>with the <region>where you want to create the repo (note: this should ideally be same as the region where your Proxy VM lies) and  <Public-IP-or-Public-DNS-of_Proxy-VM> with the Public IP or the Public DNS of the Proxy VM. 

      Console: 

      Container Registry → Pull-Through Repositories → Create

      Use:

      Repository Name: <give a name here>
      Mode: Pull-through cache
      Location: <region>; ideally same as Proxy VM) 
      Upstream Registry: docker-registry
      Registry URL: http://<Public IP of Proxy VM>:5000 OR http://<External DNS Name of Proxy VM>:5000

     

  11. Test Pulling Through CCR
    • Login to the registry 

      $ crusoe registry tokens create --alias my-token
      $ docker login registry.<region>.ccr.crusoecloudcompute.com/<namespace> -u <your-email>
    • Pull the image to test 

      $ docker pull registry.<region>.ccr.crusoecloudcompute.com/<namespace>/alpine:latest
    • This should check for the image in the internal registry and pull the image from there. You can compare the Image digest of the pulled image and the image hosted on your internal registry to verify. The image once pulled will be cached automatically. 

      Successful response:

      latest: Pulling from <namespace>/alpine
      6e174226ea69: Pull complete 
      Digest: sha256:4bcff63911fcb4448bd4fdacxxxxxxxxxxxxx
      Status: Downloaded newer image for registry.<region>.ccr.crusoecloudcompute.com/<namespace>/alpine:latest

 

Common Issues and Fixes

  1. Issue 1 : 

    failed to resolve reference .... not found

    You execute a docker pull command docker pull registry.<region>.ccr.crusoecloudcompute.com/<namespace>/<image:tag> which gives the the following error

    Error response from daemon: failed to resolve reference "registry.<region>.ccr.crusoecloudcompute.com/<namespace>/<image:tag>": registry.<region>.ccr.crusoecloudcompute.com/<namespace>/<image:tag>: not found

    Fix: 

    Ensure that the image and the tag exists in the upstream private hosted registry

  2. Issue 2

    bad request, check request parameters: Failed to create upstream registry, Please verify the URL, username and password

    You can see the above error when trying to create the CCR registry.

    Fix

    (Common Cause)Ensure that you are using the Public IP of the Proxy VM in the Registry URL parameter when creating the registry. 

    If that is correct, please ensure that the create request does not have any trailing slashes, invalid characters, or extra spaces in the URL, username, or password fields. Also, verify that the credentials you are using have the required permissions to create the registry.

    If the issue persists, please reach out to Contact Support.

  3. Issue 3

    internal_error, An internal error occurred while processing your request, Please try again or contact support if the issue persists, debug: rpc error: code = Unknown desc = unknown error occurred

    You can see the above error when trying to create the CCR registry.

    Fix: 

    (Common Cause) As first step, please ensure that the upstream private registry is online and accessible from your Proxy VM. Check for network connectivity between CCR registry and the Proxy VM, and ensure that any firewall rules or security groups allow access on the required ports.

    If the issue persists, please reach out to Contact Support.

     

Additional Resources

 

 

Related to

Was this article helpful?

0 out of 0 found this helpful

Still need help?

Our support team is ready to assist you with any questions.

Have more questions? Submit a request

Recently Viewed

Comments

0 comments

Article is closed for comments.