Custom runpod container

Posted on Jan 2, 2026

In today’s cloud compute market, there are a lot of different providers — everything from small local companies trying to break through to massive giants like Amazon, Azure, and GCP. My personal favorite is GCP, not necessarily because they’re the best, but because it’s the provider I know (and have used) the most.

However, getting a GPU on GCP can be a bit complicated: you need to request quota, you need to do it in the right region, and that region needs to actually have capacity. At one point my quota request was approved, but GPUs still weren’t available in that region. So for most GPU work I’ve turned to Modal and Runpod. Modal is awesome for notebooks (and you get $30/month for free), and Runpod is great when I want to work in a custom container.

Getting a custom container working on Runpod was a bit of a hassle the first time, so I thought I’d share how to build a custom Runpod container you can SSH into.

The first step is creating a Docker image. I usually start from a base image with as much preinstalled as possible, like an official PyTorch or Hugging Face 🤗 container.

Below is our base Dockerfile, which we’ll update a bit.

FROM huggingface/transformers-pytorch-deepspeed-latest-gpu

# Install SSH server (for RunPod "SSH" connect option)
RUN apt-get update \
    && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        openssh-server \
    && rm -rf /var/lib/apt/lists/* \
    && mkdir -p /run/sshd

# Make root SSH login possible inside the container (RunPod commonly connects as root)
RUN sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config \
    && sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config \
    && sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config

RUN apt-get update && apt-get install -y curl

After that we need to add some Runpod-specific pieces to make SSH work. We’ll also expose a couple of ports and add a bash script. Below is the custom start script from Runpod’s official containers that we’ll add.

#!/usr/bin/env bash
set -e

# Start SSH if PUBLIC_KEY is provided (RunPod convention)
setup_ssh() {
  if [[ -n "${PUBLIC_KEY:-}" ]]; then
    echo "Setting up SSH authorized_keys..."
    mkdir -p ~/.ssh
    echo "$PUBLIC_KEY" >> ~/.ssh/authorized_keys
    chmod 700 ~/.ssh
    chmod 600 ~/.ssh/authorized_keys

    echo "Ensuring SSH host keys exist..."
    mkdir -p /etc/ssh
    [[ -f /etc/ssh/ssh_host_rsa_key ]] || ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -q -N ''
    [[ -f /etc/ssh/ssh_host_ecdsa_key ]] || ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -q -N ''
    [[ -f /etc/ssh/ssh_host_ed25519_key ]] || ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -q -N ''

    echo "Starting sshd..."
    mkdir -p /run/sshd
    /usr/sbin/sshd
  else
    echo "PUBLIC_KEY not set; not starting sshd."
    echo "(Set PUBLIC_KEY in RunPod to enable SSH access.)"
  fi
}

export_env_vars() {
  # Make env vars available in interactive shells
  echo "Exporting environment variables for interactive shells..."
  printenv | grep -E '^[A-Z_][A-Z0-9_]*=' | grep -v '^PUBLIC_KEY=' | awk -F= '{ val=$0; sub(/^[^=]*=/,"",val); print "export " $1 "=\"" val "\"" }' > /etc/rp_environment || true
  if [[ -f /etc/rp_environment ]]; then
    touch ~/.bashrc
    grep -q 'source /etc/rp_environment' ~/.bashrc || echo 'source /etc/rp_environment' >> ~/.bashrc
  fi
}

start_jupyter() {
  if [[ -n "${JUPYTER_PASSWORD:-}" ]]; then
    if python3 -c "import jupyterlab" >/dev/null 2>&1 || python3 -m jupyter lab --version >/dev/null 2>&1; then
      echo "Starting Jupyter Lab on :8888 ..."
      mkdir -p /workspace
      nohup python3 -m jupyter lab \
        --allow-root --no-browser \
        --port=8888 --ip=* \
        --FileContentsManager.delete_to_trash=False \
        --ServerApp.terminado_settings='{"shell_command":["/bin/bash"]}' \
        --IdentityProvider.token="$JUPYTER_PASSWORD" \
        --ServerApp.allow_origin=* \
        --ServerApp.preferred_dir=/workspace \
        > /jupyter.log 2>&1 &
      echo "Jupyter Lab started (token set from JUPYTER_PASSWORD)."
    else
      echo "JUPYTER_PASSWORD is set but Jupyter is not installed in this image; skipping Jupyter start."
    fi
  fi
}

echo "Pod starting..."
setup_ssh
start_jupyter
export_env_vars

echo "Pod ready. Sleeping forever."
sleep infinity

The final container image will look like this:

FROM huggingface/transformers-pytorch-deepspeed-latest-gpu


# Install SSH server (for RunPod "SSH" connect option)
RUN apt-get update \
    && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        openssh-server \
    && rm -rf /var/lib/apt/lists/* \
    && mkdir -p /run/sshd

# Make root SSH login possible inside the container (RunPod commonly connects as root)
RUN sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config \
    && sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config \
    && sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config

COPY start.sh /start.sh
RUN chmod +x /start.sh

RUN apt-get update && apt-get install -y curl

# Optional: RunPod/SSH (22) and Jupyter (8888)
EXPOSE 22 8888

CMD ["/start.sh"] 

Let’s build the container and push it to GCP Artifact Registry.

Runpod supports private container/artifact repos from Docker Hub, so for GCP Artifact Registry the container must be public (not ideal, but for this small example let’s not worry about it). Since we’ll run the image on Linux, we’ll make sure to build for the correct architecture. With that said, feel free to use any artifact registry of your choice!

For GCP I normally use the command below; then the image is built on a Linux machine and pushed to Artifact Registry.

gcloud builds submit \
  --project=<your gcp project> \
  --tag=<your artifact registry> \
  --region=europe-west1

We’re almost done. One important step is left: we need to create a template in Runpod that uses our container. Things to consider: how much disk do you need? In this example we YOLO 200GB. Then we set up the correct ports and it should be good to go. That means HTTP ports 8888 and 80, as well as TCP port 22.

config template

You also need to add the path to your container image, left blank in this example.

Let’s try it. Runpod’s official Docker images are cached, so they load incredibly fast. Ours will take a little longer to start up.

Runpod startup

There you have it — hope this saves you a bit of time building a custom image you can SSH into!