Browse documentation

A project is a directory under PROJECTS_DIR (/srv/projects in prod) plus a Docker Compose stack. Everything the Console does to a project is a compose action against that directory.

/srv/projects/<name>/
  .env.prod       generated, mode 0600 — secrets live here
  manifest.json   the same values with every secret stripped

Naming

Project names must match SLUG_RE — ^[a-z0-9][a-z0-9-]*$ (apps/server/constants/regex.constants.ts). The name becomes the directory, the compose project name, the container prefix and the hostname label, so it is validated on every operation, not just create.

Create

Orchestrator.create() (console/services/orchestrator.service.ts):

  1. buildManifest() validates the name, resolves the config, and generates secrets.
  2. Write .env.prod with mode 0600, then chmod it again.
  3. Write manifest.json with jwtSecret, postgresPassword, configEncryptionKey, platformServiceToken and minioRootPassword set to undefined — the manifest is the readable copy, .env.prod is the secret one.
  4. compose up.

Generated secrets

SecretSizePurpose
jwtSecret32 bytes hexProject JWT signing
postgresPassword24 bytes hexProject database
configEncryptionKey32 bytes, base64Encrypts secret settings
platformServiceToken32 bytes hexThe Console proxy's X-Platform-Token
minioRootPassword24 bytes hexObject storage root (minioRootUser is admin)

Derived hostnames

Given name and either an explicit domain or the platform domainSuffix:

ConfigDefault
domain<name>.<domainSuffix>
storageDomain<name>-storage.<base suffix>
minioConsoleDomain<name>-minio.<base suffix>
corsOrigins<scheme>://<domain> (plus anything you pass)
apiImagemotherbase-api:1.0.0 unless overridden
certResolverletsencrypt unless overridden

Hostnames are validated per label (^(?!-)[a-z0-9-]{1,63}(?<!-)$, max 253 chars, no line breaks) and lowercased. corsOrigins rejects line breaks too — both values are written into an env file, and a newline there would inject an unrelated variable.

Resource limits

DEFAULT_LIMITS is the "Small" tier every project inherits:

FieldEnv varDefault
apiCpusAPI_CPUS0.5
apiMemMbAPI_MEM512M
dbCpusDB_CPUS0.5
dbMemMbDB_MEM512M
minioCpusMINIO_CPUS0.25
minioMemMbMINIO_MEM256M

They are enforced by compose deploy.resources.limits and read back for usage-versus-quota metering. Projects created before these vars existed parse back to the defaults instead of failing.

Note that generated projects get CONSOLE_SQL_ENABLED=true: the SQL editor is on, and may write, out of the box. Restrict it to reads per project under Database → Settings → SQL editor.

Actions

ActionComposeNotes
startstart
stopstop
restartrestart
updatepull then upPer project — you choose when each one moves
rebuildrebuild
destroydown, then rm -rf the project directoryRemoves the project's data
logsForlogsNot audited as a mutation
shellexec <service> shOnly api, db, minio, minio-init
Warning

destroy runs compose down and then deletes the whole project directory, including .env.prod and its secrets. There is no undo — take a backup first (see backups).

Every action for a given project is serialized through a per-project queue (Orchestrator.serialize). Two overlapping update clicks cannot interleave compose invocations on the same stack; a failed operation does not block the next one.

Config and limits edits

readConfig / writeConfig and readLimits / writeLimits edit .env.prod in place: the file is parsed, only the known keys are rewritten (missing keys are appended), and everything else is preserved byte for byte. Patches are validated with projectConfigPatchSchema before anything is written.

PROJECT_CONFIG_ENV is the single mapping used by render, parse and rewrite:

domain            → PROJECT_DOMAIN
storageDomain     → STORAGE_DOMAIN
minioConsoleDomain→ MINIO_CONSOLE_DOMAIN
corsOrigins       → CORS_ORIGINS
apiImage          → API_IMAGE
certResolver      → CERT_RESOLVER

The registry

console/services/registry.service.ts derives the project list from the filesystem plus Docker, not from a database:

interface ProjectSummary {
  name; domain; storageDomain; createdAt
  status: 'running' | 'stopped'
  apiUrl?; studioUrl?; storageUrl?
}
  • Directories under PROJECTS_DIR containing a readable manifest.json are projects; anything else is skipped silently.
  • status is running when docker ps shows a container named <name>-api….
  • studioUrl falls back to consoleUrl for manifests written before the rename.
  • platformToken(name) reads PLATFORM_SERVICE_TOKEN straight out of .env.prod — this is what the proxy presents to the project API.
  • apiTarget(name) / apiTargets(name) resolve the container's address on the Traefik network via docker inspect.

If Docker is unavailable the list comes back empty rather than throwing, which is why the Console still boots on a host without a running daemon.

Project defaults

console/services/project-defaults.service.ts stores the values pre-filled for the next project you create, persisted alongside the Console settings under PROJECTS_DIR.