Lean, tested, multi-arch Docker images for Salesforce CI/CD and development.
Three purpose-built images for Salesforce work — a minimal CI runner, a full VS Code
devcontainer, and an ultra-light Alpine image for bulk data operations. All are multi-arch
(linux/amd64 + linux/arm64), version-pinned, gated by a test suite, and scanned on every
build.
| Image | Base | Purpose | Size (uncompressed) | Use it when… |
|---|---|---|---|---|
sf-ci |
ubuntu:22.04 |
Minimal CI/CD runner (Node 24 + Java 17 + SF CLI) | ~840 MB | you run deploys, Apex tests, or delta packaging in a pipeline |
sf-devcontainer |
ubuntu:24.04 |
Full VS Code dev environment (zsh, editors, extra plugins) | ~2.6 GB | you develop Salesforce locally in VS Code / Dev Containers |
sf-bulk |
node:24-alpine |
Ultra-light bulk ops, no Java | ~435 MB (< 600 MB) | you do high-volume sf data / Bulk API work and don't need Apex |
All three images ship Node.js 24 and Salesforce CLI v2
(@salesforce/cli@2.x, latest at build time) with the sfdx-git-delta plugin. See
the image decision guide.
All three are published to Docker Hub under gforceinnovation:
docker pull gforceinnovation/sf-ci:latest # minimal CI runner
docker pull gforceinnovation/sf-devcontainer:latest # VS Code devcontainer
docker pull gforceinnovation/sf-bulk:latest # Alpine bulk-ops imageOr extend one in your own Dockerfile:
FROM gforceinnovation/sf-ci:1.7.0flowchart TD
A[What are you doing?] --> B{Developing locally<br/>in VS Code?}
B -- Yes --> C[sf-devcontainer]
B -- No --> D{Need Apex compile /<br/>code-analyzer / Java?}
D -- Yes --> E[sf-ci]
D -- No --> F{High-volume data /<br/>Bulk API only?}
F -- Yes --> G[sf-bulk]
F -- No --> E
jobs:
deploy:
runs-on: ubuntu-latest
container: gforceinnovation/sf-ci:latest
steps:
- uses: actions/checkout@v4
- name: Authenticate to Salesforce
run: |
echo "${{ secrets.SF_AUTH_URL }}" > authfile
sf org login sfdx-url --sfdx-url-file authfile
- name: Deploy
run: sf project deploy startAdd .devcontainer/devcontainer.json (already present at the repo root as an example), then
run Dev Containers: Reopen in Container from the Command Palette:
{
"name": "Salesforce Development",
"image": "gforceinnovation/sf-devcontainer:latest",
"customizations": {
"vscode": { "extensions": ["salesforce.salesforcedx-vscode"] }
}
}docker run --rm -v "$(pwd):/workspace" gforceinnovation/sf-bulk:latest sf org listCopy-paste recipes for sfdx projects — zero-install dev shell, org auth from a
container, CI-parity script testing (Windows-friendly), bulk data ops:
see examples/.
Every version release publishes exactly two tags per image:
| Tag | Moves? | Points at |
|---|---|---|
1.7.0 |
immutable | one exact release — pin this in production |
latest |
moving | most recent release |
Pin an immutable tag (gforceinnovation/sf-ci:1.7.0) for reproducible pipelines; track
:latest to pick up updates automatically. Rolling major/minor tags (:1, :1.6) are no
longer published — existing ones stay frozen at 1.6.1. The tags are generated by CI from the
pushed git tag (see .github/workflows/release.yml).
Every build in CI:
-
Scans each image with Trivy; results are uploaded to GitHub Security (code scanning).
-
Generates an SBOM and provenance attestations on push, so consumers can verify how and from what each image was built.
-
Signs every pushed image with cosign (keyless, GitHub OIDC); the signature is recorded in the Rekor transparency log. Verify any image with:
cosign verify \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp \ '^https://github\.com/Gforce-Innovation-Kft/sf-docker-images/\.github/workflows/reusable-docker-image-build\.yml@.+$' \ gforceinnovation/sf-ci:latestThe identity is the CI workflow that built and pushed the image — no keys to manage or leak. Swap in
sf-devcontainer/sf-bulkand any published tag.Images published before 2026-08-06 were signed while the build workflow lived in
shared-github-actions. Verify those against the old identity instead:^https://github\.com/Gforce-Innovation-Kft/shared-github-actions/\.github/workflows/docker-build-test-push\.yml@.+$
Found a vulnerability? See SECURITY.md.
- Thin by default —
sf-cistays minimal (no editors, no zsh); tests fail the build if forbidden tools appear.sf-bulkis kept under 600 MB with no Java. - Reproducible — base images and toolchains are pinned; immutable version tags are published alongside moving ones.
- Multi-arch —
linux/amd64andlinux/arm64from a single build. - Tested — every image has a pytest-testinfra suite asserting OS, user, runtimes, plugins, tools, env vars, and size budgets. Nothing ships unless the suite is green.
- Non-root at runtime, container-mode aware — all three images run as a non-root user
(
ci/vscode, UID 1000), not just build as one; SF CLI is configured for containers (SFDX_CONTAINER_MODE, telemetry/auto-update disabled). Each image also registers UID 1001 (runner) so GitHub Actions container jobs can run unprivileged withoptions: --user 1001— see the per-image READMEs.
./scripts/setup.sh # verify Docker + Python + gh, install test deps
docker build -t sf-ci:local ./sf-ci
docker build -t sf-devcontainer:local ./sf-devcontainer
docker build -t sf-bulk:local ./sf-bulk
pip install -r tests/requirements.txt
pytest tests/ -v # all images
pytest tests/test_sf_bulk.py -v # a single imageMulti-platform build and push (requires buildx):
docker buildx create --name multiplatform --use
docker buildx build --platform linux/amd64,linux/arm64 \
--tag gforceinnovation/sf-ci:latest --push ./sf-ciSee CONTRIBUTING.md for the full workflow and CHANGELOG.md
(Keep a Changelog) for release history.
git tag -a v1.5.0 -m "Release v1.5.0"
git push origin v1.5.0CI then builds all three images multi-arch, runs the tests + Trivy scan, pushes to Docker Hub
(tags X.Y.Z + latest) with SBOM, provenance, and a keyless cosign signature, and opens a
GitHub Release with notes drawn from the matching CHANGELOG.md section plus per-image
tool-version tables (Node, npm, SF CLI, plugins) read from the built images. The per-image
pipeline lives in this repo's own
reusable-docker-image-build.yml;
release.yml is a thin matrix caller over it.
Each image has its own workflow (image-sf-ci.yml, image-sf-devcontainer.yml,
image-sf-bulk.yml), filtered by path, so touching sf-bulk/Dockerfile builds and tests
sf-bulk alone, a docs-only PR builds nothing, and the images that do build run in parallel.
Changes to a shared input — tests/requirements.txt or the reusable workflow — build
everything. Release tags always build the full set so latest stays consistent across images.
PRs that touch sf-ci additionally run an end-to-end gate: the freshly built image is published to a throwaway tag and a real downstream Salesforce release pipeline is run against it, unprivileged, before the PR can go green. That catches breakage the container test suite cannot see — an image can be internally perfect and still fail as a GitHub Actions container job.
This repo is developed with Claude Code against a disciplined,
committed context — a differentiator, not a shortcut. The loop is
CLAUDE.md → references → skills → tests → release:
CLAUDE.md— project overview, commands, and change rules Claude reads first..claude/references/— rules to read before generating code: docker-best-practices, image-conventions (per-image size budgets + allowed/forbidden tools), github-actions, devops..claude/skills/— repo skills:building-a-docker-image,testing-images,releasing, andworking-in-the-devcontainer(vendored, attributed).- Tests — pytest-testinfra suites gate every change and the CI pipeline.
Issues and PRs are welcome — see CONTRIBUTING.md and our
CODE_OF_CONDUCT.md. Commits follow
Conventional Commits.
MIT © Gforce Innovation Kft — maintained by @gambe94.