Architecture & Core Concepts
fjord is designed around a single architectural philosophy: the filesystem is the source of truth. Rather than encapsulating container definitions in an internal database or proprietary configuration format, fjord manages native FreeBSD jails by reading and writing standard compose specifications and environment files directly on disk.
/var/db/fjord/stacks/<stack-id>/
├── compose.yaml # Podman engine specification
├── appjail-director.yml # AppJail director orchestration spec
├── Makejail # AppJail build recipe
├── .env # User configuration & environment variables
└── state.json # Stack metadata (engine binding, status, channel)
Because fjord maintains zero private state locks, administrators can inspect, version, or modify stack files directly with standard shell tools (vi, git, etc.). Running podman-compose or appjail-director inside a stack directory yields the exact same behavior as triggering actions through fjord's web interface.
1. Stacks
A stack represents a cohesive application deployment. A stack may encompass a single service (such as Radarr or Caddy) or an interconnected multi-container architecture (such as Immich with its PostgreSQL and Redis sidecars).
When a stack is deployed:
- Workspace Allocation: fjord creates an isolated directory in
/var/db/fjord/stacks/<id>. - Template Synthesis: The chosen catalog manifest generates the engine specification files (
compose.yamlorappjail-director.yml). - Environment Generation: Configured variables, port bindings, and storage mounts are written to
.env. - Pre-flight: The daemon creates missing data folders (owned by the app's PUID/PGID) and verifies host port availability.
- Engine Invocation: The target orchestrator launches the container workload natively on the FreeBSD kernel.
2. Container Engines
fjord is architecturally engine-agnostic, decoupling compose specifications from the underlying execution runtime. It currently supports two container backends on FreeBSD, allowing administrators to choose the runtime best suited for each workload:
| Characteristic | Podman Engine | AppJail Engine |
|---|---|---|
| Specification Format | compose.yaml |
appjail-director.yml + Makejail |
| CLI Orchestrator | podman-compose |
appjail-director |
| OCI Runtime | ocijail |
Native jail(8) via AppJail OCI |
| Networking Model | CNI bridge network (cni-rdr), host network |
Virtual bridge (appjail-nat), host network |
| Remote Volumes | Native NFS and SMB named volumes | Host-mounted paths |
| Jail Annotation Mapping | annotations: in compose.yaml |
Jail parameters in director spec |
Engine Coexistence
Configured engines can run concurrently on a single FreeBSD host. Engine selection is configured on a per-stack basis during initial deployment and preserved in state.json.
Jail Parameter Translation
FreeBSD container images often require specific kernel jail parameters (for example, .NET applications like Sonarr/Radarr require allow.mlock=true, while database servers may require allow.sysvipc=true). In the Podman engine, these requirements are declared as OCI annotations (org.freebsd.jail.*) in compose.yaml and translated by ocijail. In the AppJail engine they ride in the jail template of the dbuild-generated bundle that fjord materializes for director.
3. Catalogs & Manifests
The fjord App Store consumes static, decoupled catalogs. A catalog is simply an HTTP-accessible endpoint publishing three components:
catalog.json: Fleet-wide metadata, categories, architectures, and available app entries.manifests/<app>.yaml: Per-application deployment schemas, required vs. optional ports, environment parameters, and default values.icons/: SVG and PNG brand icons for user interface display.
Default Catalog & Custom Sources
fjord ships with catalog.daemonless.io enabled by default, which is continuously generated from FreeBSD-native images in the Daemonless fleet. Administrators can configure private or secondary catalog URLs in Settings; fjord merges all active catalogs into a unified store, tagging each application with its origin repository.
Release Channels
Catalog manifests define release channels corresponding to image tag families:
latest: Tracks official upstream application releases (default for most workloads).pkg: Tracks stable FreeBSD Quarterly binary packages.pkg-latest: Tracks bleeding-edge FreeBSD Latest binary packages.
CPU Architecture Filtering
Manifests specify supported host architectures (amd64, arm64). fjord knows the architecture it was built for (amd64 or arm64) and filters incompatible applications from view, whichever way round the mismatch is, preventing runtime architecture errors.
4. Storage Architecture
fjord enforces a strict architectural boundary between internal application state and external user data pools:
Storage Layout
├── App Data Root (/var/db/fjord/containers/ or ZFS dataset)
│ ├── radarr/
│ │ └── config/ <-- Application config, SQLite databases, logs
│ └── immich/
│ ├── postgres/ <-- Relational database files
│ └── machine-learning/
└── User Data Pools (ZFS pools / NFS / SMB)
├── /tank/media/movies/ <-- Persistent media library
├── /tank/media/tv/
└── /tank/downloads/
App Data
App Data encompasses the internal operational state of containers: configurations, databases, caches, and socket locks. Administrators configure one or more App Data roots in Settings (typically pointing to dedicated ZFS datasets such as zroot/data/fjord). fjord provisions application subdirectories automatically upon deployment.
Folder Sets
Folder Sets represent your actual user data assets (media libraries, photo collections, document repositories, backups). Rather than re-entering absolute paths or connection URIs for every stack, administrators declare reusable Folder Sets once in Settings. When launching or editing a stack, choosing a folder set automatically maps the corresponding paths into the container specification.
Folder Sets support:
- Local filesystem paths and ZFS datasets.
- NFS endpoints (nfs://server/export).
- SMB shares (smb://user@server/share, supported on Linux hosts; on FreeBSD, NFS or local mount points are recommended due to SMB kernel protocol compatibility).
5. Pre-flight and Readiness Checks
Two layers keep a stack from failing for host reasons:
Pre-flight runs before every start and install:
- Port collisions: every published port — and, for
network_mode: hostservices, every port the image exposes — is probed. A port held by another container is reported by that container's name; a port held by a host process is reported as in use. - Data folders: missing bind-mount sources are created and chowned to the stack's
PUID/PGID, so a sidecar never crashes on a root-owned cache directory.
If pre-flight fails, the stack configuration is saved to disk without launching, so you can change a port or free it and start again.
Readiness checks run on the System page (and in the first-run wizard) and cover the host itself: the libpod socket, catatonit/conmon/ocijail and their versions, pf anchors (cni-rdr/* for podman, appjail-nat/* for AppJail), the AppJail toolchain and the fjord data root — each with why it matters and a copy-paste fix.
6. Lifecycle
What fjord does today at each stage. The FJORD Specification describes the fuller contract this is growing toward; its status table says which parts exist.
- Install: resolves the catalog manifest, computes the App data folders under
<App data>/<stack>/…, runs pre-flight, provisions and chowns the folders, writes.envand the engine spec, then hands the stack to the engine. - Delete: stops the stack and removes it from fjord. Bind-mounted data always stays on disk; there is no purge option yet.
- Update: pulls the newest image for the stack's release channel and recreates the containers or jails. Values already in
.envare kept; a manifest that gained new variables is not diffed yet (see the roadmap).