Skip to main content

Cheat sheet

The least configuration that gets each kind of client talking to LocalS3, with one snippet you can run. Each section links to the reference page with every option and every caveat.

The snippets assume a LocalS3 on localhost:29090, plain HTTP, and the key pair admin / admin. A LocalS3 started without credentials accepts any key pair, so the snippets work against it unchanged.

Start LocalS3​

docker run -d -p 29090:29090 -e LOCAL_S3_MODE=IN_MEMORY -e AWS_BUCKETS=demo linuxchina/local-s3
AddFor
-e LOCAL_S3_ICEBERG_CATALOG=truethe Iceberg REST catalog at /iceberg
-e LOCAL_S3_TLS_SELF_SIGNED=trueHTTPS on the same port, for clients that insist on TLS
-e LOCAL_S3_ACCESS_KEY_ID=admin -e LOCAL_S3_SECRET_ACCESS_KEY=adminrequiring signed requests
-e LOCAL_S3_MODE=PERSISTENCE -v $PWD/data:/datadata that survives a restart; this is the default of the image

Without Docker: java -jar s3.jar --port 29090 --buckets demo. See Deployment for every variable.

The console at http://localhost:29090/_admin/ui shows what is stored. Under Connect it shows the snippets below, filled in for this service.

AWS CLI and s5cmd​

eval "$(curl -s http://localhost:29090/_admin/snippets/env)"
aws s3 cp README.md s3://demo/
s5cmd ls 's3://demo/*'

The snippet exports AWS_ENDPOINT_URL, the region and the credentials of the service. To set them by hand: aws --endpoint-url http://localhost:29090 ... and s5cmd --endpoint-url http://localhost:29090 ..., with any AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION.

DuckDB​

One line, with the secret written by the service itself:

duckdb -init <(curl -s http://localhost:29090/_admin/snippets/duckdb)

Or by hand. USE_SSL false is the line people forget, because DuckDB uses HTTPS by default:

CREATE SECRET local_s3 (TYPE s3, ENDPOINT 'localhost:29090', URL_STYLE 'path', USE_SSL false,
KEY_ID 'admin', SECRET 'admin', REGION 'us-east-1');

COPY (SELECT * FROM range(1000)) TO 's3://demo/numbers.parquet';
SELECT count(*) FROM 's3://demo/*.parquet';

Reference: DuckDB, DuckLake, DuckDB on the built-in Iceberg catalog.

JUnit 5​

<dependency>
<groupId>org.mvnsearch</groupId>
<artifactId>local-s3-jupiter</artifactId>
<version>last_version</version>
<scope>test</scope>
</dependency>
@LocalS3
class AppTest {

@Test
void test(S3Client s3) {
s3.createBucket(b -> b.bucket("demo"));
}
}

No Docker is needed: the service runs in the test JVM, one per annotated class or method. @LocalS3(icebergCatalog = true) also serves the Iceberg catalog, and LocalS3Endpoint can be injected for its URIs. Reference: JUnit 5.

Spring Boot​

Only the dependency, for tests and local runs, so that the production jar leaves it out:

dependencies {
testAndDevelopmentOnly 'org.mvnsearch:local-s3-spring-boot-starter:last_version'
implementation 'software.amazon.awssdk:s3' // for the S3Client bean
}
local-s3:
buckets: [uploads]
@Service
class Uploads {

private final S3Client s3; // points at the embedded LocalS3

Uploads(S3Client s3) {
this.s3 = s3;
}

@EventListener
void onChange(S3Change change) { // every committed change, e.g. an upload by another process
// ...
}
}

In tests, @SpringBootTest @AutoConfigureLocalS3 uses a random port and resets the data after each test method. Reference: the starter's README, Spring Boot.

Testcontainers​

@Container
static LocalS3Container localS3 = new LocalS3Container("latest").withBuckets("demo");

S3Client s3 = S3Client.builder()
.endpointOverride(localS3.getEndpointUri())
.forcePathStyle(true)
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(localS3.getAccessKey(), localS3.getSecretKey())))
.build();

With Spring Boot and Spring Cloud AWS, @Bean @ServiceConnection LocalS3Container is the whole configuration. Reference: Testcontainers, @ServiceConnection.

Iceberg with Spark​

Start LocalS3 with LOCAL_S3_ICEBERG_CATALOG=true. The catalog URI is then the whole configuration: the catalog vends the S3 endpoint, path-style access and the credentials.

spark-sql \
--packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.10.0,org.apache.iceberg:iceberg-aws-bundle:1.10.0 \
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \
--conf spark.sql.catalog.local=org.apache.iceberg.spark.SparkCatalog \
--conf spark.sql.catalog.local.type=rest \
--conf spark.sql.catalog.local.uri=http://localhost:29090/iceberg \
--conf spark.sql.defaultCatalog=local
CREATE NAMESPACE db;
CREATE TABLE db.events (id BIGINT, name STRING) USING iceberg;
INSERT INTO db.events VALUES (1, 'a'), (2, 'b');
SELECT * FROM db.events;

The Iceberg catalog is new in 2.5.0. Until that is released, build the image from this repository, as examples/lakehouse does; that example runs Spark and Trino on the same tables with docker compose up. Reference: the built-in Iceberg REST catalog, Apache Iceberg for a catalog of your own.

PyIceberg​

With the Iceberg catalog on, as above, and pip install "pyiceberg[pyarrow]":

import pyarrow as pa
from pyiceberg.catalog import load_catalog

catalog = load_catalog("local", type="rest", uri="http://localhost:29090/iceberg")
catalog.create_namespace("db")
table = catalog.create_table("db.events", schema=pa.schema([("id", pa.int64()), ("name", pa.string())]))
table.append(pa.table({"id": [1, 2], "name": ["a", "b"]}))
print(table.scan(row_filter="id > 1").to_arrow())

Reference: PyIceberg.

delta-rs and Polars​

With pip install deltalake polars, and the bucket delta created first, e.g. with AWS_BUCKETS=delta:

import polars as pl
from deltalake import DeltaTable, write_deltalake

storage_options = {
"AWS_ENDPOINT_URL": "http://localhost:29090",
"AWS_ACCESS_KEY_ID": "admin",
"AWS_SECRET_ACCESS_KEY": "admin",
"AWS_REGION": "us-east-1",
"AWS_ALLOW_HTTP": "true",
"conditional_put": "etag",
}
df = pl.DataFrame({"id": [1, 2], "name": ["a", "b"]})
write_deltalake("s3://delta/events", df.to_arrow(), storage_options=storage_options)
print(DeltaTable("s3://delta/events", storage_options=storage_options).to_pandas())
print(pl.read_delta("s3://delta/events", storage_options=storage_options))

conditional_put = etag makes each commit a PUT with If-None-Match: *, so no lock table is needed. Reference: delta-rs and Polars, Delta Lake for Spark and delta-kernel.

Common errors​

ErrorCauseFix
DuckDB SSL connect error ... https://localhost:29090/...DuckDB uses HTTPS by defaultUSE_SSL false in the secret, or serve HTTPS
NoSuchBucket on the first write, e.g. of DuckLake or delta-rsThe bucket doesn't existAWS_BUCKETS=..., local-s3.buckets or aws s3 mb
InvalidBucketNameBucket names have 3 to 63 characters, like on Amazon S3Use a longer name
The host demo.<endpoint> can't be resolved or connected toThe client sends virtual-hosted-style requests, which only work for localhost, 127-0-0-1.nip.io and the configured virtual host domainsPath-style: URL_STYLE 'path', forcePathStyle(true), s3.path-style-access=true; the endpoint http://127-0-0-1.nip.io:29090, see virtual-hosted-style requests; or LOCAL_S3_VIRTUAL_HOST_DOMAINS
403 RequestTimeTooSkewedThe clock of the client is more than 15 minutes offLOCAL_S3_ALLOWED_CLOCK_SKEW=1h, or 0 to turn the check off (presigned URLs then don't expire either)
507 InsufficientStorageIN_MEMORY mode holds at most a quarter of the heap by defaultLOCAL_S3_IN_MEMORY_MAX_BYTES, or PERSISTENCE mode
501 NotImplementedThe operation isn't implementedSee Supported APIs; GET /_admin/stats counts them under notImplemented