hangar.yaml deploy manifest

Describe the services, databases, volumes and variables a Deploy on Hangar link creates, in a hangar.yaml file at the root of your repository.

Updated

A hangar.yaml file tells a Deploy on Hangar button what to create: the services, the managed databases they use, a volume for each service that needs one, and the variables that wire them together. The person deploying sees the whole plan before anything is created, and answers the questions the manifest asks.

Without a manifest, the button creates one public service from the repository. With one, a single click can bring up an app, its database, a background worker and their secrets.

Where it goes

Put hangar.yaml at the root of the repository, or in the folder the link names with dir. Hangar reads it from public repositories on GitHub, GitLab and Bitbucket. On other hosts the button still works and creates one service.

The manifest applies only when the project is created. After that, every service is configured from the dashboard like any other, and editing the file changes nothing in projects that already exist. If the file changes between the moment the deploy page opens and the click on deploy, the page asks to review it again.

A manifest that does not pass the checks below stops the deploy, and the deploy page lists what is wrong.

Editor support

The format is published as a JSON Schema at https://tryhangar.io/schema/hangar.v1.json. Add this line at the top of the file and editors with YAML support, such as VS Code with the YAML extension or JetBrains IDEs, complete the keys and flag mistakes as you type:

# yaml-language-server: $schema=https://tryhangar.io/schema/hangar.v1.json

The schema checks each field. Whether a reference such as ${{ db.url }} names something that exists is checked on deploy.

A first manifest

An app with a PostgreSQL database:

version: 1
services:
  web:
    env:
      DATABASE_URL: ${{ db.url }}
databases:
  db:
    kind: postgres

Top level

KeyRequiredMeaning
versionYesThe format version. Always 1
nameNoA name shown on the deploy page, up to 60 characters
descriptionNoA sentence shown on the deploy page, up to 300 characters
servicesYesThe applications to create, by name. At least one, at most five
databasesNoThe managed databases to create, by name. At most five

Service and database names use lowercase letters, digits and -, start with a letter and have at most 30 characters. A name cannot be both a service and a database.

Services

Every service is built from the same repository, branch and folder as the link. An empty service, web: {}, deploys the repository with the automatic builder on a public address.

KeyMeaning
builderHow the image is built: railpack (the automatic builder, the default), dockerfile, nixpacks, heroku_buildpacks, paketo_buildpacks or static. See Builds
dockerfileThe Dockerfile's path, relative to the app's folder. Implies the dockerfile builder
installCommandReplaces the detected install step. Only with railpack or nixpacks
buildCommandReplaces the detected build step. Only with railpack or nixpacks
preDeployThe pre-deploy command, run in the new image before each deploy goes live. The place for migrations against a managed database
startThe start command. It runs with sh -c, so &&, pipes and $VARIABLES work
portThe port the app listens on. Defaults to 3000. Hangar sets PORT to it, so an app that reads PORT needs no port
publicWhether the service gets an address with HTTPS. Defaults to true. Set false for workers
volume.pathMounts a new volume at this absolute path, such as /data
envThe service's variables, described below

The volume is named after the service, such as web-data, and starts at the size your plan gives each volume.

Pre-deploy and volumes

The pre-deploy command runs as its own short-lived container, without the service's volume. Anything it writes to the volume's path is lost. That suits migrations for PostgreSQL or MySQL, which live in another service, but not for SQLite: put SQLite migrations in start, before the server, as in the full example below.

Databases

databases:
  db:
    kind: postgres
  cache:
    kind: redis

kind is one of postgres, mysql, mariadb, mongo or redis. Each database is created in the same region as the services, with its own generated password, and is reachable only over the private network. See Databases.

Variables

Each key under env becomes a variable of that service. The value takes one of four forms.

Literal values

env:
  NODE_ENV: production
  WORKERS: 4
  DEBUG: false

Numbers and booleans are written as text, so WORKERS becomes 4 and DEBUG becomes false.

References

${{ name.field }} inserts a value from another service or database of the same manifest. It can sit inside a longer string.

env:
  DATABASE_URL: ${{ db.url }}
  REDIS_URL: ${{ cache.url }}
  PUBLIC_URL: ${{ web.url }}
  ALLOWED_HOSTS: ${{ web.host }},localhost
SourceFields
A serviceurl, its https:// address, and host, the hostname alone. Only for public services
A databaseurl, the internal connection URL, plus host, port, user, password and database

References are filled in when the project is created and saved as plain values. Changing the database password later does not update them. To keep a value in one place across services, use environment variables from the dashboard.

Generated values

env:
  SECRET_KEY:
    generate: secret
  ADMIN_PASSWORD:
    generate: password
    length: 24
  INSTANCE_ID:
    generate: uuid
generateProduces
secretHexadecimal characters, 64 by default
passwordLetters and digits, 32 by default
uuidA random UUID

length sets the size of a secret or password, from 16 to 128. The value is made once, when the project is created, and contains no character that needs quoting in a URL or a shell.

Prompts

A prompt asks the person deploying for a value on the deploy page:

env:
  ADMIN_EMAIL:
    prompt: Admin email
    description: The first account, created on the first start.
  STRIPE_SECRET_KEY:
    prompt: Stripe secret key
    secret: true
    required: false
  PLAN:
    prompt: Plan name
    default: free
KeyMeaning
promptThe field's label, up to 80 characters
descriptionA hint under the field, up to 300 characters
defaultThe value the field starts with
requiredWhether the field can be left empty. Defaults to true when there is no default
secretHides what is typed

Full example: SQLite on a volume

A Node.js app that keeps its SQLite database on a volume at /data. The migration runs in start, where the volume is mounted, and then the server starts.

# yaml-language-server: $schema=https://tryhangar.io/schema/hangar.v1.json
version: 1
name: Notes
description: A small notes app on SQLite, with its data on a persistent volume.
services:
  web:
    start: npx prisma migrate deploy && node server.js
    volume:
      path: /data
    env:
      DATABASE_URL: file:/data/notes.db
      SESSION_SECRET:
        generate: secret
      APP_URL: ${{ web.url }}
      ADMIN_EMAIL:
        prompt: Admin email
        description: Signs in as the first user.

Keep a service that writes SQLite at one replica: replicas do not coordinate their writes to a volume.

Example: app, worker and two databases

version: 1
name: Shop
services:
  web:
    preDeploy: python manage.py migrate
    start: gunicorn shop.wsgi --bind 0.0.0.0:$PORT
    port: 8000
    env:
      DATABASE_URL: ${{ db.url }}
      REDIS_URL: ${{ cache.url }}
      SECRET_KEY:
        generate: secret
      CSRF_TRUSTED_ORIGINS: ${{ web.url }}
  worker:
    public: false
    start: celery -A shop worker --loglevel=info
    env:
      DATABASE_URL: ${{ db.url }}
      REDIS_URL: ${{ cache.url }}
databases:
  db:
    kind: postgres
  cache:
    kind: redis

Each service gets its own generated values: if two services need the same secret, generate it in one and set it on the other from the dashboard, or use an environment variable.

Limits

  • At most five services and five databases.
  • One volume per service.
  • The file can be up to 64 KB.
  • Keys outside this page are refused, so a typo fails the deploy instead of being ignored.

Frequently asked questions

Does Hangar read hangar.yaml on every deploy?
No. The manifest is read once, when someone creates a project from a Deploy on Hangar link. Later deploys use the project's settings, and changing the file does not change projects that already exist.
How do I connect a service to a database in hangar.yaml?
Declare the database under databases and reference its connection URL in a variable, such as DATABASE_URL set to ${{ db.url }}. Hangar fills in the internal URL when it creates the project.
Where should SQLite migrations run on Hangar?
In the start command, before the server. The pre-deploy command runs without the service's volume, so it would migrate a database file that is thrown away.

Deploy your app on Hangar

Connect a repository and it is live in minutes.

Start deploying