Go serverless, eject to containers

Posted on Feb 20, 2026

When it comes to building API’s in Go many external things influence it, do we want it now? What is the scale? Is the usage predictable? But mainly one thing is certain it’s always K8s (well the majority), I recently started building Go APIs with serverless by default until I have a reason to not to and no I am not vendor locked and yes I can utilise native features and devex is not bad! How you ask? Let’s go through it…

Why serverless?

I am not going to talk about the pros and cons of using serverless or why you should, this article talks about a side of Go development that I rarely see and wanted to share my experience around it and how it’s not as bad as you might think.

I mainly have experience in AWS which this article will show code in.

Let’s have a Look!

So we are building an API in Go, in fact nothing changes, we are still going to build it using the standard library, the only difference here will be the entrypoint.

So how does the entrypoint look like?

func run() error {

    /**
        Set up code
    **/
	if *local {
		http.ListenAndServe(":8080", mux)
	} else {
		algnhsa.ListenAndServe(mux, &algnhsa.Options{
			RequestType: algnhsa.RequestTypeAPIGatewayV2,
		})
	}
	return nil
}

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

local is a boolean flag passed during the build stage and defaults to false. eg. ./app -local=true

I used to use the aws http adapter library but that got deprecated recently so instead I would reccomend algnhsa.

As you see in the snippet you can pass in a RequestType option. The following RequestTypes are supported:

  • RequestTypeAPIGatewayV1
  • RequestTypeAPIGatewayV2
  • RequestTypeALB

You can also use it with an ALB too!

Now that trick is revealed, let me explain why I prefer this approach to an entirely serverless one.

Why a HTTP adapter approach

It gives flexibility to start serverless today while having the window open to move away from it with minimal changes. Developer Experience is much better as you can run the API locally giving you faster feedback loops than awaiting deployment in ephemeral environments.

You can also use native features like making calls during cold starts:

func run() error {
	if *local {
		http.ListenAndServe(":8080", mux)
	} else {
        /**
            Cold start code make call to secret manager etc
        **/
		algnhsa.ListenAndServe(mux, &algnhsa.Options{
			RequestType: algnhsa.RequestTypeAPIGatewayV2,
		})
	}
	return nil
}

If you are also using a database or perhaps some storage or service instead of mocking you can also use testcontainers to blur the lines between unit and integration testing. On a serious note having the serverless API running locally but also spinning up DB containers to run the tests and then tearing them down offers the best of both worlds in terms of ephemeral environments one local and one on the cloud.

Here is an example of a Postgres container:

ctx := context.Background()

dbName := "users"
dbUser := "user"
dbPassword := "password"

postgresContainer, err := postgres.Run(ctx,
    "postgres:16-alpine",
    postgres.WithInitScripts(filepath.Join("testdata", "init-user-db.sh")),
    postgres.WithConfigFile(filepath.Join("testdata", "my-postgres.conf")),
    postgres.WithDatabase(dbName),
    postgres.WithUsername(dbUser),
    postgres.WithPassword(dbPassword),
    postgres.BasicWaitStrategies(),
)
defer func() {
    if err := testcontainers.TerminateContainer(postgresContainer); err != nil {
        log.Printf("failed to terminate container: %s", err)
    }
}()
if err != nil {
    log.Printf("failed to start container: %s", err)
    return
}

I found it relatively easy to set up (as long as you have docker installed) and gave me more confidence in my tests as oppossed to when I was mocking the database calls. You can read the module docs for more configuration options on the DB container.

Workflow

The development workflow will primarily be local so,

Locally:

  • Make code changes
  • Run the API locally and test it (testcontainers etc)
  • Push to repo once happy with the changes

On the cloud:

  • CI/CD will build and deploy the API to the cloud
  • Run tests on the cloud to validate that it works there too via CI/CD pipelines
  • Get a LGTM from a colleague and merge!

Conclusion

A reason why I am writing about this workflow is because I found the devex for it to better than what I was used to before developing with serverless apps. I no longer have to wait for long CI/CD to apply my changes and be on an AWS console to troubleshoot, instead I can do that all locally and instead use the cloud to validate that it works there too. I also don’t have to write specific AWS code for the Lambda as the http adapter takes care of that. Of course when your app goes viral and suddenly serverless is too costly the rewrite to container isn’t too cumbersome!

Let me know if you found any of this helpful or have better ways on bluesky.