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)