DEV Community

Cover image for Store your OpenTofu State in GitHub Container Registry (GHCR)
Victor M. Varela
Victor M. Varela

Posted on • Edited on

Store your OpenTofu State in GitHub Container Registry (GHCR)

When OpenTofu 1.10 announced OCI support for providers, many thought: "What about state?" Until now, nothing. So I created Ghoten — a focused OpenTofu fork that adds a native ORAS backend and a ready-made GitHub Action.

Name origin: Ghoten blends Git*Hub and OpenTofu, with a nod to Goten from *Dragon Ball Z.

The Problem

153 MB of tfstate in S3 + DynamoDB for locking = overkill for many projects. If your team already has:

  • An OCI registry in use (such as GHCR)
  • GitHub Actions deploying Terraform/OpenTofu code

Why add another service and its cost?

The Solution: ORAS Backend

Ghoten adds a native oras backend that stores state as OCI artifacts:

terraform {
  backend "oras" {
    repository = "ghcr.io/acme/infra-state"
  }
}
Enter fullscreen mode Exit fullscreen mode

That's the minimal config — one required setting. For production, add sensible defaults:

terraform {
  backend "oras" {
    repository   = "ghcr.io/myorg/infra-state"
    compression  = "gzip"
    lock_ttl     = 300
    max_versions = 10
  }
}
Enter fullscreen mode Exit fullscreen mode

lock_ttl auto-clears stale locks after crashed jobs, and max_versions keeps history without unbounded growth.

Features

  • ✅ State, locks, and versions stored as OCI artifacts (content-addressable)
  • ✅ Distributed locking with TTL-based stale lock cleanup (no DynamoDB needed)
  • ✅ Configurable versioning and retention
  • ✅ Retry with exponential backoff for transient errors
  • ✅ Rate limiting to respect registry quotas
  • ✅ Uses Docker credential helpers and ghoten login / Terraform-style host tokens
  • ✅ Compatible with OpenTofu's native encryption
  • ✅ TLS support with custom CA bundles
  • ✅ Verified against GHCR and Zot; any OCI-compliant registry expected to work

GitHub Action

The fastest path to OCI-backed state in CI. Install, auth, init, run, PR comments, and job summaries — in one step:

name: Infra Plan
on: pull_request

jobs:
  plan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v6
      - uses: vmvarela/ghoten@v1
Enter fullscreen mode Exit fullscreen mode

That's it. Defaults to plan, posts a PR comment, and writes a Job Summary.

For a plan-on-PR / apply-on-merge workflow:

name: Infrastructure
on:
  pull_request:
  push:
    branches: [main]

jobs:
  plan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    permissions: { contents: read, packages: write, pull-requests: write }
    steps:
      - uses: actions/checkout@v6
      - uses: vmvarela/ghoten@v1

  apply:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions: { contents: read, packages: write }
    steps:
      - uses: actions/checkout@v6
      - uses: vmvarela/ghoten@v1
        with:
          command: apply
Enter fullscreen mode Exit fullscreen mode

Installation

Build from source

git clone https://github.com/vmvarela/ghoten.git
cd ghoten
make build
./ghoten version
Enter fullscreen mode Exit fullscreen mode

Docker image

docker pull ghcr.io/vmvarela/ghoten:<version>
Enter fullscreen mode Exit fullscreen mode

Example with Encryption

terraform {
  backend "oras" {
    repository   = "ghcr.io/myorg/infra-state"
    compression  = "gzip"
    lock_ttl     = 300
    max_versions = 10
  }

  encryption {
    key_provider "pbkdf2" "main" {
      passphrase = var.state_passphrase
    }
    method "aes_gcm" "main" {
      key_provider = key_provider.pbkdf2.main
    }
    state { method = method.aes_gcm.main }
    plan  { method = method.aes_gcm.main }
  }
}
Enter fullscreen mode Exit fullscreen mode

For production systems, prefer a KMS-backed key provider over PBKDF2 passphrases.

Use Cases

Perfect for:

  • Startups with lean infra-as-code
  • Personal/side projects with free GHCR
  • Air-gapped environments (registry mirror)
  • Teams already invested in the OCI ecosystem
  • Multi-cloud projects that want one backend

Configuration Reference

Parameter Default Notes
repository Required. OCI repo (<registry>/<repo>)
compression none none or gzip
lock_ttl 0 Seconds; > 0 enables stale-lock cleanup
max_versions 0 Historical state versions to retain
retry_max 2 Retry count for transient errors
retry_wait_min 1 Backoff min (seconds)
retry_wait_max 30 Backoff max (seconds)
rate_limit 0 Requests/sec (0 = unlimited)
insecure false Skip TLS verification
ca_file PEM CA bundle path

Most settings also accept environment variables (TF_BACKEND_ORAS_*).

Feedback Wanted

What do you think? Would you store state in OCI registries instead of S3? What features would you need to adopt this?

Repo: github.com/vmvarela/ghoten

Top comments (0)