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
| Key | Required | Meaning |
|---|---|---|
version | Yes | The format version. Always 1 |
name | No | A name shown on the deploy page, up to 60 characters |
description | No | A sentence shown on the deploy page, up to 300 characters |
services | Yes | The applications to create, by name. At least one, at most five |
databases | No | The 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.
| Key | Meaning |
|---|---|
builder | How the image is built: railpack (the automatic builder, the default), dockerfile, nixpacks, heroku_buildpacks, paketo_buildpacks or static. See Builds |
dockerfile | The Dockerfile's path, relative to the app's folder. Implies the dockerfile builder |
installCommand | Replaces the detected install step. Only with railpack or nixpacks |
buildCommand | Replaces the detected build step. Only with railpack or nixpacks |
preDeploy | The pre-deploy command, run in the new image before each deploy goes live. The place for migrations against a managed database |
start | The start command. It runs with sh -c, so &&, pipes and $VARIABLES work |
port | The port the app listens on. Defaults to 3000. Hangar sets PORT to it, so an app that reads PORT needs no port |
public | Whether the service gets an address with HTTPS. Defaults to true. Set false for workers |
volume.path | Mounts a new volume at this absolute path, such as /data |
env | The 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
| Source | Fields |
|---|---|
| A service | url, its https:// address, and host, the hostname alone. Only for public services |
| A database | url, 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
generate | Produces |
|---|---|
secret | Hexadecimal characters, 64 by default |
password | Letters and digits, 32 by default |
uuid | A 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
| Key | Meaning |
|---|---|
prompt | The field's label, up to 80 characters |
description | A hint under the field, up to 300 characters |
default | The value the field starts with |
required | Whether the field can be left empty. Defaults to true when there is no default |
secret | Hides 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.