Reference

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 schema

It'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:

StackTypical release commands
Laravelphp artisan migrate --force
Railsbundle exec rails db:prepare
Djangopython 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.jsonDeploy wizard / API
framework, packageManager, *Command, outputDirectory, buildImage, productionPaths, rootDirectoryBuild settings
port, productionMode, runtimeRuntime settings (hasServer, runtime isolation)
releaseCommandsBuild settings → Before activation
volumesConfiguration → Persistent storage
envEnvironment variables (seeded as editable rows)
domainsPublic endpoints / custom domains
routesReverse-proxy routing (routingConfig)
resourcesOptional container resource limits
servicesCompose services
monorepoMonorepo sub-apps + workspace

See also

On this page