greyship / how it works

How Greyship works

Greyship is serverless for Go. This is exactly what happens when you deploy, how your app sleeps and wakes, and how your code and secrets are kept apart from everyone else's. Step through the boards; nothing is hand-waved.

8 chapters · 19 min read

01 · 3 min

How Greyship works

Greyship is serverless for Go: a control plane that builds and places your apps, and a data plane that runs them in sandboxes and wakes them on the next request.

Greyship runs Go backends without servers to manage. You push a normal Go program (any router, any layout) and get a URL. When nobody is using it, it sleeps and costs nothing; the next request wakes it. This page is the map. Every other article zooms into one part of it.

Two planes

The system is split in two. The control plane decides what should run: accounts, projects, deployments, configuration, scheduling and billing. The data plane actually runs it: edge routers that receive traffic, and servers that host your app inside sandboxes.

greyship — the whole system1 / 5
Two halvesThe control plane decides what should run. The data plane runs it and serves your traffic. They meet only through messages, so either side can grow on its own.

Two paths cross these planes. The deploy path turns your source into binaries and places them on servers. The request path takes a visitor's request to a running copy of your app, waking it if needed.

The parts, one line each

PartWhat it does
APIThe public /v1 API. The CLI, the dashboard and GitHub webhooks all talk to it, and it is the only part that writes to the database.
BuilderFetches your source, works out your services, downloads modules through a cache and compiles each service with go build, inside a sandbox.
SchedulerKeeps a picture of every server's free memory and decides where each instance runs.
Node agentRuns on every server. Creates, pauses, resumes and stops sandboxes, decrypts your env vars at start and collects logs and metrics.
Guest initThe first process inside every sandbox. Starts your binary, forwards signals and captures stdout and stderr.
Edge routerTerminates HTTPS, maps hostnames to services, holds requests while an app wakes, and puts apps to sleep after they go quiet.

Why only Go

Every Greyship app is a single static Go binary. That is what makes the rest possible: no container image to pull, no runtime to boot, a tiny root filesystem, and start-up in milliseconds. It also lets Greyship tune the Go runtime for you: GOMAXPROCS and GOMEMLIMIT are set to your instance's real CPU and memory limits, so Go never throttles itself or runs past its memory.

Where to go next

02 · 3 min

What happens when you run grey deploy

Step by step: how grey deploy links your project, diffs env vars, uploads source, builds each Go service in a sandbox, runs migrations and switches traffic with zero downtime.

grey deploy is the one command you need. Run it in your Go project and a few seconds later you have a live HTTPS URL. Here is everything it does on the way, in order.

$ grey deploy
✓ Created project orders-api (from go.mod)
  .env.prod vs production: + 2 keys, = 9 match
✓ Uploaded 412 files (1.8 MB)
  api      web     starts a server in cmd/api/main.go:22
  worker   worker  no server start found from main()
✓ Built 2 services
✓ Release step: cmd/migrate exited 0
✓ Live  https://api-orders-api.greyship.app
A first deploy. Later deploys skip the project step.
grey deploy — from your folder to live traffic1 / 8
Know the projectgrey reads .greyship/project.json. If the folder isn't linked yet, it creates the project from your go.mod module name and writes the link, without asking.

1. Knowing which project

The CLI looks for .greyship/project.json in the current folder and its parents. It holds the team, the project name and the project's id. It contains no secrets and is meant to be committed, so teammates and CI deploy to the same project.

You don't need to run grey init first. If there is no link, grey deploy takes the name from the last part of your go.mod module path (github.com/acme/orders-api becomes orders-api), creates the project and writes the link. The only time it stops to ask is when your team already has a project with that name: it never deploys over someone else's project without a yes.

2. Comparing env vars, without sending them

Your stored production variables are the source of truth. The CLI fetches their names and value hashes (never the values), hashes .env.prod on your machine, and shows what would change:

.env.prod vs production:
  + NEW_FLAG        only in file → will be added
  ~ STRIPE_KEY      different    → will be updated
  = 12 keys match
  · SENTRY_DSN      only stored  → kept
Continue? [y/N]

Only added and changed values are sent, over TLS, to the encrypted env store. They never travel with your source code and are never visible to the build. Deploy never rewrites your local file. More in How your secrets are protected.

3. Uploading your source

The CLI packs the folder, honouring .gitignore and .greyshipignore, and asks the API for a one-time upload URL. The tarball goes straight to object storage, not through the API.

4. Finding services and building

A builder picks up the job and starts a fresh sandbox for it. The sandbox can reach only the Go module cache, nothing else on the internet or the platform. It works out your services (see How Greyship reads your repo), then for each one runs:

CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" ./cmd/api

The Go version comes from your go.mod (toolchain, then go). Modules are fetched through a shared cache, so repeated deploys don't download the internet again. If any build fails, the deployment is marked failed and your current version keeps serving.

5. The release step

Database migrations need to run once, before new code takes traffic. If your repo has cmd/migrate (or you set [release] in greyship.toml), it is built with everything else and run exactly once per deploy, in the same sandbox and network rules as your services and with your production env. Only one release runs per project at a time. If it exits non-zero, the deploy stops and the old version keeps serving.

6. Health check and traffic switch

One instance of each web service starts and must accept connections on $PORT within 30 seconds (or answer 200 on your health_path). Once every service is healthy, the routers switch to the new version at once. The old instances finish their in-flight requests, get SIGTERM, and stop. Apps that scale to zero then go back to sleep until the first real request.

03 · 2 min

How Greyship reads your repo

Greyship finds every service in a Go repo with no config: it lists main packages, names them by folder and reads your code to tell web services from workers.

Most platforms make you describe your app in a config file. Greyship reads your repo instead. A normal Go backend (one API, or an API plus workers plus crons in one module) deploys with no greyship.toml at all.

reading a repo — no config file1 / 6
Find every mainGreyship walks the module and lists every folder with a package main that would build for Linux.

Finding services

Every folder with a package main is a candidate. Files are only counted if they'd be part of a Linux build: _windows.go files and files behind //go:build ignore or //go:build tools drop out, exactly as they would for go build.

Where the main livesBecomes
main.go or cmd/main.goone service named after the project
cmd/<name>/a service named <name>
anywhere elsea service named after its folder; if two clash, the nearest meaningful parent is added (orders-server, payments-server)
cmd/migrate/the release step, run once per deploy
vendor/, testdata/, examples/, _x/, .x/skipped
only run by //go:generate go runskipped, it's a tool

Web or worker: reading the code

Greyship doesn't guess from folder names. It parses your main() and follows the calls into your own packages. If anything reachable starts a server, the service is web; otherwise it is a worker. It recognises the standard library (http.ListenAndServe, http.Server.ListenAndServe, net.Listen) and the popular frameworks: gin, echo, fiber, chi, gorilla/mux, gRPC, Connect, fasthttp and Hertz.

Being careful matters as much as being clever. A worker that calls http.Get is still a worker: using HTTP as a client doesn't make it a server. A shared package that contains a server function your worker never calls doesn't make it web either.

  api      web     starts a server in internal/server/server.go:41 (http.ListenAndServe)
  worker   worker  no server start found from main()
  cleanup  cron    type = "cron" in greyship.toml
Every deploy prints what it found and why, so a wrong guess is obvious.

When you want to override

greyship.toml is optional and only for the few things that can't be inferred. Cron schedules are the main one, since nothing in code says "run at 3am". A declared name that matches a detected service overrides it; path renames a service or includes a folder that was skipped; exclude leaves one out.

[services.cleanup]
type = "cron"
schedule = "0 3 * * *"

[services.public]
path = "./cmd/api"      # rename api to public

[services.debug]
exclude = true

Typos fail loudly instead of being ignored: an unknown setting, a schedule that isn't valid cron, or two services that would get the same name each stop the deploy with the one-line fix.

04 · 3 min

Sleeping and waking: how scale to zero works

How Greyship scales Go apps to zero: a 30s grace window, pause instead of stop, waking during the TLS handshake, scaling out under load, and $0 while idle.

Idle costs $0 by default. That's the promise, and it's only worth something if waking up is fast. This is how a Greyship app goes to sleep and comes back.

one service, one quiet afternoon1 / 6
A request arrivesThe edge router looks up the service from the hostname in memory, with no database call. If an instance is running and has room, the request goes straight through.

The grace window

Other serverless platforms freeze your process the moment a response is sent, which kills any goroutine still working. Greyship waits. After the last in-flight request finishes, the instance stays running for a grace window, 30 seconds by default and configurable per service. Background work finishes, and a follow-up request lands on a warm process.

Pause first, stop later

When the grace window ends, the instance is paused, not stopped. Its memory stays where it is; it just gets no CPU. Billing counts running time only, so a paused app costs nothing. Paused instances are stopped only when the server needs the memory (least recently used first) or after 24 hours idle. Most wake-ups are therefore resumes, not cold starts.

Waking on the handshake

Every HTTPS connection starts with a TLS handshake, and the very first message names the host. The edge router uses that moment: as soon as it sees api.acme.com in the handshake, it starts resuming the service. By the time the encrypted request arrives, the app is usually already running.

If nothing is running or paused, the router holds the request (up to 30 seconds) while a fresh instance starts, then forwards it. Your users see a slower first response, never an error.

Scaling out under load

Each instance handles up to concurrency requests at once (100 by default). If every instance stays above that for a couple of seconds, the router asks for another one, up to max_instances. All copies share one URL. When traffic drops, they pause one by one.

SettingDefaultWhat it controls
grace30sHow long an instance keeps running after its last request
concurrency100In-flight requests per instance before another one starts
max_instancesplan limitThe most copies of a service that can run at once
min_instances0Copies kept running even with no traffic

Workers and crons sleep too

Background services follow the same rule: they're woken by events, never kept running. A worker wakes when a message lands on its built-in queue, when another service calls worker.internal, or on a schedule; it goes back to sleep once the queue is empty and the grace window passes. A cron starts at its scheduled time, runs, and is stopped.

05 · 2 min

Every grey command, under the hood

What each grey CLI command does behind the scenes: login, init, deploy, dev, logs, env, domains, rollback, pprof and more, and which parts of Greyship they touch.

The grey CLI is a single static Go binary. It is a client of Greyship's public API, the same API the dashboard uses, so nothing the CLI does is hidden from the dashboard or the other way round. The installer also creates gray and greyship, so every spelling works. Every command accepts --json for scripts, and GREYSHIP_TOKEN makes it work in CI.

Account

CommandWhat happens under the hood
grey loginAsks the API for a one-time device code, opens your browser and shows the code. You approve it while signed in; the CLI polls every 5 seconds and receives a token for one team. The token is saved in your user config folder, readable only by you.
grey logoutRevokes the token on the server first, then forgets it locally. A stolen copy stops working too.
grey whoamiShows the account and team behind the current token.
grey teamsLists the teams you belong to and your role in each.
grey tokensCreates, lists and revokes API tokens for CI and scripts. A new token is shown once; only its hash is stored.

Projects

CommandWhat happens under the hood
grey initOptional. Creates a project named after your go.mod module (or a name you choose) and writes .greyship/project.json, a small file with no secrets that you commit.
grey projectsLists your team's projects and marks the one this folder is linked to.
grey deployLinks the project if needed, diffs .env.prod by hash, uploads your source, builds every service in a sandbox, runs the release step and switches traffic. Full walkthrough.
grey rollbackPoints traffic back at an earlier deployment. Nothing is rebuilt and the release step doesn't run again, so it takes effect at once.
grey openOpens the service's URL in your browser.

Running and observing

CommandWhat happens under the hood
grey devRuns the whole project on your machine the way Greyship would: detects services, builds each one, starts them on local ports, loads .env then .env.local, and makes <service>.internal resolve locally. Rebuilds a service when its files change.
grey psLists services and their instances: running, paused or stopped.
grey logs -fStreams stdout and stderr from every instance, with JSON logs pretty-printed. Add --service to narrow it.
grey pprofTakes a CPU, heap or goroutine profile from a live service, if it imports greyship.dev/agent. The profile travels over the sandbox's side channel, not your app's port.

Env vars and domains

CommandWhat happens under the hood
grey env set / rm / lsWrites, removes or lists the project's main variables. Values are encrypted before they're stored, and only the affected services restart.
grey env pushLoads a whole file (default .env.prod), shows a key-only diff and asks before sending the changed values.
grey env pullThe only command that downloads stored values. Writes .env.prod and asks before overwriting an existing file.
grey domains addAdds a custom domain, tells you the CNAME or A record to create, verifies it and gets a certificate automatically.

06 · 3 min

How your secrets are protected

How Greyship handles env vars and secrets: compared by hash, encrypted with envelope encryption, decrypted only at instance start, scoped per service, never logged.

Your database password, your Stripe key, your signing secrets: a platform that runs your code has to hold them. This is exactly how Greyship does it, from the file on your laptop to the process that uses them.

the path a secret takes1 / 6
Your file stays yoursgrey reads .env.prod on your machine. The file is never uploaded with your source, never rewritten by deploy, and never visible to builds.

Your env files

FileUsed byLeaves your machine?
.envgrey devNever
.env.localgrey dev (overrides .env)Never
.env.prodgrey deployOnly changed values, only to the encrypted store
.env.previewpreview deploysOnly changed values, only to the encrypted store

If one of these files is tracked by git, the CLI warns you loudly and suggests adding it to .gitignore. Deploys triggered from GitHub never read env files at all; they use the variables already stored for the project.

Compared by hash, sent only when changed

On every deploy the CLI downloads key names and hashes of the stored values, never the values themselves, and hashes your .env.prod locally. It shows a key-only diff and sends only added and changed values, over TLS, to the env endpoint. The only way to download stored values is grey env pull, and it asks before overwriting a file.

Encrypted at rest, twice over

Greyship uses envelope encryption. Every value is encrypted with your team's data key. That data key is stored only in encrypted (wrapped) form, sealed by a master key kept outside the database. Someone with a copy of the database has ciphertext and a wrapped key, and nothing that opens either.

In the dashboard values are masked, and revealing one requires signing in again.

Decrypted at one moment, in one place

Secrets are decrypted only when an instance starts, by the node agent on that server, and handed to the sandbox's first process over a local channel. They are never decrypted by the API, never visible to builds, never written to logs or error messages, never included in API responses, and never stored in a snapshot in plain text: snapshots are taken before secrets are injected.

Each service gets only what it needs

Variables have two layers. Main variables go to every service in the project. Service overrides go to one service only. A service receives the main variables plus its own overrides, and never another service's, so payments can hold a key that api can't read. A service can also exclude a main key entirely. The agent only ever decrypts the rows in that service's effective set.

Tokens and logins

  • There are no passwords. You sign in with a one-time code sent to your email, or with Google or GitHub.
  • Sessions and API tokens are random values stored only as SHA-256 hashes. A token is shown once, when it's created.
  • Every token belongs to one team and only works inside it. grey logout revokes the token on the server, not just on your machine.

07 · 2 min

Isolation and networking

How Greyship isolates every Go app: each instance in its own gVisor sandbox or Firecracker microVM, per-tenant networks, private service-to-service calls, HTTPS everywhere.

Greyship treats every app and every build as untrusted, including yours, because the same rules protecting you from other tenants protect them from you. Nothing a user uploads ever runs directly on a host.

who can reach whom1 / 5
Public traffic comes in one wayRequests from the internet reach your services only through the edge router, over HTTPS. Nothing else can open a connection to an instance.

One sandbox per instance

Every instance and every build runs in its own sandbox, either a gVisor sandbox or a Firecracker microVM. gVisor puts a user-space kernel between your app and the real one; a microVM gives it a minimal kernel of its own. Either way your process never talks to the host kernel directly.

  • The root filesystem is your binary on a minimal base, mounted read-only. /tmp is a small in-memory scratch space.
  • Hard limits on memory, CPU and number of processes, and GOMAXPROCS / GOMEMLIMIT set to match them.
  • Builds run in a fresh sandbox per job that can reach only the Go module cache.

The network

  • Each instance gets its own network namespace on a per-tenant network.
  • Inbound traffic is accepted only from Greyship's routers. Instances never talk to each other directly.
  • Traffic to other tenants, the host, cloud metadata and the internal network is dropped.
  • Outbound internet works through NAT, so you can call any API or database. Port 25 is blocked and connection rates are limited to stop abuse.

Private services and .internal names

Services in the same project reach each other at http://<service>.internal, for example http://orders.internal. Hard-coding that address in your code is safe: it only resolves inside your project, and grey dev resolves it on your laptop too.

Those calls go through an internal router that only accepts traffic from instances of the same project and never from the internet. It applies the same wake, hold and scale logic as the edge, so a private service can scale to zero and still answer the first internal call. Mark a web service visibility = "private" and it gets no public URL at all. HTTP/1.1 and h2c (gRPC, Connect) work end to end.

HTTPS everywhere

Every public service gets <service>-<project>.greyship.app with a certificate, and custom domains get theirs automatically through ACME, renewed without you thinking about it. Plain HTTP redirects to HTTPS by default. Greyship's own components talk to each other over a private network with mutual TLS.

08 · 1 min

What you pay for

Greyship bills Go apps per second of running time. Idle apps cost $0, spend caps are on by default, and always-on is an explicit, clearly labelled opt-in.

Serverless only means something if the bill follows the work. Greyship charges for the time your code is actually running, not for capacity you reserved in case.

What's metered

  • Running time, per second: vCPU-seconds and GB-seconds while an instance is running.
  • Requests: public and internal requests are counted the same way.
  • Egress: data your apps send out to the internet. Traffic between your own services is free.

Paused and stopped instances aren't running, so they aren't billed. The grace window after the last request counts as running time, since your goroutines are still working in it.

Idle costs $0

Nothing keeps running while idle unless you ask for it. A service with min_instances = 1 stays warm and is labelled always on · billed 24/7 everywhere it appears. The same goes for open WebSockets: a connected socket keeps its instance awake, and the pricing page says so.

Spend caps are on by default

Every team has a spend cap from day one. If usage reaches it, services pause and the owner gets an email. A bad loop or a traffic spike can't run up a bill you didn't agree to.

Plans and payment

There is a free plan that needs no card, a Pro plan with usage included, and a Team plan per seat. Pay in US dollars with Stripe, or in naira with Paystack (cards and bank transfer); naira prices are set locally, not converted live. See pricing for the current numbers.

Questions and comments

Something unclear, or a case we haven't covered? Ask. The team answers here.

Loading comments…

Posts appear after a quick review.