/environment.md. The full docset is at /llms-full.md and the index is at /llms.md.Environment
Every C3 job runs the script: from your .c3 project configuration as a Bash script on a GPU VM. The environment mode controls what C3 prepares before that script starts:
- Python: C3 builds and caches a uv environment from
pyproject.toml+uv.lock. - Docker: C3 pulls the Docker Hub image in
docker.imageand runs your script inside it. - Bash: C3 runs your script directly on the VM. This can launch anything, but any setup you do in the script runs from scratch on every job.
Use Python or Docker when dependency setup is expensive or needs to be reproducible across jobs. Use Bash when your workload is already self-contained, has trivial setup, or you intentionally want to manage everything inside the script.
Python
Use python.project for Python projects with a pyproject.toml and uv.lock:
# .c3
project: python-example
script: run.sh
gpu: l40
time: "02:00:00"
python:
project: ./
output:
- ./results
# run.sh
#!/bin/bash
set -euo pipefail
python3 train.py --output "$C3_ARTIFACTS_DIR"
# pyproject.toml
[project]
name = "python-example"
requires-python = ">=3.11"
dependencies = [
"jax[cuda12]",
"numpy",
]
C3 uses uv to run uv sync before your script starts. The resulting environment is cached from the lock file, so repeat jobs with the same uv.lock avoid rebuilding dependencies. If your project is in a subdirectory, set python.project to that path.
Generate or refresh the lock file locally with:
uv lock
python.project to pip install in run.shIf you install Python packages inside the Bash script, those installs happen again for every job. Declaring the project with python.project lets C3 cache the environment between jobs.
Docker
Use docker: for non-Python workloads, mixed-language projects, custom CUDA/system libraries, or Python projects that need OS-level dependencies. docker: and python: are mutually exclusive.
Reference a Docker Hub image from .c3:
# .c3
project: docker-example
script: run.sh
gpu: l40
time: "02:00:00"
docker:
image: rust:1.95-slim-bookworm
output:
- ./results
# run.sh
#!/bin/bash
set -euo pipefail
cargo run --release -- --output "$C3_ARTIFACTS_DIR"
# Cargo.toml
[package]
name = "docker-example"
version = "0.1.0"
edition = "2021"
// src/main.rs
use std::{env, fs, path::PathBuf};
fn main() -> std::io::Result<()> {
let mut output = None;
let mut args = env::args().skip(1);
while let Some(arg) = args.next() {
if arg == "--output" {
output = args.next();
break;
}
}
let output = output.unwrap_or_else(|| {
env::var("C3_ARTIFACTS_DIR").unwrap_or_else(|_| ".".to_string())
});
let path = PathBuf::from(output).join("result.txt");
fs::create_dir_all(path.parent().unwrap())?;
fs::write(path, "Rust job ran on C3\n")
}
Use a public Docker Hub image that C3 can pull without authentication; bare names (ubuntu:22.04), namespaced names (user/image:tag), and explicit docker.io/... hosts are accepted, but private repositories and external registries are not. Keep secrets out of image layers: do not publish .c3.local, .env, SSH keys, cloud credentials, service tokens, or build-argument secrets.
C3 validates that docker.image points to Docker Hub, uploads your workspace, and pulls the image on the GPU VM before running bash /workspace/<script> inside the container. Docker images must include bash; minimal images such as Alpine or distroless may not have it by default.
Docker images keep their own PATH, HOME, and other ENV defaults. C3 only injects the job-specific variables needed to find the workspace and artifact directory.
Bash
Use Bash-only mode by omitting both python: and docker:. C3 still runs your script: on the GPU VM, and that script can execute shell commands, compiled binaries, Julia, R, Rust, Fortran, MPI launchers, or anything else available in the workspace or installed by the script.
# .c3
project: bash-example
script: run.sh
gpu: l40
time: "00:30:00"
datasets:
- ref: /datasets/example-inputs
mount: /data
output:
- ./results
# run.sh
#!/bin/bash
set -euo pipefail
mkdir -p results
# Any dependency setup done here runs again on every job.
chmod +x ./bin/my-simulation
./bin/my-simulation --input /data/input.dat --output results/output.dat
Bash-only jobs are the most flexible, but C3 does not cache an environment for them. If your script downloads packages, compiles dependencies, or creates virtual environments, that setup cost is paid on every run. Move that setup into python.project or docker: when repeat-job startup time matters.
Script environment variables
Your script runs with a sanitized environment. C3 forwards the runtime variables needed by the GPU/software stack but does not expose agent credentials, storage keys, or arbitrary host secrets.
| Variable | Description |
|---|---|
C3_JOB_WORKDIR | Absolute path to the job workspace. Available in all modes |
C3_ARTIFACTS_DIR | Directory for files you want uploaded as job artifacts. Available in all modes |
PYTHONUNBUFFERED | Set to 1 so Python output streams promptly. Available in all modes |
TERM | Terminal type, defaulting to dumb if unset. Available in all modes |
HOME | Set to the job workspace for Bash/Python jobs. In Docker jobs, the image controls HOME |
PATH | Runtime PATH for Bash/Python jobs, with the Python virtualenv prepended when configured. In Docker jobs, the image controls PATH |
TMPDIR | Per-job temporary directory under the job workspace for Bash/Python jobs |
VIRTUAL_ENV | Set when python.project creates a virtualenv |
Write persistent results to $C3_ARTIFACTS_DIR or a path listed in output:; files outside the configured artifact outputs are discarded when the job ends. Use data mounting for inputs that should live in C3 storage rather than in the project bundle.