Directus
Instant REST and GraphQL API over your own database
Deploy NowREADME
Instant REST and GraphQL API over your own database, with an admin app on top.
Overview
Directus wraps a SQL database in a REST and GraphQL API and an admin app, without owning the schema. It introspects whatever tables are there, so the database stays a plain database: you can keep reading and writing it with psql, an ORM or a BI tool while Directus serves the same rows over HTTP. On top of that it adds roles and granular permissions, file storage with on-the-fly image transforms, revisions and an activity log, and Flows, a visual automation builder with webhook, event and schedule triggers.
This template runs upstream's official image unmodified. There is no overlay Dockerfile here and no code in this repository between you and the app: the container's own entrypoint installs the schema, runs migrations, seeds the first administrator and then supervises the server with pm2, exactly as it does anywhere else.
What you get by hosting it
- A REST and GraphQL API over your data, generated from the schema rather than written by hand.
- An admin app for modelling collections and fields, and for editing content in them.
- Users, roles and policies, with per-collection and per-field permissions and row filters.
- File storage with image transforms, plus revisions and an activity log over every change.
- Flows: webhook, event, manual and schedule-triggered automations, built in the UI.
What you need before deploying
- An email address and a password for the first administrator (
ADMIN_EMAILandADMIN_PASSWORD). The email is the login, so it has to be a real address shape. - Nothing else. Collections, users and flows are all created in the app after it starts.
Configuration
| Variable | Required | Description |
|---|---|---|
ADMIN_EMAIL | yes | Email address for the first administrator, created on first boot. It is the login, and must be a valid address shape; it only has to receive mail if you configure SMTP. |
ADMIN_PASSWORD | yes | Password for that administrator. Directus enforces no password policy by default. It is not generated here on purpose: a template variable is stored write-only, so a generated password could never be read back. |
PROJECT_NAME | no | Name shown on the login page and in the admin app's header. Applied on first boot only; afterwards it is Settings > Project Settings. |
PROJECT_OWNER | no | Email of the person responsible for this deployment. Directus asks for one in a dialog on first sign-in, to record licence acceptance; setting it here answers that in advance. Applied on first boot only, and ignored unless it parses as an email. |
AUTH_PASSWORD_POLICY | no | Regex every password must match. Unset means no policy, which is upstream's default. |
CORS_ENABLED | no | Set to true to let browsers on other origins call the API. Off by default; the admin app is same-origin. |
CORS_ORIGIN | no | Which origins CORS allows when CORS_ENABLED is true. An origin, a comma-separated list, or true to echo the request's own origin. |
WEBSOCKETS_ENABLED | no | Set to true to serve the realtime API at /websocket. Off by default. |
EMAIL_FROM | no | Address outbound mail is sent from. Needed for password resets, user invites and the Send Email flow operation. |
EMAIL_TRANSPORT | no | sendmail (upstream's default, and this image carries no MTA), smtp, mailgun, sendgrid or ses. |
EMAIL_SMTP_HOST, EMAIL_SMTP_PORT, EMAIL_SMTP_USER, EMAIL_SMTP_PASSWORD | no | The SMTP server, when EMAIL_TRANSPORT is smtp. |
SECRET is generated by the platform and never asked for. It signs access tokens, session cookies
and password-reset tokens; Directus warns and falls back to a fresh random value on every boot
when it is unset, which would log every user out on every restart.
What the template fixes
The database is SQLite on the service's volume, which is the image's own default client with
the file moved: DB_CLIENT=sqlite3 and DB_FILENAME=/data/database.sqlite. The file sits at the
volume root rather than in a subdirectory because nothing in Directus creates one: upstream's
Dockerfile pre-creates /directus/database at build time, and a fresh volume mounted over a
different path would arrive without it. Uploads go to /data/uploads and Marketplace extensions
to /data/extensions, both on the same volume.
PUBLIC_URL is set to the service's own URL. It is what the app puts in password-reset and
email-verification links, in SSO redirect targets and in the MCP resource URLs.
The service listens on 8055 and is health-checked on /server/ping, which answers 200 pong
before the authenticate middleware runs. Neither of the two likelier paths works: / is a 302 to
/admin, and /server/health, the app's own readiness probe, throws ForbiddenError for an
unauthenticated caller in Directus 12 and answers 403. /server/ping does not touch the database,
which it does not need to here: the entrypoint runs directus bootstrap to completion and only
hands off to pm2 if it exited 0, so the server binds at all only after the schema is installed and
migrated.
The first boot logs Upload directory (/data/uploads) is not read/writeable! and
Extensions directory (/data/extensions) is not readable!. Both are a boot-time access() on a
directory that does not exist yet rather than a permission problem: the same check passes for the
SQLite file's directory, which is the same volume. Each directory is created on first use, and the
warning stops from the next boot on.
It is declared alwaysOn: true. Flows with a schedule trigger fire from inside the process, and
the app runs its own timed jobs besides, so a machine that had scaled to zero would never wake to
run one. That bills continuously.
Licensing, and what the unlicensed tier allows
Directus 12 is source-available under the Monospace Sustainable Core License 1.0, not an OSS
licence, and it meters itself. With no licence key entered, the built-in Core entitlements
(read off @directus/license 0.6.1, which the image ships) are:
| Entitlement | Core |
|---|---|
| Users (seats) | 3 |
| Collections | 25 |
| Flows | 5 |
| Activity and revision history | 30 days |
| SSO | off |
| Telemetry | required, and TELEMETRY=false is ignored while it is |
A licence key is entered in the app after deploy, not through a template variable. Read the licence in the upstream repository before using this commercially: it grants internal use, non-commercial education and research, and hosting on a licensee's behalf, but not a Competing Use.
After deploy
- Open the service URL and sign in with the
ADMIN_EMAILandADMIN_PASSWORDyou set. The first sign-in raises two dialogs before you reach anything: one asking for a licence key, where I'm using Core plan or Skip is the free path, and one asking for a project owner email and licence acceptance, which has a Remind Later. SettingPROJECT_OWNERanswers the second in advance. - Settings > Data Model > Create Collection to make your first table. Add fields to it, then switch to the Content module to add rows.
- The same rows are already an API.
GET <service-url>/items/<collection>serves them, andPOST <service-url>/graphqlserves the GraphQL equivalent. Both need a token. - To get a token: User Directory > your user > Token, generate one, and send it as
Authorization: Bearer <token>. Or exchange your login for one atPOST /auth/loginwith{"email":..., "password":...}. - Settings > Access Policies is where you open a collection up to the Public role, if you want unauthenticated reads.
Links
- Architectures:
linux/amd64andlinux/arm64. Upstream's12.5.0index carries both; nothing is rebuilt here. - Upstream: https://github.com/directus/directus
- Documentation: https://directus.com/docs/
- Image:
docker.io/directus/directus, pinned to12.5.0 - License: Monospace Sustainable Core License 1.0 (
licensein the upstream repository root, self-abbreviated MSCL-1.0-GPL). Source-available, not OSS.
Services & Specs
- Image
- docker.io/directus/directus:12.5.0
- Port
- 8055
- Healthcheck
- /server/ping
Variables
You supply 2 variables before the first deploy.
Required
ADMIN_EMAILEmail address for the first administrator, created on first boot. It is the login, so it must be a real address shape; it does not have to receive mail unless you configure SMTP
ADMIN_PASSWORDPassword for that administrator. Directus enforces no policy by default, so this is the only thing between the public URL and your data: pick a strong one
Optional (12)
PROJECT_NAMEName shown on the login page and in the admin app's header. Applied on first boot only; afterwards it is Settings > Project Settings in the app
PROJECT_OWNEREmail of the person responsible for this deployment. Directus 12 asks for one in a dialog on first sign-in, to record licence acceptance; setting it here answers that in advance. Applied on first boot only, and ignored unless it parses as an email
AUTH_PASSWORD_POLICYRegex every password must match, e.g. /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/. Unset means no policy, which is upstream's default
CORS_ENABLEDSet to true to let browsers on other origins call the API. Off by default; the admin app is same-origin and does not need it
CORS_ORIGINWhich origins CORS allows when CORS_ENABLED is true. An origin, a comma-separated list, or true to echo the request's own origin
WEBSOCKETS_ENABLEDSet to true to serve the realtime API at /websocket, for subscribing to item changes. Off by default
EMAIL_FROMAddress outbound mail is sent from. Needed for password resets, user invites and the Send Email flow operation
EMAIL_TRANSPORTHow mail is sent: sendmail (the default, which has no MTA in this image), smtp, mailgun, sendgrid or ses. Set smtp and fill the four EMAIL_SMTP_ variables below
EMAIL_SMTP_HOSTSMTP server hostname, when EMAIL_TRANSPORT is smtp
EMAIL_SMTP_PORTSMTP server port, when EMAIL_TRANSPORT is smtp. Commonly 587
EMAIL_SMTP_USERSMTP username, when EMAIL_TRANSPORT is smtp
EMAIL_SMTP_PASSWORDSMTP password, when EMAIL_TRANSPORT is smtp