openship.json
The declarative deploy config for GramCode — like vercel.json or railway.toml. Declare framework, build, runtime, env, domains, routes, resources, services and monorepo layout, and GramCode deploys the same way every time.
openship.json is a repo-root file that tells GramCode how to build, run, route, and scale a
project. It mirrors what you'd otherwise set in the deploy wizard,
so a repo becomes "ready to ship on GramCode" and auto-deploy on push
and openship deploy become deterministic — no wizard needed.
When refreshing services for a project with a saved subfolder, GramCode uses that subfolder's
openship.json when present. Otherwise, it uses the repository-root manifest, preserving existing
service definitions and environment defaults. The saved build directory still applies.
Authoritative overlay
GramCode auto-detects your stack first, then applies every field present in openship.json on
top. Fields you omit keep their detected value. So a good openship.json is small — declare
only what you want to override or pin, not the whole detected config.
Add the $schema line for editor autocomplete and inline validation:
{
"$schema": "https://gramcode.si/openship.schema.json",
"framework": "nextjs",
"port": 3000
}Validate it any time with the CLI:
openship config init # scaffold a starter openship.json
openship config validate # check it against the schemaIt's JSON, not JSONC — no comments, no trailing commas.
Build
Prop
Type
Runtime
Prop
Type
Release commands
releaseCommands runs once per deploy, after the build and before the new version goes
live: the place migrations belong. Each command runs in order, its output streams into
the deploy log, and a non-zero exit fails the deploy: nothing cuts over, and the
version that was already serving keeps serving.
{
"releaseCommands": ["php artisan migrate --force"]
}Nothing is injected for you. A framework's release set is a choice, so an app with no
releaseCommands runs no release phase at all. The equivalents elsewhere:
| Stack | Typical release commands |
|---|---|
| Laravel | php artisan migrate --force |
| Rails | bundle exec rails db:prepare |
| Django | python manage.py migrate --noinput |
Rails uses db:prepare rather than db:migrate because it also creates the database and
loads the schema when it is empty, which is exactly the state of a first deploy.
Set these commands in Build settings → Before activation, openship.json, or the
project API/SDK. A repository scan seeds a new project; existing projects keep their saved
commands, including an explicit empty list. The list is frozen on each deployment.
Each command gets the same environment variables as your start command and a 10-minute budget. It runs once per forward deployment attempt, in list order. Failure, timeout, or cancellation stops activation and leaves the previous application running.
On Docker, each command runs in a temporary container using the candidate image, the project's persistent mounts, and its connected-service networks. It publishes no ports and does not restart. GramCode removes the container and its anonymous scratch volumes afterwards. On Bare, commands run in the staged release directory after persistent paths are linked, before the supervisor changes the running app. When a forward deploy reuses a retained Bare release, the command gets a temporary copy so it cannot edit the live application's files.
Use persistent storage for data
Only database and mounted-volume changes survive a Docker release container. Put
image-local cache generation, symlinks, and collected assets in the build or start
command. Store SQLite files under a declared persistent path, such as storage/, so the
release command and app use the same database on both Docker and Bare.
Runtime support
Release commands currently require a single-app Docker or Bare deployment, including portless workers. The app may use attached or connected database services without changing its single-app deployment mode.
Full Compose/monorepo deployments, static sites, and runtimes without one-off command support fail before activation when commands are configured. Per-service release commands are not implemented. An action explicitly targeting only selected services skips the main app's release commands.
Rollbacks and database changes
Rollbacks skip release commands, including rollbacks that rebuild from Git. Other redeploys run the commands saved on that deployment, even when they reuse an existing image.
Use idempotent commands and schema changes compatible with the previous application. Rolling code back does not undo database changes, and a command may have made partial changes before failing. GramCode does not promise a transaction across multiple commands.
Persistent storage
Everything a container writes is discarded when the next version replaces it. volumes
names the paths to keep.
A bare path is relative to the app; full compose syntax works too, so you can point a mount at a specific volume name or a host directory:
{
"volumes": ["storage", "uploads:/app/public/uploads", "/srv/data:/app/var"]
}Omit the field to inherit the framework's own defaults (a Laravel app keeps storage/
with no configuration). Declare [] to turn persistence off entirely — that is
different from omitting it.
Named volumes are scoped per project, so two projects that both declare storage never
share one. On a bare (non-container) deploy the same paths are kept outside the release
directory and symlinked in. GramCode Cloud has no volume primitive: a declared mount is
reported and skipped there, so use object storage for uploads instead.
Compose services declare their own mounts under services[].volumes.
Environment variables
env is an object. A value is a plain string, or { "value", "secret" } to mark a secret
(encrypted at rest — never returned to the client afterward). These declarations are active
automatically; a value saved in the dashboard or CLI for the same key overrides the repository default.
{
"env": {
"NEXT_PUBLIC_URL": "https://app.acme.com",
"DATABASE_URL": { "value": "postgres://…", "secret": true }
}
}Domains
domains is an array of hostnames or objects. A bare label ("myapp") is a free
*.opsh.io-style subdomain; a dotted hostname ("app.acme.com") is a custom domain.
{ "domains": ["app.acme.com", { "domain": "api.acme.com", "port": 8080, "type": "custom" }] }Prop
Type
Routes
routes reproduces vercel.json-style routing, compiled to the reverse proxy at deploy.
Prop
Type
Resources
CPU/memory limits for the project's containers — a named tier, or explicit values (which become
the custom tier). See Sleep mode & resources.
Full server capacity by default
The default is unlimited on both connected and managed Cloud servers. A container
can use the host's available CPU and memory, shared with the other applications on that
server. Select a tier or explicit values only when you want an additional container cap.
The Cloud subscription sets the server's allocation separately. Changing a project's resource tier never creates or resizes a server.
Prop
Type
{
"resources": { "cpuCores": 4, "memoryMb": 8192 }
}Services (compose)
Declaring services makes the project a multi-service (Docker) project — see
Compose / multi-service. Each entry requires a name.
Already have a compose file? Don't re-declare it here — point composePath at it and leave
services out. That works for a compose file anywhere in the repo (deploy/docker-compose/),
under any filename (stack.yml), and keeps the file the single source of truth: its services are
re-read on every deploy, and build: contexts resolve relative to it, exactly as compose does.
{ "composePath": "deploy/docker-compose/docker-compose.yml" }{
"services": [
{
"name": "web",
"build": ".",
"buildArgs": { "NEXT_PUBLIC_API_URL": null },
"ports": ["3000"],
"exposed": true,
"domain": "app.acme.com"
},
{
"name": "db",
"image": "postgres:17",
"volumes": ["pgdata:/var/lib/postgresql/data"],
"env": { "POSTGRES_PASSWORD": { "value": "…", "secret": true } },
"restart": "unless-stopped"
}
]
}Prop
Type
dependsOn takes service names such as ["db"], without a port. It controls container startup
order, not database availability during an image build. For ENOTFOUND db during next build,
see Database access during a Next.js build.
Top-level env is available to Dockerfile builds as well as running containers. Use
buildArgs when a service needs an explicit override or when you want the config to document
which Dockerfile arguments it consumes. A service's own env is runtime-only and is never shared
with sibling builds. Keep runtime-only credentials in service env where possible: Docker build
arguments are not a secret-storage mechanism and can be retained in image metadata or build cache.
A compose file's own limits are honored too — GramCode reads mem_limit / cpus and
deploy.resources.limits.{memory,cpus} off each service and applies them over the project-wide
setting.
Monorepo
monorepo overrides GramCode's detected sub-apps. Entries in apps[] are matched to detected
sub-apps by rootDirectory and override their build settings — they don't declare apps from
scratch (the detector finds the apps).
Each override must have a different normalized rootDirectory. Declaring a web
process and a worker at the same root is not a supported multi-app declaration.
openship config validate rejects duplicate roots; source scans report unmatched
apps and workspace overrides in the configuration diagnostics.
Create a monorepo through the repository import flow, which supplies the detected
app metadata. The API/SDK also accepts that metadata as monorepoApps. Setting
projectType: "monorepo" (or CLI project create --type monorepo) without it now
fails before creating a project. A local CLI import scans the workspace first.
For independent processes without containers, use separate projects pointed at
the required roots and start commands.
{
"monorepo": {
"workspace": { "packageManager": "pnpm", "prepareCommand": "pnpm install && pnpm codegen" },
"apps": [
{ "name": "web", "rootDirectory": "apps/web", "framework": "nextjs", "port": 3000 },
{ "name": "api", "rootDirectory": "apps/api", "framework": "hono", "port": 8080 }
]
}
}Prop
Type
Not supported yet
sleepMode, monorepo sharedPaths, and per-app domain/env/exposed are accepted by the
validator but not applied — leave them out. Set sleep mode and per-app domains in the
dashboard for now.
How it maps
openship.json | Deploy wizard / API |
|---|---|
framework, packageManager, *Command, outputDirectory, buildImage, productionPaths, rootDirectory | Build settings |
port, productionMode, runtime | Runtime settings (hasServer, runtime isolation) |
releaseCommands | Build settings → Before activation |
volumes | Configuration → Persistent storage |
env | Environment variables (seeded as editable rows) |
domains | Public endpoints / custom domains |
routes | Reverse-proxy routing (routingConfig) |
resources | Optional container resource limits |
services | Compose services |
monorepo | Monorepo sub-apps + workspace |
See also
MCP
Let AI clients like Claude drive your GramCode instance as tools, over the Model Context Protocol.
App catalog JSON
The complete field reference for an GramCode App definition — the JSON that turns a set of containers into a one-click install, with generated secrets, a curated settings form, and a connection card.