When Portainer Community Edition deploys a stack from a git repository, it clones the repo, runs docker compose up, and then deletes the .git directory. This means the usual git rev-parse HEAD trick doesn’t work inside the running container – there’s no git history left.

But Portainer does remember the commit it deployed. The stack API endpoint exposes it in GitConfig.ConfigHash:

bash
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
  "$PORTAINER_URL/api/stacks/$PORTAINER_STACK_ID" \
  | python -m json.tool | grep ConfigHash
json
"ConfigHash": "5d346dc4e8b34235c11950e123358ebd85cfa3e8"

This is the exact commit SHA that Portainer cloned when it last (re)deployed the stack (here’s how the update mechanism works). You can query it at container startup to bake the version into your app. Here’s a minimal snippet that writes it to a file (using only Python stdlib, no extra dependencies needed in a slim image):

python
import urllib.request, json, os

url = os.environ['PORTAINER_URL'].rstrip('/') + '/api/stacks/' + os.environ['PORTAINER_STACK_ID']
req = urllib.request.Request(url, headers={'X-API-Key': os.environ['PORTAINER_API_KEY']})
data = json.load(urllib.request.urlopen(req, timeout=5))
sha = data.get('GitConfig', {}).get('ConfigHash', '')
if sha:
    open('/app/.commit_sha', 'w').write(sha + '\n')

A read-only API key is sufficient – you can create one under My account > Access tokens in the Portainer UI. The stack ID is the number visible in the stack’s URL (e.g. for #!/2/docker/stacks/mystack?id=42&type=2 the stack ID is 42).

I tried a few other approaches first (fetching the SHA from the GitHub API during docker build, baking it via a multi-stage Dockerfile), but they all had a race condition: if the branch advances between Portainer’s clone and the API call, you’d get the wrong hash. Querying Portainer’s own ConfigHash avoids this entirely because it records what was actually deployed.