Skip to content

Releases: haydenbleasel/ghost

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 19 Jul 01:57
Immutable release. Only release title and notes can be modified.
651897c

Major Changes

  • #76 e133e1f Thanks @haydenbleasel! - Rework Ghost from a hosted multi-tenant platform into a self-hosted, single-tenant deployment you run on your own Vercel account.

    Breaking: this release requires a fresh deployment. The database schema is incompatible with 1.x (migrations were squashed to a new baseline) and the auth/credential model changed entirely.

    • Deploy your own — the README now leads with a Vercel Deploy Button that clones the repo, provisions Neon Postgres, Upstash Redis, and Vercel Blob, and prompts for three secrets: HETZNER_API_TOKEN, AUTH_PASSWORD, and GHOST_SECRET.
    • Env-credential auth — Better Auth is removed (along with sign-up, passkeys, profiles, and avatars). The owner signs in with AUTH_EMAIL / AUTH_PASSWORD from the environment; sessions are a jose-signed JWT cookie verified fully in the edge middleware.
    • Hetzner token from the environmentHETZNER_API_TOKEN replaces the per-user encrypted token that previously lived in Postgres, and the token-management UI is gone.
    • Single-tenant schema — the User, Session, Account, Verification, and Passkey models are dropped; userId scoping is removed from servers and snapshot builds; golden images are keyed per deployment environment.
    • One signing secretGHOST_SECRET replaces BETTER_AUTH_SECRET, SESSION_SECRET, and BOOTSTRAP_JWT_SECRET; each token type keeps its own issuer/audience pair.
    • App at root — the dashboard now lives at / (with /new, /account, /[id]); the marketing site is removed and its content (features, supported games, how-it-works, animated logo) moved into the README.
    • Leaner by default — Sentry, PostHog, and Google Analytics are removed, along with Dependabot; React Compiler is enabled; all dependencies bumped (and zod unpinned now that @workflow/world no longer requires an exact version).

    Reconnecting servers created by a 1.x instance. Your VMs live in your own Hetzner account, so retiring the old control plane doesn't stop the games — players stay connected and nothing is deleted. But 2.0 starts with a fresh database, so those servers won't appear in your new dashboard. To move a world across:

    1. Copy the game data off the old VM. Open the VM's console in the Hetzner Cloud dashboard (or SSH if you added a key) and archive the game's data directory: tar -czf /root/world.tar.gz -C /var/lib/ghost/game/data . — then get the archive somewhere fetchable (e.g. scp it off, or upload it to any file host).
    2. Create a replacement server for the same game on your new instance and let it provision.
    3. Restore the data. Stop the new server from the dashboard, open the new VM's Hetzner console, and extract the archive over the data directory: curl -L <archive-url> | tar -xz -C /var/lib/ghost/game/data — then start the server again. (For a handful of individual files, the dashboard's Files tab can upload them directly instead.)
    4. Delete the old VM from the Hetzner console once you've confirmed the world came across, so it stops billing. Old VMs are harmless in the meantime — their agents just long-poll the retired control-plane URL and get nothing.

    If you'd rather start fresh, step 4 is the only one you need.

Patch Changes

  • #55 9f5d0fe Thanks @tomnieuwland! - Add Factorio support, using the factoriotools/factorio:stable image. Space Age DLC is enabled by default and toggleable. Supports Factorio Username/Password for server discovery.

  • #56 274f368 Thanks @tomnieuwland! - Adds a script for generating docker compose files from defined games for local testing

  • cf52b97 Thanks @haydenbleasel! - Report agent command results accurately when the ack POST fails transiently — a succeeded command was re-acked as failed and the remaining commands in the envelope were abandoned; acks are now best-effort and isolated from command execution

  • bcf6715 Thanks @haydenbleasel! - Make agent enrollment survive a lost response — one dropped enroll reply used to brick the VM forever (each retry generated a fresh keypair and hit "Token already used" in a fatal loop, forcing server deletion); the agent now persists its keypair before the enroll POST and the server replays the success idempotently when a burned token arrives with the already-enrolled public key

  • cf52b97 Thanks @haydenbleasel! - Fix lost agent events: a failed event flush never scheduled a retry (terminal events like 'Game stopped' could be dropped forever on a quiet agent), and concurrent flushes silently no-oped, letting shutdown discard buffered events mid-flight; flushes are now serialized and rescheduled on failure

  • cf52b97 Thanks @haydenbleasel! - Fix every agent file command (list, delete, install-from-URL) crashing with a ReferenceError — agent/src/files.ts used node:path helpers it never imported; the agent package is now included in the repo typecheck so this class of bug can't ship again

  • cf52b97 Thanks @haydenbleasel! - Clean up the temporary .part download when installing a file from URL fails at the final rename step, instead of leaking up to 500MB per failed attempt into the game data directory

  • cf52b97 Thanks @haydenbleasel! - Stop replaying the last 200 historical log lines on every start/restart — the log tail now follows from the triggering command's start time, so old lines no longer reappear in the console with fresh sequence numbers

  • bcf6715 Thanks @haydenbleasel! - Re-attach the console log tail when the agent boots with the game already running — after an agent crash or VM reboot the tail only started on the next user-issued start/restart, so the dashboard console silently went dead; the agent now probes the container on boot and tails from boot time (no historical replay)

  • cf52b97 Thanks @haydenbleasel! - Prevent concurrent agent state saves from racing on a shared temp file, which could crash a just-executed command or corrupt state.json and brick the agent on next boot; saves are now serialized and use unique temp files

  • cf52b97 Thanks @haydenbleasel! - Return 401 JSON from authenticated API routes when the session has expired instead of redirecting to the sign-in page — fetch/EventSource consumers were receiving the sign-in HTML with a 200, causing endless JSON parse errors and a dashboard that never noticed it was signed out; the server page poll now redirects to sign-in on 401

  • bcf6715 Thanks @haydenbleasel! - Recover snapshot builds whose builder-VM create response was lost — the retry's unique-name conflict was turned straight into a fatal build failure, stranding a billed ghost-builder-* VM that nothing tracked or deleted; the step now adopts the existing VM by its deterministic name, and transient 429 rate limits retry instead of failing the build

  • cf52b97 Thanks @haydenbleasel! - Redeliver commands whose delivery or ack was lost in transit — commands were marked delivered before the agent received them with no redelivery path, so a dropped long-poll response left them stuck forever; the agent now dedupes redeliveries against its recent command history

  • cf52b97 Thanks @haydenbleasel! - Escape dollar signs in generated docker compose files — Docker Compose interpolates $VAR/${VAR} in every string, so a server name like 'Ca$h Grab' was silently mangled and an unclosed '${' made compose fail to start entirely; applies to all games including Factorio's literal config blocks

  • bcf6715 Thanks @haydenbleasel! - Apply the observed-state transition when a retried agent event dedupes — if the first attempt crashed between inserting the event and updating the server row, the retry's duplicate hit returned early and the server was stuck showing "running" after the game had stopped (also breaking...

Read more

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 18 Jul 23:28
Immutable release. Only release title and notes can be modified.
6212266

Minor Changes

  • #53 2363908 Thanks @haydenbleasel! - Add Left 4 Dead 2 as a supported game. Uses the left4devops/l4d2 image with a docker-compose inline server.cfg for cvars that need to survive map loads (z_difficulty, sv_allow_lobby_connect_only). Join passwords are disabled — L4D2's coop lobby flow drops clients after sv_password verification on direct connect, so servers stay private via unlisted IP/port instead.

Patch Changes

  • #67 f1e0609 Thanks @haydenbleasel! - Return 400 instead of an unhandled 500 when the heartbeat, events, or command-ack agent routes receive a signed request with a malformed JSON body. JSON.parse sat outside the routes' error handling, unlike the other agent endpoints.

  • #67 f1e0609 Thanks @haydenbleasel! - Block symlink path traversal in agent file operations. resolveInRoot only normalized the path string, so a symlink planted inside the bind-mounted data root by a compromised game container could redirect FILES_INSTALL_FROM_URL/FILES_DELETE writes outside the root (the agent runs as root on the host). Paths are now resolved through realpath on their deepest existing ancestor and re-verified against the real data root before any read, write, or delete.

  • #67 f1e0609 Thanks @haydenbleasel! - Always validate game settings when creating a server. A request that omitted the settings field skipped validation entirely, bypassing required fields like Don't Starve Together's cluster token and provisioning a paid VM that could never start.

  • #67 f1e0609 Thanks @haydenbleasel! - Prevent double-delivery of agent commands. claimPendingCommands did a find-then-update, so two overlapping long-polls (e.g. an agent retry racing a still-running handler) could both read the same pending rows and execute a command — including destructive ones like FILES_DELETE — twice. Each row is now claimed with a conditional update on status: "pending", so exactly one caller wins it.

  • #67 f1e0609 Thanks @haydenbleasel! - Write the agent's state file atomically. saveState wrote a temp file but then copied it into place with a truncate-and-write, so a crash mid-save could leave a corrupt state.json that permanently bricked the agent on next boot. The temp file is now swapped in with rename.

  • #67 f1e0609 Thanks @haydenbleasel! - Fetch all pages when listing a server's backups and snapshots. listImagesForServer requested a single page of 50 images for the whole account and filtered client-side, so servers on accounts with more than 50 images silently lost backups from the UI. Backups are now filtered server-side via Hetzner's bound_to parameter and both listings follow meta.pagination.next_page to exhaustion.

  • #67 f1e0609 Thanks @haydenbleasel! - Make snapshot builder VM creation idempotent. stepCreateBuilderVm now returns the already-persisted providerBuilderId on retry instead of creating another VM, and the builder name is derived deterministically from the build id so a retried create whose first response was lost is rejected by the provider's unique-name constraint rather than silently billing a second orphaned VM.

  • #67 f1e0609 Thanks @haydenbleasel! - Harden docker-compose value escaping. escapeComposeValue only escaped double quotes, but values are emitted inside YAML double-quoted scalars where \ is the escape character — a backslash in a server name or game setting (e.g. world name, MOTD) produced invalid YAML or terminated the scalar early, and embedded newlines could inject additional YAML lines into the root-run compose file. Backslashes are now escaped before quotes and control characters are stripped.

  • #67 f1e0609 Thanks @haydenbleasel! - Stop docker compose invocations from hanging the agent. The subprocess was spawned with piped stdout/stderr that were never read, so any compose command producing more than the OS pipe buffer (~64KB, easily hit by image-pull progress) blocked forever with no timeout. Both pipes are now drained while awaiting exit, and compose failures include the last stderr line for easier debugging.

  • #67 f1e0609 Thanks @haydenbleasel! - Make agent enrollment atomic. The bootstrap token was burned in a transaction but the agent row was created after it committed, so a transient create failure left the token burned with no agent — the VM's retries then hit "Token already used" forever and the server had to be re-provisioned. The burn is now a conditional update inside the same transaction as the create, which also closes the race where two concurrent requests could both pass the burned-token check.

  • #67 f1e0609 Thanks @haydenbleasel! - Deduplicate retried agent event batches. When the agent re-sent a batch after a lost response, the server assigned fresh ids and seqs, duplicating activity timeline entries and log lines. Agent-sourced rows now use deterministic ids derived from the protocol's clientEventId (activity) and agentSeq (logs), scoped by agent id, so replayed inserts are dropped.

  • #53 2363908 Thanks @haydenbleasel! - Regenerate the Hetzner OpenAPI types. Picks up @deprecated markers on the singular get_<resource>_action endpoints (per the 2026-04-30 changelog), the Load Balancer service/target type split, the primary/secondary Zone schema split, and per-location pricing breakdowns. None of the endpoints we call were affected.

  • #67 f1e0609 Thanks @haydenbleasel! - Close the console log stream on unmount. LogsStream's effect cleanup only set a flag and never closed the live EventSource or cleared the pending reconnect timer, so navigating away from the Console tab left the SSE connection (and the server's 500ms log poll loop) running for up to the route's 300s maxDuration, with stale listeners still updating state.

  • #67 f1e0609 Thanks @haydenbleasel! - Size the Minecraft JVM heap from the actual machine instead of a hardcoded 6G. Rescaling a Minecraft server previously had no effect on the game (and on machines smaller than the default the fixed heap could exceed total RAM). The compose builder now receives the provider-reported machine memory and gives the JVM ~75% of it; the heap is applied whenever a new compose is pushed (install and settings updates).

  • #67 f1e0609 Thanks @haydenbleasel! - Stop reporting transient provisioning errors as "Owner has not configured provider credentials". The catch in stepCreateProviderServer swallowed every error from the provider lookup (including DB blips) into a permanent FatalError; it now only converts MissingProviderCredentialsError and lets everything else retry.

  • 5c50664 Thanks @haydenbleasel! - Refactor the Hetzner integration behind a provider-agnostic Provider interface so a future AWS or GCP backend can be added by writing a new implementation rather than rewriting call sites. lib/hetzner/ is replaced by lib/providers/ (interface + types + per-user factory) with lib/providers/hetzner/ as the only implement...

Read more

v1.1.1

Choose a tag to compare

@github-actions github-actions released this 08 May 07:45
Immutable release. Only release title and notes can be modified.
fde9640

Patch Changes

  • #48 e12b9e0 Thanks @haydenbleasel! - Fix Server.observedState not transitioning when the agent runs a STOP or START. Previously the agent emitted a phase: "stopped" activity event after docker compose stop, but nothing reconciled Server.observedState from the activity stream — so the badge stayed on "running" and users saw "the server does not stop" even though it had. emitActivity now updates Server.observedState for the agent-driven steady-state phases (stopped"stopped", healthy"running"); the provisioning workflow continues to own its own transitions. The START handler now also enqueues a healthy event after composeUp() so the badge flips back to "running", the agent's STOP handler kills the log tail after composeStop() so container shutdown messages reach the Console tab, and executeCommand logs start/success/failure so the agent's process console is no longer silent. Also disables the Delete menu item once desiredState is "deleted" so users can't re-trigger teardown on a server that's already being deleted.

  • #48 e12b9e0 Thanks @haydenbleasel! - Refresh marketing branding: new logo, Geist Pixel display font, redesigned homepage, and updated favicon and Open Graph image.

  • #48 e12b9e0 Thanks @haydenbleasel! - Scope the Hetzner builder snapshot's description field by SNAPSHOT_ENVIRONMENT (ghost-gold-production, ghost-gold-preview-<branch>, ghost-gold-development) instead of a shared ghost-gold literal. The labels already scoped which snapshot a deployment uses for provisioning; this aligns the human-readable description so production / preview / local images are easy to tell apart in the Hetzner dashboard and won't be confused for one another during cleanup.

  • #48 e12b9e0 Thanks @haydenbleasel! - Replace Promise.race(hook, sleep, ...) blocks in provisionServer with polling loops that read state from the database. Eliminates the Workflow run completed with N uncommitted operation(s): sleep warnings emitted when hooks won the race and left orphan timers ticking on the backend. Cancellation now propagates through desiredState polling (worst-case latency: one poll interval — 6s during boot/enroll, 10s during install) instead of a hook, and the install wait reads the latest agent-reported phase from activity_events instead of subscribing to the phase hook.

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 07 May 07:30
Immutable release. Only release title and notes can be modified.
c0bb797

Minor Changes

  • #47 6080d27 Thanks @haydenbleasel! - Add support for Counter-Strike 2 dedicated servers. Configurable game mode, starting map, max players, bot quota and difficulty, LAN mode, and Steam GSLT for public servers.

  • #47 6080d27 Thanks @haydenbleasel! - Add support for Don't Starve Together dedicated servers. Configurable game mode, server intention, max players, PvP, and Klei cluster token.

  • #47 6080d27 Thanks @haydenbleasel! - Add support for Satisfactory dedicated servers.

  • #47 6080d27 Thanks @haydenbleasel! - Add support for V Rising dedicated servers.

Patch Changes

  • 743f3c0 Thanks @haydenbleasel! - Pin zod to 4.3.6 to match the version expected by @workflow/world and @workflow/world-vercel. Zod 4.4 changed how discriminated unions handle fields overridden with z.undefined() via .extend(), causing the workflow runtime to throw Schema validation failed for POST /v3/runs/.../events on every callback and leaving runs wedged in the pending state.

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 04 May 07:31
Immutable release. Only release title and notes can be modified.
4448f0e

Major Changes