Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
152742a
docker: add dev-container Dockerfile and scripts
IvanGrigorik Nov 1, 2025
fdd200a
dev-container: finalaize container structure and user prompts
IvanGrigorik Nov 2, 2025
7f7ced7
docker: change port exposure in order to correctly ssh straight into …
IvanGrigorik Nov 3, 2025
931d968
add pykokkos into devcontainer
IvanGrigorik Nov 12, 2025
c883081
change EOF format
IvanGrigorik Nov 12, 2025
97e7a2f
change EOF format
IvanGrigorik Nov 12, 2025
ee4f50a
remove deprecated Dockerfile
IvanGrigorik Dec 18, 2025
2fbfeb8
sync up with `main`
IvanGrigorik Dec 18, 2025
c6903ea
Merge branch 'main' into grigorik/docker-devcontainer
IvanGrigorik Dec 18, 2025
5c66b36
update docker installation
IvanGrigorik Dec 18, 2025
d72ab1b
Merge branch 'main' into grigorik/docker-devcontainer
IvanGrigorik Dec 19, 2025
4d5d04f
merge conflict resolution
IvanGrigorik Dec 19, 2025
1733c7e
add flexible NVIDIA docker container with custom kernel
IvanGrigorik Dec 21, 2025
c679035
Merge branch 'grigorik/docker-devcontainer' of github.com:kokkos/pyko…
IvanGrigorik Dec 21, 2025
90f5153
add template for secrets
IvanGrigorik Dec 21, 2025
32c443e
add default template proper handling
IvanGrigorik Dec 21, 2025
ccb78f5
add pykokkos installation after base install
IvanGrigorik Dec 22, 2025
49bf16c
shellcheck warns fix
IvanGrigorik Dec 23, 2025
0aa1fc7
add workflow to check devcontainer linting errors, shell errors and b…
IvanGrigorik Dec 23, 2025
e52f2d0
fix shellcheck warns
IvanGrigorik Dec 23, 2025
2ea84b3
fix hadolint docker container issues
IvanGrigorik Dec 23, 2025
8f89922
fix shellcheck warns
IvanGrigorik Dec 23, 2025
1c19d31
add wget to install list
IvanGrigorik Dec 23, 2025
9e269c9
address all shellcheck warns
IvanGrigorik Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ tags
# Persistent undo
[._]*.un~

# Docker files and secrets
dev-container/secrets.yaml

# Output files and directories
build/
dev-container/secrets.yaml
35 changes: 0 additions & 35 deletions Dockerfile

This file was deleted.

118 changes: 118 additions & 0 deletions dev-container/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
FROM nvidia/cuda:12.4.1-devel-ubuntu22.04

# Optional ARG for username (only set if secret is provided)
ARG USERNAME=""
ENV username=${USERNAME}

# Optional ARG for SSH port
ARG PORT="2222"
ENV SSH_PORT=${PORT}

# Dependencies installation
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
build-essential \
micro \
nano \
git \
openssh-server \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/run/sshd \
&& ssh-keygen -A

# Create user using Docker secrets for sensitive data
RUN --mount=type=secret,id=username \
--mount=type=secret,id=password \
--mount=type=secret,id=ssh_key \
if [ -f /run/secrets/username ] && [ -s /run/secrets/username ] && [ -f /run/secrets/password ] && [ -s /run/secrets/password ]; then \
export NAME=$(cat /run/secrets/username) && \
export PASSWORD=$(cat /run/secrets/password) && \
useradd -m -u 1000 -s /bin/bash ${NAME} && \
echo "${NAME}:${PASSWORD}" | chpasswd && \
echo "root:${PASSWORD}" | chpasswd && \
usermod -aG sudo ${NAME} && \
chown -R ${NAME}:${NAME} /home/${NAME} && \
echo "${NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \
echo "${NAME}" > /tmp/username; \
else \
echo "root" > /tmp/username; \
fi

# Configure SSH permissions for devcontainer (only if custom user was created)
RUN export USERNAME=$(cat /tmp/username) && \
if [ "${USERNAME}" != "root" ]; then \
mkdir -p /var/run/sshd && \
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config && \
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config; \
fi

# Conditionally add SSH public key if provided
RUN --mount=type=secret,id=ssh_key \
export USERNAME=$(cat /tmp/username) && \
if [ -f /run/secrets/ssh_key ] && [ -s /run/secrets/ssh_key ] && [ "${USERNAME}" != "root" ]; then \
export SSH_PUB_KEY=$(cat /run/secrets/ssh_key) && \
mkdir -p /home/${USERNAME}/.ssh/ && \
echo "${SSH_PUB_KEY}" >> /home/${USERNAME}/.ssh/authorized_keys && \
chmod 700 /home/${USERNAME}/.ssh/ && \
chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.ssh/ && \
chmod 600 /home/${USERNAME}/.ssh/authorized_keys; \
fi

# Set up custom entrypoint and init scripts
COPY dev-container/scripts/custom-entrypoint.sh /opt/custom-entrypoint.sh
COPY dev-container/scripts/init-pykokkos.sh /opt/init-pykokkos.sh
RUN chmod +x /opt/custom-entrypoint.sh /opt/init-pykokkos.sh

# Set working directory based on USERNAME ARG
# WORKDIR_PATH will be passed as build arg from build script
ARG WORKDIR_PATH="/root"

# Set WORKDIR - use WORKDIR_PATH from build arg
WORKDIR ${WORKDIR_PATH}

# Temporarily switch to user for conda and git operations
USER ${USERNAME:-root}

# Set up and initialize conda environment
# Install conda in the user's home directory
RUN export USERNAME=$(cat /tmp/username) && \
if [ "${USERNAME}" != "root" ]; then \
export HOME_DIR="/home/${USERNAME}" && \
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh && \
bash miniconda.sh -b -p ${HOME_DIR}/miniconda3 && \
rm -f miniconda.sh && \
echo "${HOME_DIR}/miniconda3/bin" > /tmp/conda_path; \
else \
export HOME_DIR="/root" && \
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh && \
bash miniconda.sh -b -p ${HOME_DIR}/miniconda3 && \
rm -f miniconda.sh && \
echo "${HOME_DIR}/miniconda3/bin" > /tmp/conda_path; \
fi

# Initialize user's conda environment, TOS agreements and ensure it's loaded in every shell
RUN export CONDA_PATH=$(cat /tmp/conda_path) && \
${CONDA_PATH}/conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main && \
${CONDA_PATH}/conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r && \
${CONDA_PATH}/conda init

# Clone pykokkos repository (installation will happen at runtime)
RUN git clone https://github.com/kokkos/pykokkos.git

# Add conda to PATH in .bashrc
RUN export CONDA_PATH=$(cat /tmp/conda_path) && \
export HOME_DIR=$(dirname $(dirname ${CONDA_PATH})) && \
if [ "${HOME_DIR}" != "/root" ]; then \
echo "export PATH=\"${CONDA_PATH}:${HOME_DIR}/miniconda3/condabin:${HOME_DIR}/.local/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"" >> ${HOME_DIR}/.bashrc && \
echo "export condaPath=${CONDA_PATH}" >> ${HOME_DIR}/.bashrc; \
else \
echo "export PATH=\"${CONDA_PATH}:${HOME_DIR}/miniconda3/condabin:${HOME_DIR}/.local/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"" >> ${HOME_DIR}/.bashrc && \
echo "export condaPath=${CONDA_PATH}" >> ${HOME_DIR}/.bashrc; \
fi

# Switch back to root for entrypoint execution
USER root

# Expose port in order to be able to connect from IDEs
EXPOSE ${SSH_PORT}

ENTRYPOINT ["/opt/custom-entrypoint.sh"]
201 changes: 201 additions & 0 deletions dev-container/build-container.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#!/bin/bash

# Get absolute paths for all directories we need
SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")
CONTAINER_DIR=$(dirname "$SCRIPT_PATH")
PROJECT_ROOT=$(dirname "$CONTAINER_DIR")
SECRETS_FILE="${CONTAINER_DIR}/secrets.yaml"
SECRETS_TEMPLATE="${CONTAINER_DIR}/secrets-template.yaml"

# Check if secrets.yaml exists, if not copy from template
if [ ! -f "${SECRETS_FILE}" ]; then
if [ -f "${SECRETS_TEMPLATE}" ]; then
echo "secrets.yaml not found. Creating from template..."
cp "${SECRETS_TEMPLATE}" "${SECRETS_FILE}"
echo ""
echo "======================================"
echo "CONFIGURATION (OPTIONAL)"
echo "======================================"
echo "A secrets.yaml file has been created from the template."
echo "You can edit it with your configuration or skip this step:"
echo ""
echo " ${SECRETS_FILE}"
echo ""
echo "Run this script again to build the container."
echo "======================================"
exit 0
else
echo "Error: Neither secrets.yaml nor secrets-template.yaml found!"
exit 1
fi
fi

# Template values
TEMPLATE_USERNAME="root"
TEMPLATE_PASSWORD="pykPassword"
TEMPLATE_PORT=""
TEMPLATE_SSH=""

# Function to parse YAML value (simple key: value parser)
parse_yaml_value() {
local key="$1"
local file="$2"
# Remove quotes and extract value after colon
grep "^${key}:" "$file" | sed "s/^${key}:[[:space:]]*//" | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//"
}

# Function to check if a value is not a template (i.e., should be used)
# Modifies the variable: keeps value if not template, clears if template
# Returns true/false
is_not_template() {
local var_name="$1"
local template="$2"
local value="${!var_name}"

if [ "${value}" != "${template}" ] && [ -n "${value}" ]; then
# Variable is not a template, keep its value
echo "true"
else
# Variable is a template, clear it
eval "${var_name}=\"\""
echo "false"
fi
}

# Load secrets from YAML file
load_secrets() {
CONTAINER_NAME=$(parse_yaml_value "container_name" "${SECRETS_FILE}")
CONTAINER_NAME=${CONTAINER_NAME:-"pyk"}
USERNAME=$(parse_yaml_value "username" "${SECRETS_FILE}")
PASSWORD=$(parse_yaml_value "password" "${SECRETS_FILE}")
PORT=$(parse_yaml_value "port" "${SECRETS_FILE}")
SSH_KEY=$(parse_yaml_value "ssh" "${SECRETS_FILE}")
}

# Check which values are templates
check_templates() {
HAS_USERNAME=$(is_not_template USERNAME "${TEMPLATE_USERNAME}")
HAS_PASSWORD=$(is_not_template PASSWORD "${TEMPLATE_PASSWORD}")
HAS_SSH=$(is_not_template SSH_KEY "${TEMPLATE_SSH}")
HAS_PORT=$(is_not_template PORT "${TEMPLATE_PORT}")
}

# Build Docker build arguments
build_docker_args() {
BUILD_ARGS=()
SECRET_FILES=()

# Create temporary directory for secrets
SECRETS_DIR=$(mktemp -d)

if [ "$HAS_USERNAME" = "true" ]; then
BUILD_ARGS+=(--build-arg "USERNAME=${USERNAME}")
BUILD_ARGS+=(--build-arg "WORKDIR_PATH=/home/${USERNAME}")
echo -n "${USERNAME}" > "${SECRETS_DIR}/username"
SECRET_FILES+=("--secret" "id=username,src=${SECRETS_DIR}/username")
else
BUILD_ARGS+=(--build-arg "WORKDIR_PATH=/root")
# Create empty secret file to avoid errors
touch "${SECRETS_DIR}/username"
SECRET_FILES+=("--secret" "id=username,src=${SECRETS_DIR}/username")
fi

if [ "$HAS_PASSWORD" = "true" ]; then
echo -n "${PASSWORD}" > "${SECRETS_DIR}/password"
SECRET_FILES+=("--secret" "id=password,src=${SECRETS_DIR}/password")
else
# Create empty secret file to avoid errors
touch "${SECRETS_DIR}/password"
SECRET_FILES+=("--secret" "id=password,src=${SECRETS_DIR}/password")
fi

if [ "$HAS_SSH" = "true" ]; then
echo -n "${SSH_KEY}" > "${SECRETS_DIR}/ssh_key"
SECRET_FILES+=("--secret" "id=ssh_key,src=${SECRETS_DIR}/ssh_key")
else
# Create empty secret file to avoid errors
touch "${SECRETS_DIR}/ssh_key"
SECRET_FILES+=("--secret" "id=ssh_key,src=${SECRETS_DIR}/ssh_key")
fi

[ "$HAS_PORT" = "true" ] && BUILD_ARGS+=(--build-arg "PORT=${PORT}")
}

# Build Docker container
build_container() {
echo "Building docker container: ${CONTAINER_NAME}"
# TODO: add `--no-cache` after confirming that script can be builded
docker build \
"${BUILD_ARGS[@]}" \
"${SECRET_FILES[@]}" \
-t "${CONTAINER_NAME}:latest" \
-f "${CONTAINER_DIR}/Dockerfile" \
"${PROJECT_ROOT}"

local build_result=$?

# Cleanup secrets directory
if [ -n "${SECRETS_DIR}" ] && [ -d "${SECRETS_DIR}" ]; then
rm -rf "${SECRETS_DIR}"
fi

if [ $build_result != 0 ]; then
echo "Docker build failed"
exit 1
fi
}

# Stop and remove existing container
stop_and_remove_container() {
echo "Stopping ${CONTAINER_NAME}"
docker stop "${CONTAINER_NAME}" 2>/dev/null || true

echo "Removing ${CONTAINER_NAME}"
docker rm "${CONTAINER_NAME}" 2>/dev/null || true
}

# Run Docker container
run_container() {
RUN_ARGS=(
-dit
--gpus all
--security-opt seccomp=unconfined
--name "${CONTAINER_NAME}"
--restart unless-stopped
)

[ "$HAS_PORT" = "true" ] && RUN_ARGS+=(-p "${PORT}:${PORT}")
RUN_ARGS+=("${CONTAINER_NAME}")
# Explicitly pass bash to ensure container keeps running with entrypoint
RUN_ARGS+=("bash")

docker run "${RUN_ARGS[@]}"
}

# Display farewell message
farewell() {
EXEC_USERNAME=$([ "$HAS_USERNAME" = "true" ] && echo "${USERNAME}" || echo "root")
echo "##################"
echo "Container started."
echo "To access container from terminal, enter: "
echo "> docker exec -it -u ${EXEC_USERNAME} ${CONTAINER_NAME} bash"
[ "$HAS_PASSWORD" = "true" ] && echo "To access sudo mode, enter 'su' and password from secrets.yaml."
echo ""
echo "PyKokkos installation is running in the background."
echo "To check installation progress, run:"
echo "> docker exec -u root ${CONTAINER_NAME} bash -c \"cat /tmp/pyk-install.log\""
echo "##################"
}

# Main script execution flow
pushd "$CONTAINER_DIR" &> /dev/null

load_secrets
check_templates
build_docker_args
build_container
stop_and_remove_container
run_container
farewell

popd
Loading
Loading