Step 6 — Docker on GHCR
One-command reproduction with Docker on the GitHub Container Registry
pip install yourthing covers most users. But scientific-Python installs still break for a stubborn minority — the GDAL / PROJ / CUDA / macOS-vs-Linux corner cases where “it worked on my laptop” doesn’t survive contact with someone else’s environment.
A Docker image solves this for those users. A reader who has Docker installed can spin up your full stack — Python, dependencies, GPU environment, notebooks, sample data — with one command, on any OS, without touching their base Python:
docker run -p 8888:8888 ghcr.io/yourorg/yourthing:latestThis chapter walks through: writing a Dockerfile, building it in CI on every release, publishing it to the GitHub Container Registry (GHCR), and testing that the image actually works.
Publishing to GHCR is free for public repos and works exactly like publishing to any Docker registry, except you already have an account (your GitHub account) and permissions inheritable from your repo.
The Dockerfile
Base image choice matters. Two good options for scientific Python:
mambaorg/micromamba:2-ubuntu22.04— a slim Ubuntu image withmicromambapre-installed. This is whatgeoai-datacubesuses. Great when your dependencies come from conda-forge (GDAL, PROJ, PyTorch CUDA, etc.).python:3.11-slim— bare Python. Fine if your dependencies are all pip-installable and don’t touch native libraries.
Below is a Dockerfile using the micromamba base — adapt for your case:
FROM mambaorg/micromamba:2-ubuntu22.04
# --- metadata ---
LABEL org.opencontainers.image.title="yourthing"
LABEL org.opencontainers.image.description="What your code does, in one sentence"
LABEL org.opencontainers.image.source="https://github.com/yourorg/yourthing"
LABEL org.opencontainers.image.licenses="MIT"
# --- copy environment file first (best layer caching) ---
USER root
WORKDIR /home/mambauser/yourthing
# One-shot install of the heavy conda-forge stack
RUN micromamba install -y -n base -c conda-forge \
python=3.11 \
# ...heavy stuff (gdal, rasterio, pytorch, etc)... \
jupyterlab ipywidgets && \
micromamba clean --all --yes
# --- copy the package + pip-install it in editable mode ---
COPY --chown=mambauser:mambauser . /home/mambauser/yourthing
USER mambauser
RUN pip install --no-deps -e .
# --- default: run JupyterLab on port 8888, no token, listen on all interfaces ---
# (the reader binds this to 127.0.0.1 with -p when they `docker run`)
EXPOSE 8888
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--port=8888", "--no-browser", \
"--ServerApp.token=", "--ServerApp.password=", \
"--ServerApp.allow_origin=*"]A few things worth understanding:
micromamba install ... clean --all --yes— install everything in one layer, then delete the package caches, so the final image is small.USER root→USER mambauser— install as root (needed for micromamba), then drop to unprivileged for the actual container runtime. Standard Docker hygiene.pip install --no-deps -e .— install your own package without pulling in dependencies again (they’re already installed via micromamba).CMD ["jupyter", ...]— the default command when the container runs. Reasonable choice for a research package where users want notebooks; for a CLI-only package, use your CLI entrypoint instead.
The GHCR publish workflow
Create .github/workflows/docker.yml:
name: Docker
on:
push:
tags: ["v*"]
release:
types: [published]
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # <-- required to push to GHCR
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}} # v0.2.0 -> 0.2.0
type=semver,pattern={{major}}.{{minor}} # v0.2.0 -> 0.2
type=raw,value=latest # every tagged release
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxThe magic is ${{ secrets.GITHUB_TOKEN }} — GitHub Actions provides this automatically per workflow run, scoped to just this repo’s packages. No manual secret needed.
First publish: make the image public
After the workflow runs successfully the first time, your image is on GHCR as private by default. To make it publicly pullable:
- Go to your repo’s landing page → right-hand sidebar → Packages → click the package.
- On the package page → Package settings (right side) → Change visibility → Public.
- Confirm.
The image is now pullable without authentication. Test from another machine:
docker pull ghcr.io/yourorg/yourthing:latest
docker run -p 127.0.0.1:8888:8888 ghcr.io/yourorg/yourthing:latest
# open the printed URL in your browserIf you skip this step, your image is private and no one can pull it — including you, if you’re not logged into GHCR. docker pull will fail with “unauthorized.” This is the single most common reason a new GHCR image “doesn’t work.”
Apptainer / Singularity: HPC users
Many HPC clusters don’t allow Docker directly (root-privilege concerns) but do allow Apptainer (formerly Singularity). Apptainer can pull GHCR images directly:
apptainer pull docker://ghcr.io/yourorg/yourthing:latest
apptainer run yourthing_latest.sif jupyter lab --ip=0.0.0.0Mention this in your README under the Docker section — HPC users are a real audience for scientific research software.
Common issues
“Docker build fails with micromamba: command not found.” — Your base image tag doesn’t exist. Common variants: mambaorg/micromamba:2-ubuntu22.04, mambaorg/micromamba:2-ubuntu24.04, mambaorg/micromamba:latest. Check the Docker Hub tag list first.
“The workflow succeeded but nobody can pull the image.” — Visibility is private. See the callout above.
“docker build works locally but fails in GH Actions.” — Usually a caching issue. Try disabling cache-from/cache-to first, or use actions/cache explicitly.
“The image is 5 GB and takes 10 minutes to pull.” — You probably kept the micromamba package cache. Ensure micromamba clean --all --yes runs in the same RUN as the install. Also check that MANIFEST.in-worthy data files aren’t being COPY’d into the image.
Checklist
Once your Docker image is public and pullable, you have the full Level 1 stack — PyPI + Docker + CI + tests. Now write the paper — on to Step 7 — paper.md + paper.bib.