Garage on Ubuntu 24.04 on Azure User Guide
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 (regiongarage). -
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 /healthis 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.servicesystemd 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:
:3900S3 API (SigV4),:3902web bucket serving,:3903admin API (Bearer token),:80plain-text info notice -
garage-firstboot.servicesystemd 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/garagefor object metadata and data -
AWS CLI v2 preinstalled at
/usr/local/bin/awsfor 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.

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.

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.

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.

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 withgarage 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 createand delete old ones withgarage key delete; the admin token can be rotated by editing/etc/garage.tomland 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.serviceonly 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.txtis 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.