Showing posts with label testcontainers. Show all posts
Showing posts with label testcontainers. Show all posts

Monday, April 13, 2026

Alternatives for LocalStack in TestContainers with Spring Boot

Introduction

Since around 20 March 2026 the company behind LocalStack decided to not support the community edition of LocalStack anymore, only paid versions are now available. 


A lot of forks were made from that now-frozen community repository, like this one.  Will any of these become the new community supported version?

Many teams use LocalStack within TestContainers in a Spring Boot application for example. What are the alternatives? This post is about exploring those.

Note that you can also pin the version of LocalStack to the last community edition release 4.14.0, that will keep on working after 6 april 2026. But there won’t be any updates on it of course…

Two forms are emerging: either replace the whole AWS stack as much as possible with one framework, or take an separate implementation per AWS service, so: implementation A to support one specific AWS service and implementation B for another AWS service, etc.

Below I’ve split my analysis in those two forms. 



Full AWS stack replacements


Several options exist for full AWS stack replacements.

Floci

 

Floci is a free, open-source local AWS emulator which can be found at https://github.com/floci-io/floci
It is a very recently created project , but got quite some traction already.  
A week ago Floci changed into a separate organisation, see this LinkedIn post
It is not a ful drop-in replacement in TestContainers, but it comes close. Below shows what needs changing in a Kotlin Spring Boot application if you are currently using LocalStack.

Replacing LocalStack 

 

You probably have something like this currently configured with LocalStack:

@TestConfiguration

class TestConfig {

  @Bean

  fun localStackContainer(): LocalStackContainer {

    return LocalStackContainer(DockerImageName.parse("localstack/localstack:4:14:0"))

      .withServices(LocalStackContainer.Service.DYNAMODB)

  }


Basically changing it by replacing the docker image name and the environment variables should get it working already you’d think, so like this:

@TestConfiguration

class TestConfig {

@Bean

fun localStackContainer(): LocalStackContainer {

  return LocalStackContainer(DockerImageName.parse(hectorvent/floci:1.5.2).asCompatibleSubstituteFor("localstack/localstack"))

    .withEnv("AWS_ENDPOINT_URL", "http://localhost:4566")

    .withEnv("AWS_DEFAULT_REGION", "us-east-1")

    .withEnv("AWS_ACCESS_KEY_ID", "test")

    .withEnv("AWS_SECRET_ACCESS_KEY", "test")

}


But no it is not that easy. You’ll need to insert a different startup-script.
For example like this:

class FlociContainer(dockerImageName: DockerImageName) :   LocalStackContainer(dockerImageName) {


override fun containerIsStarting(containerInfo: InspectContainerResponse) {

  var shell: String

  var executable: String

  if (dockerImageName.contains("jvm")) {

    shell = "/bin/sh"

    executable = "java -jar quarkus-app/quarkus-run.jar"

  } else {

    shell = "/bin/bash"

    executable = "./application"

  }

  var command = shell + "\n" + executable + "\n"

  try {

    copyFileToContainer(Transferable.of(command, 511), STARTER_SCRIPT)

  } catch (e: Throwable) {

    logger.error("Failed to copy startup script to container: ${e.message}", e)

    throw e

  }

}

}

 

Note the logic to determine the correct command for the startup-script to use in the Docker image. Floci has a native version which has a different startup-script than the JVM version.
You’ll also need to parse for a different startup string than LocalStack has.
TestContainers has a module for LocalStack that looks for this string: ".*Ready\\.\n"
But Floci does not log that string, so one to search for is: ".*started in.*"
So you need to override that for the Floci version. 


Then the test configuration becomes this:

@TestConfiguration

class TestConfig {

@Bean

fun localStackContainer(): LocalStackContainer {

  val flociImage: DockerImageName =

  DockerImageName.parse(hectorvent/floci:1.5.2).asCompatibleSubstituteFor("localstack/localstack")

 

  return FlociContainer(flociImage)

          // Floci settings

    .withEnv("AWS_ENDPOINT_URL", "http://localhost:4566")

    .withEnv("AWS_DEFAULT_REGION", "us-east-1")

    .withEnv("AWS_ACCESS_KEY_ID", "test")

    .withEnv("AWS_SECRET_ACCESS_KEY", "test")

          // Disable services you don’t need

    .withEnv("FLOCI_SERVICES_SSM_ENABLED", "false")

    .withEnv("FLOCI_SERVICES_ELASTICSEARCH_ENABLED", "false")

           // Floci container has different log-text to look for

    .waitingFor(LogMessageWaitStrategy().withRegEx(".*started in.*").withTimes(1))

}

 

That should do it!


Building Docker JVM image of Floci and start it locally 

 

For building a Docker image for the JVM version, you can run these commands in the root directory of the Floci project: 

  1. Create the artifact (application) for the Docker image: ./mvnw clean package
  2. Build the docker image: docker build . -t floci-jvm-package:1.5.1-fix-001 -f Dockerfile.jvm-package 


To start the JVM image of Floci locally you can run this command in the root directory:

 

docker-compose -f docker-compose-fix-001.xml up

 

With this in that docker compose file:

 

services:

    floci:

        image:    floci-jvm-package:1.5.1-fix-001

        ports:

            - "4566:4566"

        volumes:

            - ./data:/app/data

            - ./init/start.d:/etc/floci/init/start.d:ro

            - ./init/stop.d:/etc/floci/init/stop.d:ro


The output will look something like this then:

Container common-adapters-floci-1    Recreated                                                                                                                                                                                                                                                                                                                          0.1s

Attaching to floci-1

floci-1    |      ______ _            ____      _____ _____

floci-1    |    |    ____| |        / __ \ / ____|_      _|

floci-1    |    | |__    | |      | |    | | |            | |   

floci-1    |    |    __| | |      | |    | | |            | |   

floci-1    |    | |        | |___| |__| | |____ _| |_

floci-1    |    |_|        |______\____/ \_____|_____|

floci-1    |

floci-1    |

floci-1    |                      Powered by Quarkus 3.32.3

floci-1    | 2026-04-11 15:51:14,012 INFO    [io.github.hectorvent.floci.lifecycle.EmulatorLifecycle] (main) === AWS Local Emulator Starting ===

floci-1    | 2026-04-11 15:51:14,012 INFO    [io.github.hectorvent.floci.lifecycle.EmulatorLifecycle] (main) Storage mode: memory

floci-1    | 2026-04-11 15:51:14,012 INFO    [io.github.hectorvent.floci.lifecycle.EmulatorLifecycle] (main) Persistent path: ./data

floci-1    | 2026-04-11 15:51:14,012 INFO    [io.github.hectorvent.floci.core.common.ServiceRegistry] (main) Enabled services: [ssm, sqs, s3, dynamodb, sns, lambda, apigateway, iam, elasticache, rds, events, scheduler, logs, monitoring, secretsmanager, apigatewayv2, kinesis, kms, cognito-idp, states, cloudformation, acm, email, es]

floci-1    | 2026-04-11 15:51:14,013 INFO    [io.github.hectorvent.floci.lifecycle.EmulatorLifecycle] (main) === AWS Local Emulator Ready ===

floci-1    | 2026-04-11 15:51:14,021 INFO    [io.quarkus] (main) floci 1.4.0 native (powered by Quarkus 3.32.3) started in 0.085s. Listening on: http://0.0.0.0:4566

floci-1    | 2026-04-11 15:51:14,021 INFO    [io.quarkus] (main) Profile prod activated.

floci-1    | 2026-04-11 15:51:14,021 INFO    [io.quarkus] (main) Installed features: [cdi, config-yaml, rest, rest-jackson, smallrye-context-propagation, vertx]


Notice also the ‘started in 0.085s’, which contains the string for which the above example FlociContainer is looking for to see if the container started successfully.


Automatically create your application queues in Floci


A fancy enhancement for when you are using Spring Boot: you can write a BeanFactoryPostProcessor which parses for @SqsListener annotations to automatically create the queues in the Floci container.

Run Integration Tests against locally running Floci


To run your tests against a locally running Floci instance directly, for example to see its logging, started up with Docker like mentioned above, you override the endpoint when creating the DynamoDBClient:

    return DynamoDbClient.builder()

          .endpointOverride(URI.create("http://localhost:4566"))

    .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(localStackContainer.accessKey, localStackContainer.secretKey)))

    .region(Region.of(localStackContainer.region))

    .build()

}


Testcontainers module for Floci

 

There’s now also a testcontainers module available for floci so you should not need all above manual changes: https://testcontainers.com/modules/floci/

I did not test this module yet.

 



Other full replacement options

  • Rust focused it seems: Rustack
  • Only 1 maintainer and runs standalone, some AWS, some GCP, some Azure services supported: CloudTwin
  • Python focused it seems: Moto: This post talks about Moto mocking and using it in unittests. Is it a mocking or emulating framework?
  • Any of the LocalStack forks.


AWS stack per-service replacement


Multiple options exist, this list might not be listing all options. 

  1. DynamoDB: 
    1. use the AWS DynamoDB Local replacement. Docker image is here. Can be used on its own, and there’s a TestContainers module for it too here.
    2. Dynalite: last update in 2020, seems to be dead. The TestContainers module seems to be gone now too
  2. S3: Minio Not supported anymore, see here.
  3. SQS: https://lib.rs/crates/tc_elasticmq. Docker image here.






Wednesday, April 6, 2022

Configuring MySQL test-containers in your Spring Boot Java Integration Tests

Introduction

In your Integration Tests (IT) you often try to use the H2 in-memory database, to improve the speed of your integration tests. But on the other hand you want to mimic the production database as much as possible in your integration-tests.

Setting H2 in MySQL database compatibility mode tries to emulate MySQL as much as possible, but only a small subset of the differences are implemented. What for example not works correctly in H2 for JSON fields is that it escapes strings with "". For that reason you usually want to switch to for example starting a Docker database testcontainer in your IT tests, which uses a real MySQL database. With the Java-specific version in https://github.com/testcontainers/testcontainers-java.


Configuration

There are several good-to-know tips when configuring the testcontainers in your ITs.

  1. The simplest configuration is using a datasource URL in the Spring Boot properties file. This has the disadvantage that whatever database name you specify, the testcontainers library still creates a DB named 'test'. So below it will be named 'integration_test_db' you'd think, but it is still named 'test' when the IT runs:

    spring.datasource.url=jdbc:tc:mysql:5.7.32:///integration_test_db?sessionVariables=sql_mode='STRICT_TRANS_TABLES'&TC_MY_CNF=mysql&TC_INITSCRIPT=mysql/init_mysql_integration_tests.sql

    To be able to do everything on the started database, including giving it the name you want, use this URL (or see below the Java version). Notice the user 'root' in the URL:
    spring.datasource.url=jdbc:tc:mysql:5.7.32:///integration_test_db?user=root&password=&sessionVariables=sql_mode='STRICT_TRANS_TABLES'&TC_MY_CNF=mysql&TC_INITSCRIPT=mysql/init_mysql_integration_tests.sql

    Via: https://github.com/testcontainers/testcontainers-java/issues/932

  2. To initialize your database, specify the script via this extra datasource URL variable:

    TC_INITSCRIPT=mysql/init_mysql_integration_tests.sql

    Note the default directory it looks in is ..../resources for the scripts. So the full path is ..../resources/mysql/

  3. An example to prevent GROUP BY error from strict mode, add this in your TC_INITSCRIPT:

    SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES';
    SET SESSION sql_mode = 'STRICT_TRANS_TABLES';


  4. To configure it in the IT Java class itself:

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = { SomeClassA.class, SomeClassB.class, ApplicationConfiguration.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    @TestPropertySource(locations = {
            "classpath:/application-test-mysql.properties" })
    @ContextConfiguration(initializers = {ThisITClass.Initializer.class})

    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
            public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
                TestPropertyValues.of(
                        "spring.datasource.url=" + mySQLContainer.getJdbcUrl(),
                        "spring.datasource.username=" + mySQLContainer.getUsername(),
                        "spring.datasource.password=" + mySQLContainer.getPassword()
                ).applyTo(configurableApplicationContext.getEnvironment());
            }
        }

    @ClassRule
    public static MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:5.7.31")
                .withUsername("root") // So now you can do a GRANT too for example
                .withPassword("") // Only possible for user 'root'
                .withEnv("MYSQL_ROOT_HOST", "%")
                .withDatabaseName("integration_test_db") // So this name will now be used, not 'test'
                .withInitScript("mysql/init_mysql_integration_tests.sql")

                ;

  5. The MySQL docker image used in the tests is retrieved from DockerHub https://hub.docker.com/_/mysql