Storage Azure

Garage on Ubuntu 24.04 on Azure User Guide

| Product: Garage on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of Garage on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Garage is a lightweight, self-hosted, S3 compatible distributed object storage server written in Rust by Deuxfleurs. It is designed to be simple to operate and efficient on modest hardware, and it exposes the Amazon S3 wire protocol so tools written for S3 (aws-cli, any aws-sdk, Restic, rclone, backup agents, static-site tooling, data pipelines) work against it unchanged.

The image ships Garage 2.3.0 (the latest stable release, installed from the official static binary and checksum pinned). A single garage process runs the S3 API, the admin API and the static web endpoint together with replication factor 1 — a complete single-node object store on one virtual machine. Everything is bound to loopback; an nginx TLS front publishes three endpoints:

  • S3 API on https://<vm-ip>:3900, authenticated with AWS SigV4 access and secret keys (region garage).

  • Web bucket endpoint on https://<vm-ip>:3902, for serving buckets configured for static website hosting.

  • Admin API on https://<vm-ip>:3903, protected by a Bearer token (GET /health is unauthenticated).

On every fresh customer virtual machine, garage-firstboot.service generates a per-VM RPC secret, admin token, metrics token, S3 access key and secret key, and a TLS certificate bound to the VM's IP. It then applies the single-node storage layout, creates a demo bucket with an owner key, and records everything to /root/garage-credentials.txt (mode 0600, root only). No two virtual machines ever share credentials, and there is no default login.

What is included:

  • Garage 2.3.0 static binary at /usr/local/bin/garage (S3 API + admin API + web endpoint in one server)

  • garage.service systemd unit, gated on a first-boot marker so it never starts with a default secret, all listeners bound to 127.0.0.1

  • nginx TLS front: :3900 S3 API (SigV4), :3902 web bucket serving, :3903 admin API (Bearer token), :80 plain-text info notice

  • garage-firstboot.service systemd oneshot that generates per-VM secrets, applies the storage layout, creates a demo bucket + key, and rotates the TLS cert

  • A dedicated 40 GiB data volume mounted at /var/lib/garage for object metadata and data

  • AWS CLI v2 preinstalled at /usr/local/bin/aws for command-line S3 operations

  • Ubuntu 24.04 LTS base with latest security patches applied at build time

  • Azure Linux Agent for seamless cloud integration and SSH key injection

  • 24/7 cloudimg support with guaranteed 24 hour response SLA

Prerequisites

  • An active Azure subscription

  • A subscription to the Garage on Ubuntu 24.04 listing on Azure Marketplace

  • An SSH public key for VM authentication

  • A virtual network and subnet in the target region

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for development and small workloads. Production deployments should use Standard_D2s_v3 or larger and enlarge or add data disks mounted at /var/lib/garage for capacity and throughput.

Step 1: Deploy from the Azure Portal

Navigate to Marketplace in the Azure Portal, search for Garage, select the cloudimg publisher entry, and click Create.

On the Networking tab attach a network security group that allows inbound TCP 22 from your management IP range, TCP 3900 (the S3 API) from your application networks, and — only if you need them remotely — TCP 3902 (web serving) and TCP 3903 (admin API) from your admin networks. The internal Garage ports are bound to loopback and are never exposed directly.

Click Review + create, wait for validation, then Create. Deployment takes around two minutes.

Step 2: Deploy from the Azure CLI

RG="garage-prod"
LOCATION="eastus"
VM_NAME="garage-01"
ADMIN_USER="azureuser"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/garage-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"

az group create --name "$RG" --location "$LOCATION"

az network vnet create \
  --resource-group "$RG" \
  --name garage-vnet --address-prefix 10.96.0.0/16 \
  --subnet-name garage-subnet --subnet-prefix 10.96.1.0/24

az network nsg create --resource-group "$RG" --name garage-nsg

az network nsg rule create \
  --resource-group "$RG" --nsg-name garage-nsg \
  --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" \
  --destination-port-ranges 22 --access Allow --protocol Tcp

az network nsg rule create \
  --resource-group "$RG" --nsg-name garage-nsg \
  --name allow-s3-api --priority 110 \
  --source-address-prefixes 10.96.0.0/16 \
  --destination-port-ranges 3900 --access Allow --protocol Tcp

az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$GALLERY_IMAGE_ID" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username "$ADMIN_USER" --ssh-key-values "$SSH_KEY" \
  --vnet-name garage-vnet --subnet garage-subnet --nsg garage-nsg \
  --public-ip-sku Standard

Step 3: Connect via SSH

ssh azureuser@<vm-ip>

garage.service and nginx.service will already be running, and garage-firstboot.service will already have generated the per-VM credentials, applied the storage layout, and created the demo bucket.

Step 4: Verify the Services

sudo systemctl is-active garage.service nginx.service

Expected: active twice. Confirm the firstboot sentinel:

sudo test -f /var/lib/cloudimg/garage-firstboot.done && echo FIRSTBOOT_DONE

Confirm the public TLS listeners and that the internal Garage ports are bound to loopback only:

sudo ss -tln | grep -E ':(3900|3901|3902|3903|3910|3912|3913) '

You will see 0.0.0.0:3900, 0.0.0.0:3902 and 0.0.0.0:3903 (public, TLS) while the S3 backend 3910, web 3912, admin 3913 and RPC 3901 are bound to 127.0.0.1.

garage.service and nginx.service active; public TLS on 3900, 3902 and 3903 while the Garage backend ports are bound to loopback

Step 5: Retrieve the Per-VM Credentials

sudo cat /root/garage-credentials.txt

You will see the per-VM S3 access key, S3 secret key, the admin and metrics tokens, and the endpoint URLs:

S3_ACCESS_KEY=<S3_ACCESS_KEY>
S3_SECRET_KEY=<S3_SECRET_KEY>
S3_REGION=garage
S3_ENDPOINT_URL=https://<vm-ip>:3900
WEB_ENDPOINT_URL=https://<vm-ip>:3902
ADMIN_ENDPOINT_URL=https://<vm-ip>:3903
ADMIN_TOKEN=<ADMIN_TOKEN>
METRICS_TOKEN=<METRICS_TOKEN>
DEMO_BUCKET=cloudimg

Store these values in your secret store. The file is mode 0600 (root only).

sudo /usr/local/bin/garage --version

Step 6: Inspect the Cluster Status and Layout

The garage CLI reads /etc/garage.toml and talks to the local node over RPC. Confirm the single node is healthy and the storage layout is applied:

sudo garage -c /etc/garage.toml status
sudo garage -c /etc/garage.toml layout show

You will see one healthy node in zone dc1 with its capacity, and a current cluster layout at version 1.

Garage single-node status showing one healthy node and the applied storage layout at version 1

Step 7: Round-trip Test from the CLI

The AWS CLI is preinstalled. This reads the per-VM S3 keys straight from the credentials file and performs a full create-bucket, put, get, verify, delete round-trip through the TLS S3 endpoint. Garage validates SigV4 with region garage, so the client must sign with that region:

export AWS_ACCESS_KEY_ID="$(sudo awk -F= '/^S3_ACCESS_KEY=/{print $2}' /root/garage-credentials.txt)"
export AWS_SECRET_ACCESS_KEY="$(sudo awk -F= '/^S3_SECRET_KEY=/{print $2}' /root/garage-credentials.txt)"
export AWS_DEFAULT_REGION="garage"
EP="--no-verify-ssl --endpoint-url https://127.0.0.1:3900"
aws $EP s3 mb s3://cloudimg-demo
echo "cloudimg-roundtrip-ok" > /tmp/probe.txt
aws $EP s3 cp /tmp/probe.txt s3://cloudimg-demo/probe.txt
aws $EP s3 cp s3://cloudimg-demo/probe.txt /tmp/probe-back.txt
cat /tmp/probe-back.txt
aws $EP s3 rm s3://cloudimg-demo/probe.txt
aws $EP s3 rb s3://cloudimg-demo

Expected output includes make_bucket, the upload and download lines, cloudimg-roundtrip-ok, then clean removals. The --no-verify-ssl flag is only needed for the self-signed certificate on a first local connection.

aws s3 make-bucket, put, get, verify and remove round-trip through the Garage TLS S3 endpoint

Step 8: List Buckets and Objects

export AWS_ACCESS_KEY_ID="$(sudo awk -F= '/^S3_ACCESS_KEY=/{print $2}' /root/garage-credentials.txt)"
export AWS_SECRET_ACCESS_KEY="$(sudo awk -F= '/^S3_SECRET_KEY=/{print $2}' /root/garage-credentials.txt)"
export AWS_DEFAULT_REGION="garage"
aws --no-verify-ssl --endpoint-url https://127.0.0.1:3900 s3 ls

The cloudimg demo bucket created at first boot is listed. To list the objects inside a bucket, add the bucket name:

export AWS_ACCESS_KEY_ID="$(sudo awk -F= '/^S3_ACCESS_KEY=/{print $2}' /root/garage-credentials.txt)"
export AWS_SECRET_ACCESS_KEY="$(sudo awk -F= '/^S3_SECRET_KEY=/{print $2}' /root/garage-credentials.txt)"
export AWS_DEFAULT_REGION="garage"
aws --no-verify-ssl --endpoint-url https://127.0.0.1:3900 s3 ls s3://cloudimg --recursive || true

Step 9: Query the Admin API

GET /health is unauthenticated and returns 200 when the node is ready. The richer endpoints require the per-VM admin token as an HTTP Bearer token:

curl -ks -o /dev/null -w 'health: HTTP %{http_code}\n' https://127.0.0.1:3903/health
ADMIN_TOKEN="$(sudo awk -F= '/^ADMIN_TOKEN=/{print $2}' /root/garage-credentials.txt)"
curl -ks -H "Authorization: Bearer ${ADMIN_TOKEN}" https://127.0.0.1:3903/v2/GetClusterHealth

The cluster health response reports "status":"healthy" with the partition counts for the single node.

Garage admin API health check returning HTTP 200 and GetClusterHealth reporting a healthy cluster

Step 10: Connect from Application Code

Any aws-sdk works. A representative Python boto3 example, pointing at the public S3 endpoint with the per-VM keys and region garage:

python3 -c "
import boto3, urllib3
urllib3.disable_warnings()
s3 = boto3.client(
    's3',
    endpoint_url='https://<vm-ip>:3900',
    aws_access_key_id='<S3_ACCESS_KEY>',
    aws_secret_access_key='<S3_SECRET_KEY>',
    region_name='garage',
    verify=False,
)
print(s3.list_buckets())
"

For production, install the per-VM certificate (/etc/garage/tls/garage.crt) into your client's trust store instead of disabling verification, or front Garage with a certificate from a public CA.

Step 11: Create Your Own Buckets and Keys

New buckets and access keys are managed with the garage CLI over SSH. Create a bucket, mint an application key, and grant it access:

sudo garage -c /etc/garage.toml bucket create my-app        # <your-token>
sudo garage -c /etc/garage.toml key create my-app-key       # <your-token>
sudo garage -c /etc/garage.toml bucket allow --read --write my-app --key my-app-key   # <your-token>

The key create command prints the new access key ID and secret once — store them in your secret store immediately.

Step 12: Server Components

Component Path

garage binary /usr/local/bin/garage

AWS CLI v2 /usr/local/bin/aws

Configuration file /etc/garage.toml

Data volume (metadata + data) /var/lib/garage (meta/, data/)

TLS certificate + key /etc/garage/tls/garage.crt, garage.key

nginx site /etc/nginx/sites-available/garage

Systemd unit /etc/systemd/system/garage.service

Firstboot script /usr/local/sbin/garage-firstboot.sh

Firstboot service /etc/systemd/system/garage-firstboot.service

Credentials file /root/garage-credentials.txt

Firstboot sentinel /var/lib/cloudimg/garage-firstboot.done

Step 13: Managing the Service

Status:

sudo systemctl status garage.service --no-pager | head -n 8

Restart (both the server and the TLS front):

sudo systemctl restart garage.service
sudo systemctl restart nginx.service

View recent activity (systemd journal):

sudo journalctl -u garage.service --no-pager -n 30 | tail -8

Step 14: Production Hardening

The shipped image is single-node — appropriate for development, staging, and small to medium workloads. Production deployments should:

  • Grow the data volume — the object metadata and data live on the dedicated disk at /var/lib/garage, so it scales independently of the OS disk. Expand it or attach a larger Premium SSD as capacity grows, then update the layout capacity with garage layout assign.

  • Restrict the admin and web ports — expose TCP 3903 (admin) and 3902 (web) only to the networks that need them, or keep them internal and reach the admin API over an SSH tunnel.

  • Use a CA-signed certificate — replace the per-VM self-signed certificate at /etc/garage/tls/ with one from a public CA or your internal PKI, so clients do not need --no-verify-ssl.

  • Rotate credentials on a schedule — mint fresh application keys with garage key create and delete old ones with garage key delete; the admin token can be rotated by editing /etc/garage.toml and restarting the service.

Security Model

  • No default login. The RPC secret, admin token, metrics token and S3 access/secret key are all generated uniquely on each virtual machine at first boot. Nothing is baked into the image.

  • Gated startup. garage.service only starts after first boot has written the per-VM configuration and secrets, so the service can never come up with a build-time secret.

  • Loopback by default. Every Garage listener is bound to 127.0.0.1; only the nginx TLS front is public, and only on the S3, web and admin ports.

  • Root-only credentials. /root/garage-credentials.txt is mode 0600, readable only by root.

Support

cloudimg provides 24/7 support with a guaranteed 24 hour response SLA. For assistance with this image, contact support@cloudimg.co.uk.