Posts by javi(3)

Page 1 of 1
Cool Pic

Deploying your Astro blog

Keep your Astro source private and publish only the static build

GitHub Pages for private respositories is not supported on GitHub Free (read here) and I didn’t like the idea of making the whole blog’s source code and post drafts publicly available. So I had to come up with a solution.

NOTE

I’m aware that Cloudfare Pages it’s free, faster than GitHub Pages, and supports private repos. But I also had the comments section tied to the public repo so. Maybe next time…

The idea is simply using a two-repository strategy:

  1. Private repo - Where we keep our source code with drafts, work-in-progress posts, and all our Astro source files.
  2. Public repo - Just the contents of the dist/ folder, for GitHub Pages deployment.

Configuring the Deployment Target

By deployment target, I mean the public repo from where we will will serve the site to the world, using GitHub Pages.

NOTE

Remember, our private repo is where we write posts, keep drafts, and build the Astro project. The public repo is just where we push the final static output.

In our public repo, we have to configure a publishing source for GitHub Pages. There are a couple of options here:

  • To publish when changes are pushed to a specific branch.
  • Or you can write a GitHub Actions workflow to publish your site.

We’ll be using the first option; just click on the Settings tab of your repo, and once there, find the Pages slot in the left sidebar. Mine looked like this:

the branch method

What this means is that whenever we push changes to the master branch (root folder /), our GitHub pages will be published.

For Astro Users

Before building our site, we need to tell Astro the final public URL where our site will be published. This is controlled by two settings in astro.config.ts:

  • site is the domain where we will serve the site.
  • base is the path under that domain where your site will live.

Since we will be serving the page from the URL of the public repo, we need to add it to our astro.config.ts:

export default defineConfig({
site: 'https://<username>.github.io',
base: '/',
// more stuff...
})

If your public repo is the special GitHub Pages repo named <username>.github.io, then the site is served from the root: https://<username>.github.io/.

TIP

In my case, since it was my personal page, I used the https://<username>.github.io URL. But it doesn’t have to be, any public repo URL will do, e.g. https://github.com/<username>/<repo-name>.

But if you’re publishing to a repo named foo, then you should use something like:

  • site: 'https://github.com'
  • base: 'foo'

TLDR

Just three steps:

  1. Build your project; from the private repo run:
Terminal window
npm run build
  1. Copy the contents of the dist folder, from the private repo to the public repo:
Terminal window
rsync -a --delete --exclude '.git' dist/ ../my-blog-public/
  1. Move to the public repo, and push changes:
Terminal window
cd ../my-blog-public
git add -A
git commit -m 'new build'
git push

1. Creating the Build

Not much to say here, whenever you add a new post, and are ready to publish, you need to build right? Let’s assume you have the following build script in the package.json of your private repo:

"build": "astro build"

We just have to change to the root of your private repo, and run:

Terminal window
npm run build

In the case of an Astro project, we should end up with a dist folder containing the artifacts of our build.

2. Copy the Build to the Public Repo

Now the goal is to move the contents of the dist folder (not the folder itself, but its contents), to the root of our public repo. :

Terminal window
rsync -a --delete --exclude '.git' dist/ ../my-blog-public/

Let’s go over the flags:

  • dist/ with trailing slash copies the contents of dist, not the folder itself.
  • --delete removes stale files from the public repo.
  • --exclude '.git' preserves the public repo Git metadata.
  • It copies hidden files like .nojekyll.

TIP

In Astro, dist is the default build folder; but if you’re using another one, or not even using Astro, feel free to use whatever folder you need to.

The command above assumes that you have cloned your public repo to the parent folder of the private repo. Check the following folder structure:

Terminal window
parent-folder/
├── my-private-repo/ # 👈 We're working here.
├── src/
├── content/
└── dist/
└── my-blog-public/ # 👈 Make sure you have cloned your public repo.
└── .git/

3. Pushing Changes to the Public Repo

Push the changes to your GitHub public remote:

Terminal window
cd ../my-blog-public
git add -A # Stage the changes
git commit -m "New build" # Commit
git push # Push

That should be enough you have your page live after a few seconds.

GitHub Actions

Ok, now that we’re familiar with the steps to publish our site manually, let’s see how we can do the same with a GitHub Actions workflow.

NOTE

Remember, the idea is quite simple: keep the repo with the source code and posts private, and push the build to a public one.

In the private repo, if I go to Settings > Pages, I’ll see a warning:

no GH Pages

That’s the root of the problem: with a free account, I can’t have GitHub Pages in a private repo.

Creating a PAT

Before start writing the action, we need to create a GitHub Personal Access Token tied to the user that has push access to the public repo (myself).

In order to do that we have to click on our user logo (upper right corner), and once the sidebar opens up, click on Settings → Developer Settings → Personal access tokens. I selected Tokens (classic), then Generate new token, and Generate new token (classic).

NOTE

Make sure you do not over-scope the token. For your use, just public_repo is enough to gives the token access to public repositories, which is enough for your workflow to push the built dist/ files to.

Give it a descriptive description (under Note); I named mine Deploy code-blue to lifeBalance.github.io. Once the token has been generated, you are redirected to a new page; you should copy the token name GH_PAGES_TOKEN.

WARNING

Make sure to copy your token now as you will not be able to see it again.

If for some reason you didn’t copy the token, just regenerate it again, no big deal.

Using the PAT

Now we need to add the PAT as a secret (e.g., GH_PAGES_TOKEN) in the private repo:

  1. In our private repo, we go to Settings → Secrets and variables → Actions
  2. Click New repository secret.
  3. Name it GH_PAGES_TOKEN (or whatever you name it in the *.yml file).
  4. Paste your copied PAT.

That’s it — now your GitHub Actions workflow will automatically have access to it through ${{ secrets.GH_PAGES_TOKEN }} — no manual shell exports needed ever again.

The Workflow

Create a .github/workflows/deploy.yml in the private repo:

name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch: # Manual trigger for actions
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Add .nojekyll file
run: echo > dist/.nojekyll # Ensures GitHub Pages ignores Jekyll processing
- name: Deploy to GitHub Pages
env:
GH_PAGES_TOKEN: ${{ secrets.GH_PAGES_TOKEN }}
run: |
TEMP_DIR=$(mktemp -d)
cp -r dist/. "$TEMP_DIR"
cd "$TEMP_DIR"
git init
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git remote add origin https://$GH_PAGES_TOKEN@github.com/lifeBalance/lifeBalance.github.io.git
git checkout -b master # Use 'master' instead of 'main' for GitHub Pages default branch
git add .
git commit -m "Automated deploy"
git push -f origin master # Push to master branch

Switching Workflows

If your GitHub Pro subscription expires and you can no longer publish GitHub Pages from a private repo, you do not need to delete your old GitHub Pages workflow. In my case, I kept .github/workflows/astro.yml around and disabled it.

  1. Go to the Actions tab.
  2. Find the Deploy Astro site to Pages workflow. That is the workflow name from astro.yml.
  3. Open any previous run of that workflow.
  4. Click the ••• menu in the top-right corner.
  5. Click Disable workflow.

Disable workflow

IMPORTANT

This only disables astro.yml. It does not affect deploy.yml. The file stays in the repo, but GitHub will not run it unless you enable it again.

If you later re-enable GitHub Pro and want to deploy directly from the private repo again:

  1. Disable deploy.yml, the workflow that pushes the build to the public repo.
  2. Re-enable astro.yml.
  3. In the private repo, configure GitHub Pages to use GitHub Actions as the source.
Cool pic

Garage

An S3 object store so reliable you can run it outside datacenters

Most web apps eventually need to handle file uploads — profile pictures, attachments, assets. The instinct is to just write them to disk, but that breaks the moment your app restarts or scales.

Object Storage

Object storage (also called blob storage) is a way of storing data where each piece of data is kept as an independent object rather than inside a traditional file hierarchy or fixed disk blocks. Each object contains the data itself, some metadata describing it, and a unique identifier used to retrieve it.

My Use Case

Sometimes apps run on infrastructure where disk persistence isn’t reliable. That’s the case with Next.js apps, where local filesystem storage is ephemeral — files can disappear after restarts, redeploys, or when the app moves between instances. I needed an object storage solution that matches the production environment.

Why Garage

Garage is a lightweight, self-hosted S3-compatible store that gives you that — without handing your data to a third party. The project is not even hosted in GitHub but in deuxfleurs, which is the collective/organization behind Garage.

NOTE

deuxfleurs (aka 🌼🌼) is a French non-profit that builds decentralized, self-hosted infrastructure tools. Garage was originally built to power their own infrastructure and then open-sourced.

Using something like Garage (or MinIO) makes sense if:

  • You want full control over where data lives — no third party
  • You’re building for a client with data residency requirements
  • You’re deploying the whole stack on your own infra (VPS, bare metal) and want storage there too, not on AWS
  • You want prod to also be self-hosted — in which case Garage in dev mirrors prod exactly

Set Up a Garage Container

You can get familiar with several ways of getting Garage up and running in their quick start. We’ll be using a Docker container, so let’s add the following to our docker-compose.yml file:

services:
# Other services
garage:
image: dxflrs/garage:b72b090a097c8ee2711c8fb065d250ed68dcd0bf
container_name: garage_s3
ports:
- '3900:3900'
- '3901:3901'
environment:
- GARAGE_ALLOW_WORLD_READABLE=1
volumes:
- ./garage_config/garage.toml:/etc/garage.toml
- ./garage_metadata:/var/lib/garage/meta
- ./garage_data:/var/lib/garage/data

Garage Configuration

According to the aforementioned quick start, we need to create a configuration file:

# metadata_dir = "/tmp/meta"
# data_dir = "/tmp/data"
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
# rpc_secret = "$(openssl rand -hex 32)"
rpc_secret = "b89a46551da109ca0ecac670ae2dff0482a1ab98aa3ec28a96f1c65a656dee34"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.garage.localhost"
[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.garage.localhost"
index = "index.html"
[k2v_api]
api_bind_addr = "[::]:3904"
[admin]
api_bind_addr = "[::]:3903"
# admin_token = "$(openssl rand -base64 32)"
admin_token = "zNZmDBE9Tiqs8CaUmN90Loo/63T+c32x0xC8M0Rjk8U="
# metrics_token = "$(openssl rand -base64 32)"
metrics_token = "69VTAfJSVuaog72X8oxcReRFAQKJJelF4BJzQgeXjvk="

IMPORTANT

This file is mounted as a volume in our Docker setup; the default location where garage looks up this file within the container is /etc/garage.toml.

To get the values for some configuration options, we have to run in the terminal the following commands:

  • openssl rand -hex 32 and paste the output in rpc_secret
  • openssl rand -base64 32 and paste the output in admin_token
  • openssl rand -base64 32 and paste the output in metrics_token

Creating a Cluster Layout

Creating a cluster layout for a Garage deployment means:

  1. Informing Garage of the disk space available on each node of the cluster (-c)
  2. As well as the name of the zone (e.g. datacenter), each machine is located in (-z).

Copy Node ID Prefix

Let’s start Garage with docker compose up garage, and check the status with:

Terminal window
docker exec -it garage_s3 /garage status
INFO garage_net::netapp: Connected to 127.0.0.1:3901, negotiating handshake...
INFO garage_net::netapp: Connection established to 2778c9ed427e0ca2
==== HEALTHY NODES ====
ID Hostname Address Tags Zone Capacity DataAvail
2778c9ed427e0ca2 0d45c890ad78 127.0.0.1:3901 NO ROLE ASSIGNED

Then let’s copy the prefix of the node, e.g. 2778 from 2778c9ed427e0ca2, and paste it in <NODE_ID_PREFIX>:

Terminal window
docker exec -it garage_s3 /garage layout assign -z dc1 -c 1G <NODE_ID_PREFIX>

Then apply the layout:

Terminal window
docker exec -it garage_s3 /garage layout apply --version 1

Which should output:

==== COMPUTATION OF A NEW PARTITION ASSIGNATION ====
Partitions are replicated 1 times on at least 1 distinct zones.
Optimal partition size: 3.9 MB
Usable capacity / total cluster capacity: 1000.0 MB / 1000.0 MB (100.0 %)
Effective capacity (replication factor 1): 1000.0 MB
dc1 Tags Partitions Capacity Usable capacity
2778c9ed427e0ca2 256 (256 new) 1000.0 MB 1000.0 MB (100.0%)
TOTAL 256 (256 unique) 1000.0 MB 1000.0 MB (100.0%)
New cluster layout with updated role assignment has been applied in cluster.
Data will now be moved around between nodes accordingly.

Create Bucket

Create bucket, which is the place where our image files will live:

Terminal window
docker exec -it garage_s3 /garage bucket create app-uploads

NOTE

You should get the output: Bucket app-uploads was created.

Add the bucket name to the .env file:

S3_BUCKET=app-uploads

Create Key

Let’s create the key that we’ll use to access the bucket we just created:

Terminal window
docker exec -it garage_s3 /garage key create next-demo-local

Which should output the keys:

Key name: next-demo-local
Key ID: GKbccef323a6f4dedd82f85d59
Secret key: 5ed99b90d1f7f0bd766755cfc3a9daad89ed747fb058dd66c502e544a3ed8f3c
Can create buckets: false
Key-specific bucket aliases:
Authorized buckets:

And paste these values in our .env file:

S3_ACCESS_KEY=GKbccef323a6f4dedd82f85d59
S3_SECRET_KEY=5ed99b90d1f7f0bd766755cfc3a9daad89ed747fb058dd66c502e544a3ed8f3c

Associate Key and Bucket

We need to grant the key access to the bucket:

Terminal window
docker exec -it garage_s3 /garage bucket allow --read --write --owner app-uploads --key next-demo-local

The command above should output something like:

New permissions for GKbccef323a6f4dedd82f85d59 on app-uploads: read true, write true, owner true.

It’s a good idea to verify with this:

Terminal window
docker exec -it garage_s3 /garage key info next-demo-local

Which should output:

Key name: next-demo-local
Key ID: GKbccef323a6f4dedd82f85d59
Secret key: (redacted)
Can create buckets: false
Key-specific bucket aliases:
Authorized buckets:
RWO app-uploads dfc275695b62a17e

NOTE

Note that Can create buckets is false, which means that this key cannot create new buckets, but it can still fully use the buckets explicitly granted to it, in this case app-uploads

Serving Files

Garage in this setup does not allow anonymous object reads, so a raw object URL like http://localhost:3900/app-uploads/events/... cannot be rendered directly in the browser.

NOTE

Whas is an Anonymous Object Read

Imagine you paste http://localhost:3900/app-uploads/events/a.png in browser:

  • Browser sends no S3 auth key/signature.
  • If storage still returns file, that’s an anonymous read. In our setup, Garage rejects that (AccessDenied), so objects are private by default. That’s why our app has to fetch files with credentials server-side (or use signed URLs).

To solve this, we expose a Next.js route handler at app/api/files/[...key]/route.ts. How it works:

  1. The events API returns Mongo data and sets image to a local app URL (for example: /api/files/events/my-image.png).
  2. The browser requests that local URL.
  3. The file route reconstructs the object key from the path (events/my-image.png).
  4. The route fetches the object from Garage using server-side credentials (s3Client + GetObjectCommand).
  5. The route streams bytes back with Content-Type, so the browser can display the image normally.

Why we need it:

  • Keeps Garage bucket private (no anonymous public reads).
  • Browser never needs storage credentials.
  • Works the same way in dev and prod: app API mediates access to object storage.
Cool Pic

Microsandbox

Caging the Autocomplete Beast

I wasn’t very happy with my current sandboxing solution (E2B), so decided to give a try to MicroSandbox, and was glad I switched.

What are sandboxes for?

We’re entering an era where AI agents don’t just suggest code, they write it and execute it. They also install packages, run scripts, inspect files, call CLIs, start dev servers, and sometimes touch code we barely understand yet.

That changes the infrastructure we need around development.

If an agent is going to run arbitrary commands, it needs a safe place to do it. Not my laptop. Not my production machine. Not a long-lived environment full of secrets. It needs a disposable, isolated computer where mistakes are contained and experiments are cheap.

That’s what sandboxes are for.

A good sandbox gives each agent its own environment: a filesystem, a shell, network boundaries, resource limits, and a way to run and inspect code without trusting it too much. For AI coding tools, this is becoming table stakes.

The Microsandbox CLI

Before using the Microsandbox SDK, it’s worth getting familiar with what Microsandbox can do through the Microsandbox CLI.

In this post we’ll look into how to:

  • Install the msb CLI.
  • Create a persistent Node-based sandbox.
  • Execute shell commands inside the sandbox.
  • Install runtime tools and project dependencies.
  • Create and run an Astro app inside the sandbox.
  • Expose the Astro dev server through a mapped local port.
  • Inspect, stop, restart, and remove the sandbox.

Installation

We can install the CLI globally, by downloading and running the following script:

Terminal window
curl -fsSL https://install.microsandbox.dev | sh

WARNING

microsandbox requires Linux with KVM enabled, or macOS with Apple Silicon (M-series chip). Both use hardware virtualization.

After the installation, you’ll have to source ~/.zshrc, then verify the installation:

Terminal window
msb --version
msb 0.4.6

Using the msb CLI

In this section we’ll demo how to create a sandbox named astro-demo, which is nothing but a cointainerized Astro app, running in a Microsandbox.

1. Create A Persistent Node Sandbox

Let’s use the msb CLI to create a named sandbox from the node:22.22.3-slim Docker image and publish Astro’s default dev port:

Terminal window
msb run \
--name astro-demo \
--detach \
--replace \
-m 2G \
-c 2 \
-p 4324:4321 \
node:22.22.3-slim

Several things going on in the command above:

  • The --name flag will name our persistent sandbox as astro-demo.
  • The --detach flag to run the sandbox in the background and returns the terminal inmediately.
  • The --replace flag will recreate the sandbox if it already exists.
  • The -m 2G flag sets 2GB of memory.
  • The -c 2 flag sets 2 virtual CPUs.
  • The -p 4324:4321 is mapping ports from host 4324 -> sandbox 4321.
  • We’re using the node:22.22.3-slim Docker image, which gives us Node.js preinstalled, based on Debian slim. The slim variant is smaller than the full Node image, but still supports apt-get, so we can install tools like git and curl.

2. Prepare The Runtime

Now that our sandbox is running, we can install tools into it:

Terminal window
msb exec astro-demo -- sh -c "apt-get update && apt-get install -y git curl"

Enable pnpm as our package manager:

Terminal window
msb exec astro-demo -- corepack enable
msb exec astro-demo -- corepack prepare pnpm@latest --activate

Verify that everything was successfully installed by checking their versions:

Terminal window
msb exec astro-demo -- node --version
msb exec astro-demo -- pnpm --version
msb exec astro-demo -- git --version

3. Create An Astro App

Let’s create the app inside /home/workspace:

Terminal window
msb exec astro-demo -- sh -c "mkdir -p /home/workspace && cd /home/workspace && pnpm create astro@latest . --template basics --no-install --yes"

The command above will create without installing dependencies. Let’s install them:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && pnpm install --dangerously-allow-all-builds"

TIP

The --dangerously-allow-all-builds flag is for pnpm install. It allows packages with build/postinstall scripts to run during installation.

We use it because generated frontend apps often depend on packages that need install-time build steps. Without it, pnpm may skip those scripts and later commands can fail with ERR_PNPM_IGNORED_BUILDS.

Prefer running it in a separate install step:

Terminal window
pnpm install --dangerously-allow-all-builds

instead of passing it through pnpm create, where it may not be forwarded reliably.

If pnpm create astro prompts unexpectedly, use npm instead:

Terminal window
msb exec astro-demo -- sh -c "mkdir -p /home/workspace && cd /home/workspace && npm create astro@latest . -- --template basics --install --yes"

4. Start The Astro Dev Server

Ok, let’s start the Astro dev server on all interfaces so the published port can reach it:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && pnpm dev --host 0.0.0.0 --port 4321"

The command above also starts the sandbox if it was stopped. Now the sandboxed app should be available at https://localhost:4324 👍

TIP

You can start/stop the sandbox at anytime with:

Terminal window
msb stop astro-demo # Stop
msb start astro-demo # Start

Then inspect the logs:

Terminal window
msb logs astro-demo

NOTE

To start the Astro dev server in the background, we need to background the host-side msb exec process:

Terminal window
nohup msb exec astro-demo -- sh -c "cd /home/workspace && pnpm dev --host 0.0.0.0 --port 4321" </dev/null >/dev/null 2>&1 &
disown

Oof, lot to unpack:

  • nohup keeps msb exec alive after shell hangups.
  • </dev/null detaches stdin.
  • >/dev/null 2>&1 detaches output
  • & backgrounds the host msb exec process (outside the "")
  • disown removes it from shell job control.

To stop the background process, find it with:

Terminal window
ps aux | grep "msb exec astro-demo"

Then kill the matching PID:

Terminal window
kill <PID>

It makes things quite difficult if you’re not used to this level of shell-fu, so nothing wrong if you prefer to run it in the foreground, and open new terminal tabs.

5. Run Commands Inside The Sandbox

Using msb we can inspect files within the sandbox:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && ls -la"

Read package metadata:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && cat package.json"

Run a build:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && pnpm build"

6. Test Environment Variables

Run a one-off command with an env var:

Terminal window
msb exec astro-demo -- sh -c "DEMO_ENV=hello node -e 'console.log(process.env.DEMO_ENV)'"

For app runtime env vars, start the dev server with env in the shell command:

Terminal window
msb exec astro-demo -- sh -c "cd /home/workspace && PUBLIC_DEMO_VALUE=hello pnpm dev --host 0.0.0.0 > /tmp/astro.log 2>&1 &"

7. Inspect Sandbox State

The msb CLI offers a bunch of commands for managing sandboxes. We can list them:

Terminal window
msb ls

Show the ones that are running:

Terminal window
msb ps

Inspect a particular sandbox:

Terminal window
msb inspect astro-demo

Show its metrics:

Terminal window
msb metrics astro-demo

Show its logs:

Terminal window
msb logs astro-demo

8. Stop And Resume

To start (or restart) an existing sandbox:

Terminal window
msb start astro-demo

To stop it:

Terminal window
msb stop astro-demo

9. Clean Up

Whenever we are done with a sandbox, we can stop it:

Terminal window
msb stop astro-demo

And remove it:

Terminal window
msb rm astro-demo

Huzzah!

We’ve played a bit with the CLI, now go have fun with the SDK.