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
autosshandsystemdUse 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
autosshinstalled on Proxy VM-
systemd installed (default on most Linux distros)
Step-by-Step Instructions
-
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) -
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.
- Ingress:
- Note: If this is not hosted on Crusoe Cloud but is on your Network, you can still ensure that the above is in place.
-
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
-
-
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"]}
-
-
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
- In Crusoe Networking -> Firewall Rules section, ensure you have the following rule in place
-
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/configHost 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
-
-
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
-
-
Persist the Tunnel Using autossh on Proxy VM:
-
Install autossh
$ sudo apt update && sudo apt install -y autosshTest
$ 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"]}
-
-
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.
- In Crusoe Networking -> Firewall Rules section, ensure you have the following rule in place
-
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-registryHere 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
-
-
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
-
Issue 1 :
failed to resolve reference .... not foundYou execute a docker pull command
docker pull registry.<region>.ccr.crusoecloudcompute.com/<namespace>/<image:tag>which gives the the following errorError response from daemon: failed to resolve reference "registry.<region>.ccr.crusoecloudcompute.com/<namespace>/<image:tag>": registry.<region>.ccr.crusoecloudcompute.com/<namespace>/<image:tag>: not foundFix:
Ensure that the image and the tag exists in the upstream private hosted registry
-
Issue 2 :
bad request, check request parameters: Failed to create upstream registry, Please verify the URL, username and passwordYou 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 URLparameter 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.
-
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 occurredYou 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
- Crusoe Container Registry: https://docs.crusoecloud.com/container-registry
- Crusoe Firewall Rules: https://docs.crusoecloud.com/networking/firewall-rules
- Crusoe Compute: https://docs.crusoecloud.com/compute