Any Cloud. Locally.
Light, fluffy, and always free, now for Oracle Cloud
No account. No API key ceremony. No feature gates. Just docker compose up.
Quick Start · Features · Services · SDKs · Terraform · Docs
floci-oci is a free, open-source local Oracle Cloud Infrastructure (OCI) emulator for development, testing, and CI.
It gives you OCI-shaped services on your machine without an Oracle Cloud account, uploaded API keys, or paid feature gates. Point the OCI SDKs, the OCI CLI, Terraform, or OpenTofu at http://localhost:4599 and keep your existing workflows.
floci-oci is the OCI member of the Floci emulator family, named after floccus, the cloud formation that looks like popcorn.
| Emulator | Cloud | Port |
|---|---|---|
| floci | AWS | 4566 |
| floci-az | Azure | 4577 |
| floci-gcp | GCP | 4588 |
| floci-oci | OCI | 4599 |
Create a compose.yaml file:
services:
floci-oci:
image: floci/floci-oci:latest
ports:
- "4599:4599"Start floci-oci:
docker compose upUse your existing OCI tools normally:
oci os ns get --endpoint http://localhost:4599
oci os bucket create --endpoint http://localhost:4599 \
--compartment-id ocid1.tenancy.oc1..flocilocaltenancy0000000000000000000000000000000000000000 \
--namespace floci-local --name my-bucket
oci iam compartment list --endpoint http://localhost:4599 \
--compartment-id ocid1.tenancy.oc1..flocilocaltenancy0000000000000000000000000000000000000000Any locally generated API key works: floci-oci parses the request signature for tenancy and user context but never verifies it. See OCI CLI & SDK Setup for a one-time throwaway config.
Prefer building from source?
Requirements: JDK 25.
./mvnw quarkus:dev # dev mode on port 4599
# or
./mvnw clean package -DskipTests
java -jar target/quarkus-app/quarkus-run.jarVerify it is up:
curl http://localhost:4599/_floci-oci/healthBundled CLI wrapper
bin/ocilocal injects the emulator endpoint into every OCI CLI call:
bin/ocilocal os ns get
bin/ocilocal iam region listLocal OCI without the cloud account
Run OCI-compatible services locally without an Oracle Cloud account, uploaded API keys, or paid feature gates.
Real OCI wire protocols
Real request and response shapes: opc-request-id on every response, opc-next-page pagination headers, etag / if-match conditional requests, OCIDs, work requests for async operations, and OCI's exact error bodies (NotAuthorizedOrNotFound, BucketNotFound, …).
Drop-in SDK, CLI, and IaC compatibility
The official oci-java-sdk, Python SDK, OCI CLI, and the oracle/oci Terraform provider work unchanged, validated continuously by the compatibility suite.
Fast enough for CI
Built on Quarkus with GraalVM native-image support, so it starts fast and stays light in test pipelines.
Configurable persistence
Choose from in-memory, persistent, hybrid, and write-ahead log storage depending on the durability profile you need.
Oracle Cloud has no official local emulator: no LocalStack equivalent, no all-in-one dev container. Testing OCI integrations means a real tenancy, real credentials, and real network round-trips, even in CI.
floci-oci fills that gap the same way its siblings do for AWS, Azure, and GCP: one container, one port, real wire protocols, MIT licensed, free forever.
flowchart LR
Client["OCI SDK / CLI / Terraform"]
subgraph FlociOci ["floci-oci, port 4599"]
Router["HTTP Router\nJAX-RS"]
subgraph Services ["Services"]
A["Identity /20160918\nObject Storage /n/…\nQueue /20210201\nStreaming /20180418"]
B["Vault · KMS · Secrets\n/20180608 · /20190301\nreal AES/RSA/ECDSA"]
C["Functions /20181201"]
D["OKE Container Engine\n/20180222"]
end
WR["Work Requests\nasync-operation plane"]
Auth["Signature parsing\ntenancy context"]
Router --> Auth --> Services
Services --> WR
Services --> Store[("StorageBackend\nmemory · hybrid · persistent · wal\ntenancy-isolated")]
end
Docker["Docker Engine"]
Client -->|"HTTP :4599\nOCI wire protocol"| Router
C -->|"invoke"| Fn["fnproject/fnserver\nsidecar"]
Fn -->|"runs your image"| Docker
D -->|"startCluster"| K3s["rancher/k3s\nsidecar"]
K3s -->|"spawns cluster"| Docker
| Category | Services |
|---|---|
| Identity | Compartments, users, groups, user-group memberships, policies, availability domains, regions, region subscriptions, tenancies |
| Storage | Object Storage: namespaces, buckets, objects, listing, rename, copy, batch delete, multipart uploads, pre-authenticated requests |
| Messaging | Queue (visibility timeouts, dead-letter queues, channels), Streaming (partitioned log, cursors, consumer groups) |
| Security | Vault + KMS (vaults, keys, key versions, real AES-GCM / RSA / ECDSA crypto), Secrets and secret bundles |
| Serverless | Functions (applications, functions, real invocation through an Fn Project sidecar) |
| Kubernetes | Container Engine for Kubernetes (OKE): clusters, node pools, options, kubeconfig generation, work requests, real-mode k3s sidecar |
| Async operations | Work requests, partitioned per service (Identity/Queue/Streaming use SUCCEEDED, Object Storage/OKE use COMPLETED) |
For operation-level compatibility, see the Services Overview.
Detailed service notes
| Service | How it works | Notable features |
|---|---|---|
| Identity (IAM) | In-process | Compartment CRUD incl. compartmentIdInSubtree listing and async delete via work request; users, groups, memberships; policies (statements stored verbatim); reference data (3 ADs, regions, tenancy); etag concurrency on every mutation |
| Object Storage | In-process | Bucket CRUD (delete requires empty); objects with Content-MD5 verification, opc-meta-* metadata, Range/206 reads, conditional headers; ListObjects with prefix/start/end/delimiter/fields and nextStartWith truncation; rename; async copy via work request; batch delete with per-object success/failure results; multipart uploads (per-part etag + opc-content-md5); pre-authenticated requests with anonymous /p/{token}/… data path |
| Queue | In-process | Work-request-driven control plane (mutations return no body); data plane with visibility timeouts, 0-visibility peeks, long polling, dead-letter queues, per-channel filtering, stats; wrapped {"items":[…]} list shape |
| Streaming | In-process | Partitioned append-only log with key-hash placement; opaque cursors (TRIM_HORIZON/LATEST/AT_OFFSET/AFTER_OFFSET/AT_TIME), group cursors with commit + resume, opc-next-cursor paging; CreateStream returns body and work request |
| Vault + KMS | In-process, real crypto | Vaults and keys with schedule/cancel deletion (no DELETE verb), key rotation; AES-GCM encrypt/decrypt whose envelope survives rotation, RSA/ECDSA sign/verify via JCA, CRC32 plaintextChecksum |
| Secrets | In-process | Secret versions with CURRENT/PREVIOUS/LATEST stages; the Secret shape never echoes content: retrieval is through /20190301/secretbundles, including the bodyless POST getByName |
| Functions | Real Docker (Fn Project) | Applications and functions with deterministic image digests; invocation proxied to a shared fnproject/fnserver sidecar that runs your real FDK image. mock: true disables Docker entirely |
| OKE (Container Engine) | Real Docker (k3s) / In-process | Control plane CRUD (/20180222), options, kubeconfig YAML generator, work requests tracking; real-mode k3s sidecar driver (rancher/k3s:v1.30.1-k3s1) binding dynamic host ports (6443..6543) and persistent volumes (/var/lib/rancher/k3s). mock: true disables sidecar |
| Work Requests | In-process | 202 + opc-work-request-id responses, pollable per service (/20160918, /20210201, /20180418, /20180222 and unversioned /workRequests) with errors/logs endpoints |
Not implemented yet: identity domains, API keys/auth tokens, dynamic groups, tag namespaces, object versioning, retention rules (stubbed empty for Terraform compatibility), lifecycle policies, replication, the Amazon S3 Compatibility API, queue consumer groups, stream pools' Kafka settings and connect harnesses, KMS import/backup/replication, secret rotation, and pre-built functions (PBF).
floci-oci runs a real container where in-process emulation would not be faithful.
| Service | Default image | What is real |
|---|---|---|
| Functions | fnproject/fnserver:latest |
The open-source engine OCI Functions is built on. Your function image runs for real: fnserver spawns it as a sibling container and speaks the FDK http-stream contract |
| OKE | rancher/k3s:v1.30.1-k3s1 |
Real local Kubernetes cluster execution via a k3s sidecar container with dynamic host port mapping (6443..6543) and named data volume /var/lib/rancher/k3s |
Docker-backed services need the Docker socket:
docker run -d --name floci-oci \
-p 4599:4599 \
-v /var/run/docker.sock:/var/run/docker.sock \
floci/floci-oci:latestSet FLOCI_OCI_SERVICES_FUNCTIONS_MOCK=true or FLOCI_OCI_SERVICES_OKE_MOCK=true to skip Docker entirely. The management
plane stays fully usable and invocations/cluster records return synthetic data. That is the default in
the test suite, so ./mvnw test never needs a Docker daemon.
services/functions/ is also the reference implementation for contributing a
sidecar-backed service; see the "Services with Container Sidecars" section of
AGENTS.md.
floci-oci can trade speed for durability depending on the workflow. Configure the default mode with FLOCI_OCI_STORAGE_MODE, or override storage per service.
| Mode | Behavior | Best for | Durability |
|---|---|---|---|
memory |
Entirely in RAM. Data is lost when the container stops. | CI and ephemeral tests | None |
persistent |
Loaded at startup and flushed to disk immediately on every write operation. | Simple local state preservation with immediate persistence | Medium |
hybrid |
In-memory performance with periodic async flushing every 5 seconds. | Local development | Good |
wal |
Write-ahead log. Every mutation is logged before responding. | Maximum durability | Highest |
Per-service overrides use the map form: FLOCI_OCI_STORAGE_SERVICES_OBJECTSTORAGE_MODE=wal.
For more detail, see the Storage Configuration documentation.
floci-oci supports per-tenancy resource isolation with no extra setup. The tenancy OCID in your signing key's keyId is the storage partition: requests signed with different tenancy OCIDs see fully isolated resources.
# Two profiles with different tenancy OCIDs see independent worlds
oci --profile TENANCY_A iam user list --endpoint http://localhost:4599
oci --profile TENANCY_B iam user list --endpoint http://localhost:4599Unsigned requests fall back to FLOCI_OCI_DEFAULT_TENANCY_ID. Compartments are a field on each resource (filtered via compartmentId), exactly as on real OCI. Tenancy is the isolation boundary, compartments are the organizational one.
Point your existing OCI SDK at http://localhost:4599. Any locally generated RSA key works: the signature is parsed for tenancy/user context, never verified.
Java, oci-java-sdk
KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
var auth = SimpleAuthenticationDetailsProvider.builder()
.tenantId("ocid1.tenancy.oc1..flocilocaltenancy0000000000000000000000000000000000000000")
.userId("ocid1.user.oc1..anyuser")
.fingerprint("aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99")
.privateKeySupplier(() -> new ByteArrayInputStream(pemOf(pair)))
.region(Region.US_ASHBURN_1)
.build();
ObjectStorageClient client = ObjectStorageClient.builder().build(auth);
client.setEndpoint("http://localhost:4599");
String namespace = client.getNamespace(GetNamespaceRequest.builder().build()).getValue();
client.createBucket(CreateBucketRequest.builder()
.namespaceName(namespace)
.createBucketDetails(CreateBucketDetails.builder()
.name("my-bucket")
.compartmentId(auth.getTenantId())
.build())
.build());Python, oci
import oci
config = {
"user": "ocid1.user.oc1..anyuser",
"fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99",
"tenancy": "ocid1.tenancy.oc1..flocilocaltenancy0000000000000000000000000000000000000000",
"region": "us-ashburn-1",
"key_file": "~/.oci/floci_key.pem", # any locally generated key
}
client = oci.object_storage.ObjectStorageClient(
config, service_endpoint="http://localhost:4599")
namespace = client.get_namespace().data
client.put_object(namespace, "my-bucket", "hello.txt", b"hello from floci-oci")
print(client.get_object(namespace, "my-bucket", "hello.txt").data.content)Go, oci-go-sdk
package main
import (
"context"
"fmt"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/containerengine"
)
func main() {
provider := common.NewRawConfigurationProvider(
"ocid1.tenancy.oc1..flocilocaltenancy0000000000000000000000000000000000000000",
"ocid1.user.oc1..anyuser",
"us-ashburn-1",
"aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99",
"-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
nil,
)
client, err := containerengine.NewContainerEngineClientWithConfigurationProvider(provider)
if err != nil {
panic(err)
}
client.Host = "http://localhost:4599"
resp, err := client.CreateCluster(context.Background(), containerengine.CreateClusterRequest{
CreateClusterDetails: containerengine.CreateClusterDetails{
CompartmentId: common.String("ocid1.tenancy.oc1..flocilocaltenancy0000000000000000000000000000000000000000"),
Name: common.String("go-demo-cluster"),
VcnId: common.String("ocid1.vcn.oc1.iad.demovcn"),
KubernetesVersion: common.String("v1.30.1"),
},
})
if err != nil {
panic(err)
}
fmt.Println("Cluster OCID:", *resp.Cluster.Id)
}Bash, OCI CLI
# One-time throwaway profile
mkdir -p ~/.oci && openssl genrsa -out ~/.oci/floci_key.pem 2048
cat >> ~/.oci/config <<'EOF'
[FLOCI]
user=ocid1.user.oc1..flocilocaluser0000000000000000000000000000000000000000000000
fingerprint=aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99
tenancy=ocid1.tenancy.oc1..flocilocaltenancy0000000000000000000000000000000000000000
region=us-ashburn-1
key_file=~/.oci/floci_key.pem
EOF
oci --profile FLOCI os ns get --endpoint http://localhost:4599The official oracle/oci provider works against floci-oci through its per-client host overrides, with no provider fork and no wrapper:
export TF_VAR_CLIENT_HOST_OVERRIDES="oci_identity.IdentityClient=http://localhost:4599;oci_object_storage.ObjectStorageClient=http://localhost:4599"provider "oci" {
tenancy_ocid = "ocid1.tenancy.oc1..flocilocaltenancy0000000000000000000000000000000000000000"
user_ocid = "ocid1.user.oc1..anyuser"
fingerprint = "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99"
private_key_path = "./any_local_key.pem"
region = "us-ashburn-1"
}
resource "oci_objectstorage_bucket" "demo" {
compartment_id = "ocid1.tenancy.oc1..flocilocaltenancy0000000000000000000000000000000000000000"
namespace = "floci-local"
name = "demo-bucket"
}The full apply → plan (zero drift) → destroy cycle is validated in CI for both Terraform and OpenTofu. See compatibility-tests/compat-terraform for a working example.
The compatibility-tests directory validates floci-oci with real SDKs and IaC tooling, not just theoretical protocol adherence.
| Module | Language / Tool | SDK / Client | Tests |
|---|---|---|---|
sdk-test-java |
Java | oci-java-sdk (incl. OKE integration) | 20 |
sdk-test-python |
Python 3 | oci (incl. UploadManager streaming) |
20 |
sdk-test-go |
Go | oci-go-sdk (OKE container engine) | 1 |
sdk-test-cli |
Bash / BATS | OCI CLI 3.89.3 (all 8 services, incl. error codes and PAR access) | 54 |
compat-terraform |
Terraform | oracle/oci provider (12 resources, zero-drift plan) | 6 |
compat-opentofu |
OpenTofu | oracle/oci provider (12 resources, zero-drift plan) | 6 |
Note
sdk-test-go currently focuses on OKE (Container Engine for Kubernetes) compatibility verification; coverage for additional OCI services will be added in future releases.
Run everything in Docker against a compose-built emulator:
make compat-dockerOr a single suite against a locally running emulator: make test-java-compat, make test-python-compat, make test-terraform-compat, make test-opentofu-compat.
Every tag combines a variant and a channel.
| Channel | Standard | Compat with OCI CLI and Python SDK |
|---|---|---|
| Release, floating | latest |
latest-compat |
| Release, pinned | x.y.z |
x.y.z-compat |
| Nightly, floating | nightly |
nightly-compat |
| Nightly, dated | nightly-mmddyyyy |
nightly-mmddyyyy-compat |
Use latest for stable releases, a pinned version for reproducible builds, and nightly to track main.
# Recommended
image: floci/floci-oci:latest
# Includes the OCI CLI and the oci Python SDK
image: floci/floci-oci:latest-compat
# Pinned release
image: floci/floci-oci:x.y.z
# Track main
image: floci/floci-oci:nightlyThe compat image ships a throwaway API key and config, so the CLI works with no setup:
docker run -d --name floci-oci -p 4599:4599 floci/floci-oci:latest-compat
docker exec floci-oci ocilocal os ns getocilocal is a thin wrapper that points oci at the emulator. The bundled key exists
only because the OCI CLI refuses to run without one. floci-oci parses request
signatures but never verifies them, so the key is public by design. Never point that
config at real OCI.
Stable releases ship on the 1st and 3rd Tuesday of each month. Between trains, floci/floci-oci:nightly tracks main. Every merged fix is available the next day, and dated nightly-mmddyyyy tags let you pin a specific night's build.
Versions are derived from Conventional Commits by semantic-release; CHANGELOG.md is generated, never hand-edited. Releases are cut from main only: there are no maintenance branches.
All settings are overridable through environment variables with the FLOCI_OCI_ prefix.
| Variable | Default | Description |
|---|---|---|
FLOCI_OCI_PORT |
4599 |
Port exposed by the floci-oci API |
FLOCI_OCI_DEFAULT_REGION |
us-ashburn-1 |
Region used in OCIDs and reference data |
FLOCI_OCI_DEFAULT_REALM |
oc1 |
Realm key used when minting OCIDs |
FLOCI_OCI_DEFAULT_TENANCY_ID |
ocid1.tenancy.oc1..flocilocal… |
Tenancy used for unsigned requests |
FLOCI_OCI_DEFAULT_NAMESPACE |
floci-local |
Object Storage namespace |
FLOCI_OCI_BASE_URL |
http://localhost:4599 |
Base URL used when floci-oci returns service URLs |
FLOCI_OCI_HOSTNAME |
Unset | Hostname used in returned URLs when floci-oci runs inside Docker Compose |
FLOCI_OCI_STORAGE_MODE |
memory |
Storage mode: memory, persistent, hybrid, or wal |
FLOCI_OCI_STORAGE_PERSISTENT_PATH |
./data |
Directory used for persisted state |
FLOCI_OCI_AUTH_REQUIRE_SIGNATURE |
false |
Reject unsigned requests with 401 NotAuthenticated |
FLOCI_OCI_TLS_ENABLED |
false |
Serve HTTPS + HTTP on the same port |
Full reference: configuration docs
When your application runs in a different container, set FLOCI_OCI_HOSTNAME to the floci-oci service name so returned URLs resolve correctly.
services:
floci-oci:
image: floci/floci-oci:latest
ports:
- "4599:4599"
environment:
- FLOCI_OCI_HOSTNAME=floci-oci
my-app:
environment:
- OCI_ENDPOINT=http://floci-oci:4599
depends_on:
- floci-ociJoin the Floci community on Slack or GitHub Discussions. Feature ideas, compatibility questions, design tradeoffs, and rough proposals are welcome.
Floci is independent open source, funded by the people and companies who use it. Sponsorship buys gratitude and nothing else: every emulated service is free for everyone, forever, and no sponsor gets features, priority, or roadmap influence that the rest of the Flock does not.
Large logo with top placement in the emulator READMEs and on floci.io, plus a mention in release notes.
Logo in the emulator READMEs and on floci.io, plus a mention in release notes.
Your logo here. Become a sponsor.
Name in the emulator READMEs, a sponsor badge on GitHub, and our sincere thanks.
Every sponsor, including the Friends of the Flock who support Floci outside these tiers, is listed in THANKS.md.
MIT. Use it however you want.