Featured image of post Migrating From LocalStack to Floci

Migrating From LocalStack to Floci

Introduction

As of March 2026, LocalStack now requires an auth token to start the AWS environment. That change was enough to push this setup toward Floci, an open-source alternative that can emulate AWS and other cloud providers such as Azure and GCP.

In this post, we will walk through the migration process from the original Localstack setup to the new Floci architecture for deploying our static Hugo site.

Architecture

Architecture Diagram

The architecture is split into two containerized services, Floci and Hugo.

Floci acts as the local AWS-compatible backend. It exposes the S3-compatible endpoint, hosts the bucket, and receives all storage operations that the site needs during deployment.

The Hugo container is the build and deployment worker. It builds and renders the static site, runs the AWS CLI commands, and pushes the generated files into Floci’s S3 bucket.

Docker Compose ties the two services together and orchestrates their operation.

Docker Compose

The compose file below shows the overall layout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
services:
  floci:
    image: floci/floci:latest
    ports:
      - "4566:4566"
    environment:
      - FLOCI_HOSTNAME=floci  # URLs will use http://floci:4566/...
  habibiops:
    build: .
    environment:
      - AWS_ENDPOINT_URL=http://floci:4566
    depends_on:
      - floci

There are two important configurations for Floci:

  • Port 4566, which is exposed on the host for local access.
  • Hostname, so that other locally running containers can reach Floci. This is set through the environment variable FLOCI_HOSTNAME.

For our site, habibiops, we only need to define AWS_ENDPOINT_URL, which points to the Floci hostname and port.

AWS_ENDPOINT_URL only works between containers. If you need to access Floci from your host machine, use localhost:4566 or 0.0.0.0:4566 instead, without changing /etc/hosts.

Hugo Container

The Hugo image changed in two important ways:

  • localstack was removed from the base image and replaced with ubuntu
  • aws-cli was added so the build can still publish content to S3

The Dockerfile now looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
FROM ubuntu:26.04

ARG WORKING_DIR="/opt/floci"
ENV WORKING_DIR="${WORKING_DIR}"

RUN apt-get update && apt-get install -y --no-install-recommends \
    hugo \
    ca-certificates \
    git \
    golang \
    curl \
    unzip

RUN mkdir -p $WORKING_DIR

WORKDIR $WORKING_DIR

RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-$(arch).zip" -o "awscliv2.zip" \
    && unzip awscliv2.zip \
    && ./aws/install \
    && rm awscliv2.zip

COPY . .

RUN chmod +x ./setup.sh

ENTRYPOINT ["./setup.sh"]

One important note: the AWS CLI package is architecture dependent. The download URL changes depending on whether the host is running on x86_64 or aarch64. The arch command can be used to detect the current architecture before choosing the correct installer URL.

Hugo Setup Script

The setup script keeps the same deployment steps, but adds HUGO_ENV so the build can distinguish between local and production runs.

Hugo differentiates between different environments based on the folder name in the config directory. For example, if we want to define dev and prod, then we would have this structure:

1
2
3
4
config
├── _default
├── dev
└── prod

The _default applies to all environments. Any changes or differences between environments would be implemented and defined separately within their respective folders. For example, dev/config.toml is edited to include these variables:

1
2
baseurl = "http://localhost:4566/habibiops"
uglyURLs = true

While prod/config.toml contains:

1
baseurl = "https://habibiops.com"

Below is the minimal setup.sh script, including the environment variable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# variable definition and env vars exports
...
export HUGO_ENV=dev

cd "$SITE_DIR" && hugo --gc --minify

aws s3 mb "s3://$BUCKET"
aws s3api put-bucket-policy --bucket "$BUCKET" --policy "file://$POLICY_FILE"

cd "$PUBLIC_DIR"
aws s3 sync . "s3://$BUCKET" --delete

aws s3 website "s3://$BUCKET/" --index-document index.html --error-document 404.html

Deploying Changes

When the site changes, the habibiops container needs to be rebuilt. Docker Compose will otherwise reuse the cached image, so the update needs to be explicit:

1
podman compose up -d --build habibiops

In this setup, depends_on controls the flow of execution. Compose starts floci first, then brings up habibiops. After floci is running, habibiops starts building, creates the S3 bucket, builds the Hugo site, and deploys it to S3 via the Floci endpoint. Once that finishes, habibiops container exits (with status 0) while floci remains running and serves the deployed static site.

Finally, we can access the site locally through the same endpoint used by the containerized S3 workflow:

1
http://localhost:4566/habibiops

Conclusion

The migration adds some complexity, mainly because Docker Compose is now part of the deployment flow and the setup script had to be adjusted for the new endpoint model. The tradeoff is cleaner separation: Floci runs as an independent service, and any AWS-backed workload can sit beside it as its own container while still using the same AWS CLI workflow.