# Install Falak (production)

> Install the Falak control plane on Ubuntu or Debian with one command — every installer flag and variable, the files it creates, and internal TLS testing.

Source: https://falak.sh/docs/operations/install/

Falak runs as a Docker Compose stack on a single Linux host. The installer sets up Docker, writes a generated configuration to `/opt/falak/.env`, starts the stack and creates the first administrator. Day-2 operations use [`falak-ctl`](/docs/operations/falak-ctl/).

```text title="What you are installing"
internet ──:80/:443──► edge (Caddy)
                        ├─ falak.example.com          Let's Encrypt ─► control-plane (FrankenPHP) · reverb (websockets)
                        ├─ agents.falak.example.com   Fleet-CA cert + client-cert (mTLS) check ─► agent-api
                        ├─ registry.falak.example.com Let's Encrypt + basic auth ─► registry (built-in image registry)
                        └─ grafana.falak.example.com  Let's Encrypt ─► grafana            (optional)
control-plane · agent-api · horizon · reverb · scheduler ─► postgres 17 · valkey
builder (falak-builder serve: PHP/Composer, Node, Bun) ─► edge
```

## Before you begin

- A host that meets the [requirements](/docs/getting-started/requirements/#control-plane-host): Ubuntu 22.04/24.04 or Debian 12, amd64 or arm64, 4 GB RAM (8 GB with observability), ports 80 and 443 free.
- [DNS records](/docs/operations/dns/) for `falak.example.com`, `agents.falak.example.com` and `registry.falak.example.com` (plus `grafana.` with observability) pointing at the host.

## Install

```bash
curl -fsSL https://falak.sh/install.sh \
  | sudo bash -s -- --domain falak.example.com --email you@example.com
```

Pin a release by using that release's copy of the script:

```bash
curl -fsSL https://github.com/OthmanHaba/falak/releases/download/v0.2.6/install.sh \
  | sudo bash -s -- --domain falak.example.com --email you@example.com
```

## What the installer does

1. **Preflight**: checks root, OS, CPU architecture, RAM, disk, that ports 80/443 are free, and that DNS for the panel and `agents.` (and `grafana.` with `--observability`) points at this host's public IP. It prints missing records.
2. Installs **Docker Engine** and the Compose plugin from Docker's official apt repository if missing.
3. Downloads `falak-deploy.tar.gz`, verifies it against `SHA256SUMS`, unpacks it to `/opt/falak/{deploy,observability}`, and installs `falak-ctl` to `/usr/local/bin`.
4. Generates `/opt/falak/.env` (mode 600): `APP_KEY`, database and Valkey passwords, Reverb keys, the builder token, the OTLP token, the registry credentials and all `FALAK_*` URLs.
5. Pulls `ghcr.io/<owner>/falak-{control-plane,builder,edge}:<version>`, starts the stack and waits until every service is healthy (up to 10 minutes). Migrations run in the `control-plane` service on start.
6. With `--observability`, creates a Grafana service account token for Falak.
7. Creates the first administrator and **prints the password once**.

## Options

Every option can also be set with the environment variable in the second column.

| Option | Env | Default | Meaning |
|---|---|---|---|
| `--domain NAME` | `FALAK_DOMAIN` | required | Panel domain; agents use `agents.NAME` |
| `--email ADDRESS` | `FALAK_EMAIL` | required | Let's Encrypt account and first admin e-mail |
| `--admin-email ADDRESS` | `FALAK_ADMIN_EMAIL` | `--email` | First admin, if different |
| `--version TAG` | `FALAK_VERSION` | latest release | Release to install |
| `--observability` | `FALAK_OBSERVABILITY=1` | off | Also run Grafana, Loki, Tempo, VictoriaMetrics (`grafana.NAME`) |
| `--no-observability` | `FALAK_OBSERVABILITY=0` | | Turn it off on a re-run |
| `--tls acme\|internal` | `FALAK_TLS` | `acme` | `internal` = Caddy's local CA, **testing only** |
| `--repo OWNER/NAME` | `FALAK_REPO` | `OthmanHaba/falak` | GitHub repository of the release (forks) |
| `--image-prefix PREFIX` | `FALAK_IMAGE_PREFIX` | `ghcr.io/<owner>` | Image registry prefix |
| `--build-from-source` | `FALAK_BUILD_FROM_SOURCE=1` | off | Build images locally instead of pulling them |
| `--ref REF` | `FALAK_REF` | `--version` or `main` | Git ref for `--build-from-source` |
| `--source-dir PATH` | `FALAK_DEPLOY_SOURCE` | | Use deploy files from a local checkout (offline/testing) |
| `--http-port N` / `--https-port N` | `FALAK_HTTP_PORT` / `FALAK_HTTPS_PORT` | `80` / `443` | Other ports only with `--tls internal` (ACME needs 80/443) |
| `--dir PATH` | `FALAK_DIR` | `/opt/falak` | Install directory |
| `--skip-dns-check` | `FALAK_SKIP_DNS_CHECK=1` | off | Do not require DNS to point at this host |
| `--force` | | off | Continue on an unsupported OS or low resources |
| `-h`, `--help` | | | Print usage |

## Re-running

Running the installer again is safe: secrets in `.env` are kept, settings from the flags are updated, the stack is converged, services whose mounted config files changed are recreated, and the admin is not created twice. Use it to enable observability later:

```bash
curl -fsSL https://falak.sh/install.sh \
  | sudo bash -s -- --domain falak.example.com --email you@example.com --observability
```

## Files

```text
/opt/falak/.env            settings + secrets (install.sh; never commit or share)
/opt/falak/custom.env      optional extra app env (GITHUB_APP_*, mirrors, FALAK_* tuning) loaded by the app containers
/opt/falak/deploy/         compose.yml, falak-ctl, image support files (replaced on update; previous kept as deploy.prev)
/opt/falak/observability/  Loki/Tempo/Grafana/gateway configs
/opt/falak/backups/        falak-ctl backup output
/usr/local/bin/falak-ctl   the operations tool
```

See [Configuration](/docs/operations/configuration/) for what goes in `.env` versus `custom.env`.

## After installing

1. Log in at `https://falak.example.com` with the printed password.
2. Configure [mail](/docs/operations/configuration/#mail) so invitations and e-mail alerts work.
3. On a panel reachable from the internet, [restrict sign-up](/docs/operations/configuration/#who-can-sign-up) with `FALAK_REGISTRATION=invite` or `closed` (anyone can create an account by default).
4. Schedule [backups](/docs/operations/backup-restore/) and copy them off the host.
5. Read [Security hardening](/docs/operations/security/).
6. [Connect servers](/docs/servers/connect-custom-server/). Agent binaries come from your panel (`/install/agent/linux-{amd64,arm64}`), not from GitHub.

## The built-in image registry

The stack runs an image registry (Docker Distribution) that the edge serves at `https://registry.<domain>` with a Let's Encrypt certificate and basic auth. Docker-mode builds (Dockerfile sites, Compose services with `build:`) push there, and servers pull from it, pinned by digest. Only the edge can reach the registry container; images live in the `registry-data` volume.

- The installer generates the settings into `.env`: `FALAK_REGISTRY_HOST`, `FALAK_REGISTRY_URL`, `FALAK_REGISTRY_USERNAME` (`falak`) and `FALAK_REGISTRY_PASSWORD`. Falak hands the credentials to builders and servers itself.
- Create the `registry.<domain>` [DNS record](/docs/operations/dns/) like the others. `falak-ctl registry status` shows its address and size and checks that it answers with its credentials; `falak-ctl doctor` also checks that it refuses requests without them.
- **Builder servers and external builders** push to `https://registry.<domain>` over the internet, so they need to resolve and reach it. With `--tls internal`, Docker on other hosts doesn't trust the registry's certificate: Docker builds need `--tls acme` (or Caddy's internal root trusted in each Docker daemon).
- **Installs from before the registry** get `FALAK_REGISTRY_*` added to `.env` by the next `falak-ctl up` or `falak-ctl update` (after an update run by an older falak-ctl, run `falak-ctl up` once). Then add the `registry.` DNS record and check it with `falak-ctl registry status`.

### Registry storage

Every Docker build pushes an image. Two jobs keep the registry from growing forever:

- **Daily cleanup of old images** (`falak:registry-prune` in the control plane, 03:45, after the artifacts prune): deletes the images of builds whose artifact was pruned (each site keeps its newest `FALAK_ARTIFACTS_KEEP` builds, default 10). An image stays while a release may still run it (pending, live or kept for rollback), while its build is running or less than a day old; tags that aren't build ids are never touched. Images of a deleted site's builds go `FALAK_REGISTRY_DELETED_SITE_GRACE_DAYS` (default 7) days after the build. Preview with `falak-ctl registry prune --dry-run`, or run it now with `falak-ctl registry prune`.
- **Weekly garbage collection** (`/etc/cron.d/falak-registry-gc`, Sunday 04:17, written by `falak-ctl up` and `update`): `falak-ctl registry gc` deletes the layers nothing references any more, which is what frees disk space. The registry is stopped while it runs (a push during garbage collection could lose layers), so it runs at night and is skipped (logged, tried again the next week) while an image build is queued or running, or when the control plane can't tell. `--force` runs it anyway. Output goes to `/var/log/falak-registry-gc.log`. Set `FALAK_REGISTRY_GC=0` in `.env` and run `falak-ctl up` to remove the cron entry.

## Testing locally with internal TLS

`--tls internal` issues certificates from Caddy's local CA, skips the DNS check and allows custom ports:

```bash
sudo bash install.sh --domain falak.test --email you@example.com --tls internal --http-port 8080 --https-port 9443
curl -k --resolve falak.test:9443:127.0.0.1 https://falak.test:9443/up
```

Do not use internal TLS in production: server install scripts and agents would not trust the panel's certificate, and Docker on other hosts would not trust the registry's.

## Build images yourself

```bash
docker build -f control-plane/Dockerfile --build-arg FALAK_VERSION=dev -t falak-local/falak-control-plane:dev .
docker build -f deploy/builder.Dockerfile --build-arg FALAK_VERSION=dev -t falak-local/falak-builder:dev .
docker build --build-arg FALAK_VERSION=dev -t falak-local/falak-edge:dev deploy/edge
```

Or let the installer do it with `--build-from-source [--ref REF]`.

## Troubleshooting

| Symptom | Fix |
|---|---|
| `DNS does not point at this host` | Create the printed A/AAAA records and wait (`dig +short falak.example.com`). With Cloudflare, use DNS only. |
| `port 80 is in use` | Stop the other web server: `systemctl disable --now nginx apache2 caddy`. |
| `pulling images … failed` | The release is not published, or GHCR packages are private (`docker login ghcr.io`). |
| `the stack did not become healthy` | The installer prints the last logs; then `falak-ctl doctor`, `falak-ctl logs control-plane`. |
| `FALAK_EDGE_SUBNET … overlaps` | Pick another private /24 in `.env` (`FALAK_EDGE_SUBNET=…`) and re-run. The app trusts proxy headers only from that subnet. |
| Docker build fails at push (`lookup registry.falak.local … no such host`, `401`, `x509`) | The install has no built-in registry yet, or its DNS record is missing: run `falak-ctl up` (adds `FALAK_REGISTRY_*`), create the `registry.<domain>` record, then `falak-ctl registry status`. `x509` with `--tls internal`: see [the built-in image registry](#the-built-in-image-registry). |
| Browser shows a certificate error | `falak-ctl logs edge` for ACME errors. Ports 80/443 must be reachable from the internet. Let's Encrypt rate limits apply to repeated reinstalls. |

## Next steps
