A step-by-step guide to containerising a Python package, publishing the image to GitHub Container Registry (GHCR), and making it easy for teammates to run locally.
- Docker Desktop installed and running
- A GitHub account
- A Python package managed with Poetry
- A package with at least one entry point (e.g. a FastAPI app)
If your package does not have an API yet, add FastAPI and uvicorn:
poetry add fastapi uvicornAdd a minimal FastAPI app to your package __init__.py:
from importlib.metadata import version
from fastapi import FastAPI
__version__ = version("your-package-name")
app = FastAPI()
@app.get("/version")
def get_version():
return {"version": __version__}Test it locally:
poetry run uvicorn your_package_name:app --reload
curl http://localhost:8000/versionCreate a Dockerfile at the project root:
# Base image: slim Python to keep the image size small
FROM python:3.10-slim
# Set the working directory inside the container
WORKDIR /app
# Copy dependency manifest, readme, and source code into the container
COPY pyproject.toml .
COPY README.md .
COPY src/ src/
# Install Poetry, disable virtualenv creation (not needed inside a container),
# and install only production dependencies
RUN pip install poetry && \
poetry config virtualenvs.create false && \
poetry install --only main
# Start the app on all interfaces so it is reachable from outside the container
CMD ["uvicorn", "your_package_name:app", "--host", "0.0.0.0", "--port", "8000"]Build and test locally:
docker build -t your-package-name .
docker run -p 8000:8000 your-package-name
curl http://localhost:8000/version- Go to GitHub -> Settings -> Developer Settings -> Personal Access Tokens -> Tokens (classic)
- Click "Generate new token (classic)"
- Give it a name, e.g.
ghcr-push - Check the
write:packagesscope - Click "Generate token" and copy it immediately — GitHub will not show it again
Create a .env file at the project root:
GITHUB_PAT=your_token_here
GITHUB_USERNAME=your_github_username_in_lowercaseAdd .env to .gitignore immediately:
echo ".env" >> .gitignoreNever commit the .env file.
source .env
docker login ghcr.io -u $GITHUB_USERNAME --password $GITHUB_PATGHCR requires lowercase repository names. Tag and push:
source .env
docker tag your-package-name ghcr.io/$GITHUB_USERNAME/your-package-name:latest
docker push ghcr.io/$GITHUB_USERNAME/your-package-name:latestGHCR packages are private by default.
- Go to
https://github.com/YOUR_USERNAME?tab=packages - Click on your package
- Click "Package settings"
- Scroll to "Danger Zone" and set visibility to Public
Images built on Apple Silicon (ARM) will not run on Intel/AMD machines and vice versa. Build for both platforms in one step using Docker Buildx:
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/$GITHUB_USERNAME/your-package-name:latest \
--push .Platform coverage:
| Platform | Covers |
|---|---|
| linux/amd64 | Intel Macs, Intel/AMD Linux, Windows |
| linux/arm64 | Apple Silicon Macs (M1/M2/M3/M4) |
Pushing both a version tag and latest is standard practice. latest is a convenience
pointer for teammates. The version tag gives you a permanent, reproducible reference for
rollbacks.
Read the version from pyproject.toml automatically and push both tags:
VERSION=$(poetry version -s)
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/$GITHUB_USERNAME/your-package-name:$VERSION \
-t ghcr.io/$GITHUB_USERNAME/your-package-name:latest \
--push .Add the following to your Makefile. The VERSION variable is read live from
pyproject.toml via poetry version -s.
REGISTRY := ghcr.io/your_github_username
IMAGE := your-package-name
VERSION := $(shell poetry version -s)
docker-build:
docker buildx build --platform linux/amd64,linux/arm64 \
-t $(REGISTRY)/$(IMAGE):$(VERSION) \
-t $(REGISTRY)/$(IMAGE):latest \
--push .
release-patch:
poetry version patch
git add pyproject.toml
git commit -m "chore: bump version to $$(poetry version -s)"
git push
$(MAKE) docker-build
release-minor:
poetry version minor
git add pyproject.toml
git commit -m "chore: bump version to $$(poetry version -s)"
git push
$(MAKE) docker-build
$(MAKE) build
$(MAKE) upload
release-major:
poetry version major
git add pyproject.toml
git commit -m "chore: bump version to $$(poetry version -s)"
git push
$(MAKE) docker-build
$(MAKE) build
$(MAKE) uploadRelease flow:
make release-patch # bug fixes — bumps version, commits, pushes container
make release-minor # new features — also publishes to PyPI
make release-major # breaking changes — also publishes to PyPICreate docker-compose.yml at the project root:
services:
your-package-name:
image: ghcr.io/your_github_username/your-package-name:latest
ports:
- "8000:8000"Teammates only need Docker Desktop. They do not need Python, Poetry, or a clone of the repository. They can get the compose file and start the container with two commands:
curl -O https://raw.githubusercontent.com/YOUR_USERNAME/YOUR_REPO/main/docker-compose.yml
docker compose upTo stop:
docker compose down| Step | What it does |
|---|---|
| Dockerfile | Defines how the image is built |
| GHCR | Hosts the image publicly |
| Buildx | Builds for both ARM and AMD64 in one step |
| Version tags | Gives you rollback points |
| Makefile | Automates the full release cycle |
| docker-compose.yml | Lets teammates run the app with one command |