On AWS
EC2 with an EBS volume, S3 for media and backups, an ALB for TLS — and the two AWS defaults that quietly break a workspace.
Derived, not driven. Every value here comes from reading the hub's own requirements, and the stack has been exercised end to end locally — but this page has not been run against a live AWS account. Treat it as a careful specification rather than a transcript, and tell us where it is wrong.
A workspace is one stateful container with two volumes. Almost everything below follows from that.
Two AWS defaults that break it
Fargate. Its storage is ephemeral, so /repo starts empty on every task. With a seed configured that used to mean re-cloning over the loss; today it means the hub refuses to start. Either way it is not a deployment. Use the EC2 launch type, or run Docker on the instance directly.
EFS for /data. Yjs persistence is small-write-heavy and SQLite over NFS is the wrong shape for it. Use a gp3 EBS volume.
Neither is exotic advice — they are simply the two choices an AWS-shaped instinct reaches for first.
The shape
- EC2,
t4g.small(arm64) is comfortable. One instance. If you want the--rendersidecar (browser exports from the hosted studio), check its architecture before you pick the instance: the sidecar image is published for arm64 from after v1.0.3 only, and it is a ~3 GB image running Chromium, sot4g.small's 2 GB is tight for hub + render together. Either size up, or take at3.small(amd64), or skip the sidecar and export from Maude Desktop.workspace-up --renderrefuses up front on a host the image cannot run on, so a wrong choice here costs a message rather than a deployment. - A gp3 EBS volume, mounted for
/dataand/repo, withDeleteOnTermination=false. That single flag is the difference between replacing an instance and losing a project. - An ALB with an ACM certificate. It upgrades WebSockets natively, so you can skip Caddy and let the ALB terminate TLS. Set the idle timeout well above the default — sync sockets are long-lived; 4000 s is a reasonable ceiling.
- S3 for media and backups.
- DLM snapshots of the EBS volume as a second, independent layer. The hub's own backups depend on the hub working; snapshots do not.
No autoscaling, and desired count 1. The hub is single-process and stateful — Y.Doc in memory, SQLite on disk. Two instances over one volume is a corrupt database, not double the capacity. Set the deployment's minimumHealthyPercent to 0 so a rollout stops the old task before starting the new one.
IAM — least privilege for the bucket
The hub needs three verbs against one prefix, plus two reads on the bucket itself. Nothing else.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::acme-design-assets/*"
},
{
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetLifecycleConfiguration"],
"Resource": "arn:aws:s3:::acme-design-assets"
}
]
}s3:GetLifecycleConfiguration reads a bucket's configuration, not its data, and it is what lets workspace-up prove the point below instead of shrugging at it. Without it the s3-no-expiry check gets a 403 where it expects a 404, and — correctly, since it cannot read the config it cannot call the bucket safe — reports skipped forever. An earlier version of this page omitted the verb, so the recommended policy disabled a check the same tool performs.
No lifecycle rule on assets/. A canvas in git history can reference media no current canvas does, so "unreferenced" never means "unreachable" — an expired object is a permanently broken canvas with no recovery path. Backups under backups/ rotate themselves (14 generations); they need no lifecycle rule either.
Require IMDSv2
HttpTokens=required, HttpPutResponseHopLimit=1Not boilerplate. If you configure BYO identity, the hub fetches a key set from a URL your identity provider names. The hub pins that fetch to the issuer's origin and refuses to connect to any private or link-local address — but IMDSv2 is the layer that does not depend on us getting that right, and 169.254.169.254 is where the instance's credentials live.
Bringing it up
- Create the volume and instance; attach the EBS volume with
DeleteOnTermination=false. - Point DNS at the ALB — two records: the workspace domain and the canvas domain (e.g.
design.acme.comandcanvas.acme.com). The canvas iframe lives on its own origin (that split is what keeps studio cookies unreachable from canvas code), so without the second name every canvas is a blank frame in remote browsers. maude hub workspace-up --dry-run --domain design.acme.com --canvas-domain canvas.acme.com …and read what it would write.- Drop
--dry-run, then read the verification report honestly — a check that could not run prints as skipped, never as passed. maude hub restore-drill, and schedule it.- Remove
MAUDE_SEED_REPOfrom.env.
Then Durability, which is the page that matters once this is live.
Workspace mode
A hub that owns the project — autosave becomes append-only git history, media lives in object storage, and teammates sign in with an email instead of pasting a token. Self-hosted, one command, verified before it claims to work.
Durability
What survives a restart, a lost volume, and a second hub pointed at the same bucket — and how to prove it rather than hope.