# Loch.sh Docs > Documentation for the Loch.sh infrastructure This file contains all documentation content in a single document following the llmstxt.org standard. ## Audit trail The API records every action on the panel in `/audit.log`. Only admins can consult it, and nobody can change it from the panel. ### What is recorded | Kind | Examples | |---|---| | Authentication | Logins, logouts, failed logins, accounts created at first login | | Containers | Container and stack actions, shell, logs, file tail, port proxy, file copies | | Administration | Users, API keys, scheduled tasks, SFTP users, websites, managed services, private networks, functions | | Function invocations | `function invoked`: one entry per function and per minute, with the count, errors and users; never the event or the response | | Every write request | `api request`: method, path, status, user, API key id, client IP | | Every refused request | `access denied`: 403, or 401 with an API key | | Sensitive reads | Container inspection (shows environment variables), site settings, file downloads and tails | | The trail itself | Each consultation (`audit trail viewed`) | What is never written: - **No secrets**: no password, token, header or request body. - Text values are cut at 2 KB. - Anonymous refusals are grouped to one entry per IP and per minute; unknown routes are ignored. An anonymous client cannot flood the trail. Each line is a JSON object: `time`, `level`, `msg` (the event) and structured fields. ### Consult it #### In the interface 1. Open **Security → Audit trail**. 2. Filter by **Date range** (shortcuts: **Last hour**, **Last 24 hours**, **Last 7 days**, **Last 30 days**), **Events**, **Levels** and **User (email)**, or type in **Search all fields**. 3. Click **Load older entries** to page back. Consultations of the trail are hidden by default: turn on **Show consultations of the audit trail** to see them. #### Through the API `GET /audit`, admin only. An API key needs the `audit:read` scope. Entries come newest first, with a cursor for older pages. See the [API reference](../reference/api.md). ### Keep it tamper-proof - **Mount the log directory.** In the image, `/app/log` is a volume. Bind-mount it (`-v /srv/loch/log:/app/log`), or the trail is lost when the container is recreated. - **No write route**: the trail is read-only from the panel. - **The API container is protected**: shell, file copy, file tail and `exec` scheduled tasks are refused on it, since they would reach `audit.log`. - **Root on the host can still edit the file.** For stronger integrity, ship it to a remote syslog or set it append-only (`chattr +a`). - **Rotation**: the API does not rotate the file. Watch the disk space. ### Good to know - Behind a reverse proxy, set `TRUSTED_PROXIES`, or every entry shows the proxy IP. - A write that fails (disk full, file removed) is reported on the API's standard error. --- ## Configuration The API reads its configuration from environment variables (or a `.env` file in its working directory; the environment wins). This page lists the variables you set on most installations. Full list: [Configuration reference](../reference/configuration.md). ### Authentication | Variable | Description | |---|---| | `OIDC_PROVIDER` | `microsoft` (default), `keycloak` or `generic` | | `CLIENT_ID`, `CLIENT_SECRET` | OAuth 2.0 client registered in the provider | | `REDIRECT_URI` | Public URL of the API; the callback is `/openid-connect/redirect` | | `SCOPE` | OAuth scopes, for example `openid email profile` | | `TENANT_ID`, `APPLICATION_ID` | Microsoft Entra ID only | | `KEYCLOAK_URL`, `KEYCLOAK_REALM` | Keycloak only | | `OIDC_DISCOVERY_URL` | Generic provider only | ### Security and network | Variable | Description | |---|---| | `FRONT_URL` | URL of the web interface: the only origin allowed by CORS, the CSRF check and the WebSocket | | `COOKIE_SECRET` | Secret signing the session cookies, at least 32 characters | | `PASSWORD_SECRET` | Required at startup, not used by the current version | | `TRUSTED_PROXIES` | Subnet of your reverse proxy, allowed to set `X-Forwarded-For` | ### Storage | Variable | Default | Description | |---|---|---| | `DATA_DIR` | `./data` | Users, API keys, scheduled tasks (JSON files). `/app/data` in the image | | `API_LOG_DIR` | `./log` | Directory of `audit.log`. `/app/log` in the image | ### Optional modules Each module is off by default. Enable it, then set its variables. | Module | Switch | Guide | |---|---|---| | Websites | `HOSTING_ENABLED=true` (also needs `HOSTING_TRAEFIK_NETWORK`) | [Websites](./hosting.md) | | SFTP accounts | `SFTP_ENABLED=true` (also needs `SFTP_DIRECTORY`, `SFTP_API_URL`, `SFTP_API_KEY`) | [SFTP](./sftp.md) | | Managed services | `SERVICES_ENABLED=true` | [Managed services](./services.md) | | Functions (work in progress) | `FUNCTIONS_ENABLED=true` (also needs `HOSTING_STACKS_DIRECTORY`) | [Functions module](./functions.md) | | Private networks | Always available | [Private networks](./networks.md) | ### Startup checks The API checks every variable when it starts. If anything is wrong, it **refuses to start** and logs every problem at once (`invalid configuration`, one line per variable): - a missing required variable, a wrong type or out-of-range value, a `COOKIE_SECRET` shorter than 32 characters; - an unknown variable with a known prefix (`OIDC_`, `SFTP_`, `HOSTING_`, `SERVICES_`, `FUNCTIONS_`, `NETWORKS_`, `CRON_`, `COOKIE_`, `KEYCLOAK_`, `TRUSTED_`): usually a typo, and the message suggests the closest name; - a removed variable: the message says what replaces it. Values are never written in the logs. Only the installation checks of the services and functions modules (paths, mounts, Docker) disable that module without stopping the API. ### Good to know - Changing a variable requires restarting the API container. - Variables of the interface container (`API_URL`, `WS_URL`, branding) are listed in [Installation](./installation.md#interface-settings). --- ## Functions module :::caution Work in progress Functions are a work in progress: the API, the interface and the CLI exist, but the feature can still change. Runtime updates, external secret stores, public HTTP access and a per-function disk quota are not available yet. ::: The Functions module runs handlers written for AWS Lambda™ (Node.js, Python, or a compiled `bootstrap`) on the Docker host, in the AWS Lambda base images published by AWS, with [Loch Lambda Runtime](/lambda-runtime/) as entrypoint. This page sets the module up. To create and run functions, see [Functions](../user-guide/functions.md). :::note Admin only. Requires `FUNCTIONS_ENABLED=true` and `HOSTING_STACKS_DIRECTORY` (the compose files go there, even when the Hosting module is off). ::: ### Enable the module Add the variables to the API `.env`, then restart the API: | Variable | Role | Default | |---|---|---| | `FUNCTIONS_ENABLED` | Enables the module | `false` | | `FUNCTIONS_DATA_DIRECTORY` | Code, runtime binaries, secrets and references. Same path on the host and in the API container | `/data/functions` | | `FUNCTIONS_RUNTIME_DOWNLOAD` | Download the runtime binary from GitHub when it is missing | `true` | | `FUNCTIONS_RUNTIME_SOURCE_DIR` | Directory holding the runtime binary, for offline installations | empty | `FUNCTIONS_DATA_DIRECTORY` takes letters, digits and `_ . / -` only. It must be outside `HOSTING_STACKS_DIRECTORY`, `HOSTING_SITES_DIRECTORY`, `SFTP_DIRECTORY`, `SERVICES_DATA_DIRECTORY` and `API_LOG_DIR`. A subdirectory of `DATA_DIR` is fine, `DATA_DIR` itself is not. The API checks it at startup. Every directory under it must be owned by the panel and writable by nobody else. The menu **Serverless → Functions** appears for admins. If the installation checks fail (directories, Docker), only the module is disabled: the API logs say why, and `GET /functions/config` answers `FUNCTIONS_MISCONFIGURED`. Limits and budgets: [Configuration reference](../reference/configuration.md#functions). ### Runtime binary Every function runs the Loch Lambda Runtime binary. The panel build trusts given releases (today v1.0.1, amd64 and arm64) and only stores a binary that matches their size and SHA-256. | Mode | How | |---|---| | **Download** (default) | At startup, the panel downloads the release for the host architecture from GitHub over HTTPS. Allow outbound HTTPS to `github.com`, `release-assets.githubusercontent.com` and `objects.githubusercontent.com`. `HTTPS_PROXY` is honored | | **Offline** | Set `FUNCTIONS_RUNTIME_DOWNLOAD=false` and `FUNCTIONS_RUNTIME_SOURCE_DIR` to a directory holding `loch-lambda-runtime-linux-`, mounted read-only in the API container | ```bash # On a machine with Internet access, then copy the files to the host gh release download v1.0.1 -R loch-sh/lambda-runtime -p 'loch-lambda-runtime-linux-*' -p 'SHA256SUMS*' shasum -a 256 -c SHA256SUMS --ignore-missing ``` The source directory and its files must be owned by the panel's user or root and writable by nobody else, as regular files (no symlinks). Otherwise the API refuses to start. While no trusted binary is installed, the list page says **No trusted function runtime is installed on the server yet**, and creating or starting a function fails with `FUNCTION_RUNTIME_UNAVAILABLE`: the reason is in the API logs. An installed binary is never replaced. One that fails verification must be removed by hand. Release verification: [Releases](/lambda-runtime/developer/releases/). ### What the panel creates | Object | Name | |---|---| | Compose project | `fn-` in `HOSTING_STACKS_DIRECTORY`. It starts with `docker compose up -d` without the panel | | Private network | `fn-`, created with the function | | Image | `mirror.gcr.io/amazon/aws-lambda-nodejs`, `…-python` or `…-provided`, pinned by digest | | Code | Read-only deployments in `FUNCTIONS_DATA_DIRECTORY`, mounted at `/var/task` | | Secrets | A file of the private directory, never in `compose.yaml` | A compose file edited by hand is never run again: the function shows **Edited outside the panel** and can only be purged. ### Networks and egress - A function never joins the `services` network. It joins its own network `fn-` and the private networks you attach. - It has no published port. Members of its networks reach it on port 8080, with its invoke credentials. - **Internet access** is off by default. When on, the function also reaches the host gateway, the LAN and cloud metadata endpoints unless `DOCKER-USER` firewall rules block them. Turning it on is audited as a warning. - Function containers are protected like the API container: no shell, file copy, file tail, port tunnel, container action or `exec` scheduled task. Their stdout logs stay readable by admins. Restricted users never see them, and `fn-*` containers cannot be granted. ### API key scopes | Scope | Allows | |---|---| | `functions:read` | Read functions and the module configuration | | `functions:write` | Create, resize, start, stop, restart, delete | | `functions:deploy` | Code, deployments, secrets, credentials, variables, handler, networks, egress, MCP opt-in | | `functions:invoke` | Invoke. Implied by no other scope but `*` | The interface does not offer these scopes in its key form yet: use a key with **All permissions**, or create one through the API. Details: [API key scopes](../reference/scopes.md). ### Audit Every change is audited (`function created`, `function code deployed`, `function secret set` with the name only, `function egress toggled`…). Invocations are counted per function and per minute in `function invoked`, never with their content. Full list: [Audit trail](./audit.md). ### Good to know - The total memory and CPU of all functions are capped by `FUNCTIONS_MAX_TOTAL_MEMORY` and `FUNCTIONS_MAX_TOTAL_CPUS` (default: the host memory and 4 × its CPUs). - The kept deployments bound the disk used per function: `FUNCTIONS_DEPLOYMENTS_KEPT` × 250 MB at most. - Deleting the compose directory does not delete the code or the secrets. - At startup, the panel removes interrupted uploads and logs the functions edited outside the panel. - AWS and AWS Lambda are trademarks of Amazon.com, Inc. or its affiliates. Loch.sh is not affiliated with or endorsed by Amazon. --- ## Websites The Hosting module lets an admin create a website without writing a compose file. Each site is a plain Docker Compose project generated by the panel: it still boots with `docker compose up -d` if the panel is down. :::note Admin only. Requires `HOSTING_ENABLED=true` and a running Traefik. ::: ### Enable the module Add these variables to the API `.env`, then restart the API: | Variable | Role | Default | |---|---|---| | `HOSTING_ENABLED` | Enables the module | `false` | | `HOSTING_TRAEFIK_NETWORK` | External Docker network shared with Traefik | required | | `HOSTING_STACKS_DIRECTORY` | Where the compose projects are written | `/data/compose` | | `HOSTING_SITES_DIRECTORY` | Where the site files live (usually the SFTP directory) | `/data/websites/hosted` | Both directories must exist, be writable, and be mounted **at the same path** in the API container (`-v /data/compose:/data/compose`). The API refuses to start otherwise. The Traefik network is not checked at startup: if it is missing, deployments fail. Traefik entrypoints, certificate resolver and resource limits: see [Configuration reference](../reference/configuration.md). ### Create a website 1. Open **Hosting → Sites** and click **New hosting**. 2. Follow the steps: | Step | What you choose | |---|---| | **Type** | The template (table below) | | **Site** | **Name**, **Site directory** (new, or an existing one shared with SFTP users), **Document root**, **Domains**, **HTTPS**, **Redirect HTTP to HTTPS** | | **PHP & resources** | **PHP version**, memory and CPU limits, `php.ini` settings, **PHP modules** | | **Advanced** | Routing (Symfony, Laravel or custom rules), **Environment variables**, extra `php.ini` and virtual host directives, **Networks** | | **Review** | Check, then **Create the hosting** | 3. Follow the deployment in the **Last operation** tab of the site. ```bash loch hosting create blog --template php-nginx --php 8.4 --domain blog.example.com --https --redirect-https ``` Every flag: [CLI hosting guide](/cli/guides/hosting/). #### Templates | Template | Label | For | |---|---|---| | `php-nginx` | **PHP + Nginx** | Most PHP applications (WordPress, Laravel, Symfony) | | `php-apache` | **PHP + Apache** | Applications that rely on `.htaccess` | | `nginx` | **Static (Nginx)** | HTML sites, single-page applications | | `apache` | **Static (Apache)** | Static files with `.htaccess` support | Templates use the webdevops images. PHP versions: 8.5, 8.4, 8.3, 8.2, 8.1, 8.0, 7.4 (each also as `-alpine`) and 5.6. **8.4** is preselected. A version whose security support has ended is tagged **Deprecated**. #### Domains and HTTPS Traefik routes the domains to the site. Turn on **HTTPS** only once the DNS of every domain points to the server: otherwise the certificate cannot be issued. #### Limits | Setting | Rule | |---|---| | Name | Lowercase letters, digits, dashes, 32 characters max. Cannot be changed later | | Domains | 20 max | | Environment variables | 100 max. Variables managed by the panel (`PHP_*`, `WEB_DOCUMENT_ROOT`, `APPLICATION_*`, `CONTAINER_*`, `LD_*`) are refused | | Memory limit | Off (unlimited), or 64 MB up to `HOSTING_MAX_MEMORY_MB` | | CPU limit | Off (unlimited), or 0.1 up to `HOSTING_MAX_CPUS` | | Networks | 16 max | When the container reaches its memory limit, Docker kills it: leave the limit off unless you need to cap usage. #### Private networks A site reaches managed services through the networks it joins (for example `svc-postgres-17`). When the services module is enabled, the shared network `services` is preselected. See [Private networks](./networks.md). ### Update a website 1. Open the site, tab **Settings**. 2. Change the settings, then click **Save and redeploy**. The site directory and the name cannot be changed. Changing the networks redeploys the site (a few seconds of downtime). ```bash loch hosting update blog --domain www.blog.example.com ``` ### Redeploy, start, stop | Action | Effect | |---|---| | **Redeploy** | Recreates the container from the current settings. Tick **Pull the latest image** to get PHP and web server patches | | **Start** / **Stop** / **Restart** | Acts on the containers, without changing the files | ```bash loch hosting deploy blog --pull ``` ### Delete a website 1. Click **Delete** on the site. 2. Tick **Also delete the site files** only if you want the files gone too. 3. Type the site name to confirm. The files are kept by default. Deleting them is refused while another site uses the same directory. ```bash loch hosting delete blog --purge-data ``` :::warning Deleting the site files cannot be undone. SFTP users of that site lose all their files. ::: ### When a deployment fails - **Update**: the previous files are restored and redeployed. The **Last operation** tab shows **The previous configuration was restored.** and the Docker Compose output. - **Create** or **Redeploy**: nothing to roll back. Read the output in **Last operation**, fix the settings, then save again. ### Good to know - A compose file edited by hand is shown **Modified manually**. Saving the settings then overwrites it (**Overwrite manual changes**); a redeploy runs it as is only after you confirm. - Containers run without published ports and with `no-new-privileges`. - The ionCube PHP module is disabled by default: it runs encoded PHP and is common in malware. - PHP in the command line (crons) is never time-limited; `max_execution_time` applies to web requests only. - The panel only touches the `hosted-*` directories it generated: CI stacks in the same directory are never modified. --- ## Installation Loch Panel runs as two containers: the **API** (Go, port 2538), which talks to the Docker daemon, and the **interface** (nginx, port 80), a static web app that calls the API. :::note Prerequisites - A Linux server with Docker and `docker compose`. - An OIDC provider: Microsoft Entra ID, Keycloak, or any standard OIDC provider (Authentik, Authelia, Zitadel, Auth0, Okta, Google, GitLab…). - Two HTTPS URLs, for example `https://panel.example.com` (interface) and `https://panel-api.example.com` (API). ::: ### 1. Build the images Each project ships a multi-stage Dockerfile. Base images are pulled from `mirror.gcr.io`. ```bash # In host-panel-api/ docker build -t loch-panel-api . # In host-panel-interface/ docker build -t loch-panel-interface . ``` | Image | Base | Port | Volumes | |---|---|---|---| | API | `mirror.gcr.io/library/alpine` (includes `docker-cli` and the compose plugin) | 2538 | `/app/data`, `/app/log` | | Interface | `mirror.gcr.io/library/nginx` | 80 | `/branding` | ### 2. Register the OIDC client In your provider, create an OAuth 2.0 client (Authorization Code flow with PKCE): 1. Set the redirect URI to `/openid-connect/redirect`, for example `https://panel-api.example.com/openid-connect/redirect`. 2. Allow the scopes `openid email profile`. 3. Write down the client ID and secret, plus the provider settings below. | `OIDC_PROVIDER` | Also set | |---|---| | `microsoft` (default) | `TENANT_ID`, `APPLICATION_ID` | | `keycloak` | `KEYCLOAK_URL`, `KEYCLOAK_REALM` | | `generic` | `OIDC_DISCOVERY_URL` (the `.well-known/openid-configuration` URL) | The ID token must carry an `email` (or `preferred_username`) claim: it identifies the user. ### 3. Write the API configuration Create a `.env` file next to your compose file: ```env OIDC_PROVIDER=generic OIDC_DISCOVERY_URL=https://auth.example.com/.well-known/openid-configuration CLIENT_ID=loch-panel CLIENT_SECRET=change-me REDIRECT_URI=https://panel-api.example.com SCOPE=openid email profile FRONT_URL=https://panel.example.com COOKIE_SECRET=a-random-secret-of-at-least-32-characters PASSWORD_SECRET=required-but-not-used-yet ``` `REDIRECT_URI` is the public URL of the API, without the callback path. Every variable is checked at startup: see [Configuration](./configuration.md). ### 4. Start the containers ```yaml services: api: image: loch-panel-api restart: unless-stopped env_file: .env ports: - "2538:2538" volumes: - /var/run/docker.sock:/var/run/docker.sock - ./data:/app/data # users, API keys, scheduled tasks - ./log:/app/log # audit trail # Hosting and services: same path on the host and in the container # - /data/compose:/data/compose # - /data/websites/hosted:/data/websites/hosted interface: image: loch-panel-interface restart: unless-stopped ports: - "8080:80" environment: API_URL: https://panel-api.example.com WS_URL: wss://panel-api.example.com ``` ```bash docker compose up -d ``` The API exposes a health check on `/_health` (also used by the image `HEALTHCHECK`). It answers `{"message":"up","docker":true}` when Docker is reachable. ### 5. Log in as the first admin Open the interface and log in. **The first user to log in becomes admin.** Every later user is created at their first login with the restricted profile and no container. Grant them access in [Users](./users.md). ### Interface settings {#interface-settings} All optional, read when the container starts (no rebuild). | Variable | Default | Description | |---|---|---| | `API_URL` | `http://localhost:2538` | Base URL of the API | | `WS_URL` | `ws://localhost:2538` | Base URL of the API WebSocket (logs, terminal, proxy) | | `APP_NAME` | `Loch.sh Panel` | Name shown in the browser tab and terminal windows | | `LOGO_URL` | `/logo.svg` | Logo URL; overrides a logo mounted in `/branding` | | `FAVICON_URL` | the logo, else `/favicon.svg` | Favicon URL | | `SUPPORT_PHONE` | — | Support phone on the home page (hidden when empty) | | `SUPPORT_URL` | — | Support link on the home page (hidden when empty) | | `STATUS_URL` | — | Status page embedded on the home page (hidden when empty) | To use your own logo, mount `logo.svg` (or `.png`, `.webp`, `.jpg`, `.jpeg`, `.ico`) and optionally `favicon.*` into `/branding`. ### Reverse proxy Put both containers behind an HTTPS reverse proxy (Traefik, nginx…): - **HTTPS is required.** The session cookie is `Secure` and `SameSite=None`. - **WebSocket**: the proxy must forward the upgrade on the API route `/ws` (terminal, live logs, stats, port proxy). Use `wss://` in `WS_URL`. - **Origin**: `FRONT_URL` must be exactly the interface URL (scheme, host, port). It is the only origin allowed by CORS, the CSRF check and the WebSocket. - **Client IP**: set `TRUSTED_PROXIES` to the subnet of the proxy, for example the Traefik network (`docker network inspect traefik`). Without it, the rate limit and the audit trail see the proxy address. - **Cookie domain**: `COOKIE_DOMAIN` defaults to the API host. Change it only if you know you need to. ### Good to know - The API needs the Docker socket: anyone who controls the API container controls the host. Do not expose port 2538 without the reverse proxy. - Mount `/app/log`, or the audit trail is lost when the container is recreated. - Hosting and services directories must be bind-mounted at the **same path** in the API container as on the host; the API checks it at startup. - The panel pulls the images it runs (sites, services) from `mirror.gcr.io`; the services module also needs outbound HTTPS to discover versions (see the [Configuration reference](../reference/configuration.md#services)). - Optional modules: [Websites](./hosting.md), [SFTP](./sftp.md), [Managed services](./services.md), [Private networks](./networks.md). --- ## Private networks A private network decides which websites reach which service instances and [functions](../user-guide/functions.md). A site only sees the instances and functions on the networks it joins. :::note Admin only, always available from **Network → Private networks**. The shared network `services` exists when the services module is enabled. ::: ### What a network is - A plain Docker `bridge` network, created **internal**: no Internet access through it. - Its Docker name is `loch-net-`. - Members reach each other by container name (`svc-postgres-17`, `hosted-shop-web-1`). - Networks do not route to each other. A site on two networks talks to both, nothing more. Every member's compose file declares its networks, with the `docker network create` command in its header: a stack still boots without the panel. ### The `services` network The shared network of the services module, listed first and tagged **Shared**. - New sites join it by default, and so do instances created without a network. - It cannot be deleted. - ElasticMQ (no authentication) is refused on it. ### Create a network 1. Click **New network**. 2. Fill in: | Field | Rule | |---|---| | **Name** | 1 to 24 lowercase letters, digits or dashes, starting with a letter. Cannot be changed | | **Description** | Optional, 200 characters max, editable later | | **Addressing** | **Automatic** (Docker picks a free range) or **Manual subnet**: a private IPv4 range (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`), from `/16` to `/28` | | **Members** | Optional: sites, instances and functions to attach right away | 3. Click **Create the network**. ```bash loch networks create backend --subnet 10.20.0.0/24 --description "Shop" ``` Every command: [CLI networks guide](/cli/guides/networks/). #### Creation rules - The names `services`, `pub`, `default`, `host`, `none`, `bridge`, `traefik`, `config` and `docker` are reserved. - At most `NETWORKS_MAX` private networks (default 50; `services` does not count). - A subnet that overlaps another Docker network, or a range listed in `NETWORKS_RESERVED_RANGES` (your LAN, a VPN), is refused. - The subnet cannot be changed later: create a new network and move the members. Docker's default address pools hold about 31 networks for the whole host. Extend `default-address-pools` in `/etc/docker/daemon.json` before creating many networks. ### Attach and detach members On the network page, click **Attach a resource** and choose a site, a service instance or a function. To remove one, click **Detach** on its row. ```bash loch networks attach backend --site shop loch networks detach backend --instance postgres-17 ``` The member's **container is recreated**: a few seconds of downtime for that container only. | Member | Behaviour | |---|---| | Site | Redeployed like a settings update. A site may have no network | | Instance | Restarted (up to a few minutes). An instance keeps at least one network | | Function | Recreated: an invocation in progress is cut. A function keeps at least one network and never joins `services` | You can also change networks from the site settings (**Networks**), from the instance list (**Change the networks**) or from the **Networks** tab of a function. Attaching or detaching a function with an API key also needs `functions:deploy`. ### Delete a network Click **Delete**. A network must have no member left: detach them first. ```bash loch networks delete backend ``` ### Good to know - A network is **not** a boundary against CI or anyone using the Docker socket: any container can join a network by name. Such containers appear as **Not managed**; click **Disconnect** to remove them. - **Other Docker networks** (tab on the list) shows every other network of the host, read-only. - A missing network is recreated when the API starts. One replaced outside the panel is flagged and its members are not trusted. - Sites on a panel network lose `NET_RAW`. - IPv4 only. Traffic is clear text unless the engine uses TLS. - Each function gets its own private network `fn-` at creation. Attach to it the sites and instances that invoke the function. It is deleted with the function, unless other members still use it. --- ## Security The points an admin must get right. The API applies the rest by itself. ### Network exposure - **The API controls Docker.** It mounts the Docker socket: whoever controls the API controls the host. Publish it only through an HTTPS reverse proxy. - **HTTPS only.** Session cookies are `Secure`, `httpOnly` and signed with `COOKIE_SECRET` (at least 32 characters). Keep this secret out of version control. - **Trusted proxies.** The client IP (rate limit, audit trail) is read from `X-Forwarded-For` only when the request comes from the loopback or from `TRUSTED_PROXIES`. List the reverse proxy subnet only, never a whole Docker range: any container could then forge its IP. - **Rate limit**: 120 requests per minute per IP, WebSocket included. ### Origin check (CSRF) The session cookie is `SameSite=None`, so the API checks the origin of every write request (`POST`, `PUT`, `PATCH`, `DELETE`): - a browser request must carry the `Origin` of `FRONT_URL` (scheme, host and port), otherwise it gets `403 FORBIDDEN_ORIGIN`; - the WebSocket checks the same origin; - requests without browser headers (CLI, MCP server, scripts) are not concerned: they use API keys, not cookies. If writes fail with `FORBIDDEN_ORIGIN`, `FRONT_URL` does not match the URL users type in their browser. ### API keys Each user creates their own keys, for the `loch` CLI and scripts. | Rule | Value | |---|---| | Format | `loch_` + 64 hexadecimal characters, sent in `X-API-Key` | | Shown | Once, at creation; only its SHA-256 hash is stored | | Lifetime | 1 to 365 days | | Per user | 20 keys at most | | Profile | The key has the profile of its user: a restricted user's key never reaches admin routes | | Scopes | A key can be limited to some features; it can only create keys within its own scopes | - Deleting a user revokes all their keys. - Admin routes (users, domains, websites, SFTP, services, networks, functions, audit) always require the admin profile, whatever the scopes. - Full list of scopes: [API key scopes](../reference/scopes.md). ### The API container is protected Nobody, admin included, can open a shell in the API container, copy files to or from it, follow its files, or target it with an `exec` scheduled task. These would give access to the audit log. Stack actions also skip it. Function containers (`fn-*`) are protected the same way: no shell, file copy, file tail, port tunnel, container action or Docker scheduled task, since they hold secrets. Their environment is masked. Manage them from the function page. See [Functions module](./functions.md). ### Private networks are not a sandbox [Private networks](./networks.md) separate the panel's sites and service instances from each other. They are **not** an access control against CI stacks or anything with access to the Docker socket: any container can join a network by name. The panel shows the containers it does not manage on its networks, so you can disconnect them. ### Good to know - Grant restricted users only the containers they need: a shell in a container gives everything that container can reach. - Check the [audit trail](./audit.md) for `access denied` entries. - Managed services (published ports, TLS) have their own rules: see [Managed services](./services.md). - To report a vulnerability, contact the maintainers privately, not in a public issue. --- ## Managed services The services module runs databases, caches, message brokers and object storage for your websites, each from its official Docker image. You get credentials; the panel runs the engine. :::note Admin only. Requires `SERVICES_ENABLED=true`. ::: ### Enable the module Add the variables to the API `.env`, then restart the API: | Variable | Role | Default | |---|---|---| | `SERVICES_ENABLED` | Enables the module | `false` | | `SERVICES_DATA_DIRECTORY` | Data of the instances, same path on the host and in the API container | `/data/services` | | `SERVICES_ENGINES` | Enabled engines, comma-separated (empty: all) | all | The compose files go to `HOSTING_STACKS_DIRECTORY`, so the module needs that directory even when the Hosting module is off. Other variables (limits, exposure, TLS, version pinning): [Configuration reference](../reference/configuration.md). The menus **Databases**, **Cache**, **Messaging**, **Queues** and **Storage** list the enabled engines. ### Shared and dedicated instances | Topology | Engines | One instance | A resource is | |---|---|---|---| | **Shared** | PostgreSQL, MariaDB, MySQL, MongoDB, RabbitMQ | Per engine version (`svc-postgres-17`), used by every site | A database (or RabbitMQ virtual host) and its user | | **Dedicated** | Redis, Valkey, NATS, ElasticMQ, Eclipse Mosquitto, Loch S3 | Per resource (`svc-redis-cache`), named and sized at creation | The instance itself | - A shared instance starts with its first resource and is removed, **with its data**, when its last resource is deleted. - A dedicated instance is deleted with its data. Engines, versions and options: [Services engines reference](../reference/services-engines.md). ### Create a resource 1. Open the engine in the menu (for example **Databases → PostgreSQL**) and click **New database** (**New instance** for a dedicated engine). 2. Choose the **Version**. It cannot be changed later. 3. For a dedicated engine, fill in **Size and settings**. **Custom** accepts any size in a range. 4. Under **Network access**, choose the networks (default: `services`), then enter the **Name**. 5. Click **Create**. ```bash loch services create shop --engine postgres --version 17 ``` Every flag: [CLI services guide](/cli/guides/services/). ### Credentials After creation, **Connection details** shows the host, port, user, password and connection URI. :::warning The password is shown **once**. Copy it before you click **I have copied the password**. If you lose it, generate a new one. ::: **New password** generates another one. The old one stops working at once and open connections are closed; a dedicated instance restarts. ```bash loch services reset-password postgres-17 shop ``` The MCP server never returns a password: run `loch services reset-password` in a terminal. ### Resize a dedicated instance Click **Edit** on the instance, change its size or settings, then confirm. The container is recreated: clients are disconnected for a few seconds, data is kept. If it does not start, the previous settings are restored. Settings marked **Set at creation** never change. ```bash loch services update redis-cache --option memory=1024 ``` ### Update a dedicated instance The panel discovers the published versions every 12 hours (or click **Check for updates**). When an instance shows **Update available**, click **Update** and choose: - the latest image of its version (same data format), or - one of up to 3 newer versions. The image is pulled, then the instance is recreated. If it does not start, the previous version is restored. There is never a downgrade. ```bash loch services upgrade redis-cache ``` :::warning No backup is made before a version change, and there is no way back. Back up the data first. ::: Shared instances are not updated: create the resources on an instance of the new version and move the data. ### Publish a port By default an instance is **Internal only**: only the sites on its networks reach it. To let clients outside Docker connect, click **Configure the published port**, enter an address and a port, then **Publish**. The instance restarts. ```bash loch services expose postgres-17 127.0.0.1:5417 ``` The server environment sets the allowed addresses (`SERVICES_EXPOSE_ALLOWED_CIDRS`, default `127.0.0.1`). On a public address, TLS becomes mandatory for every client, the sites included. Publishing needs the `services:expose` scope for API keys and is not available through the MCP server. ### Delete | What | How | |---|---| | A database or virtual host | **Delete**, type the name. The last one also removes the shared instance and its data | | A dedicated instance | **Delete**, type the name. Data deleted | | An empty shared instance | **Remove instance** | ```bash loch services delete postgres-17 shop ``` Deletion is final. ### Good to know - Passwords are random (about 190 bits), generated by the panel, never chosen by users and never stored in clear (except the Loch S3 secret, which SigV4 needs). - ElasticMQ has **no authentication**: it is refused on the shared `services` network. Put it on a private network shared only with the sites allowed to use it. - An instance whose files were edited by hand is shown **Edited by hand** and never run by the panel. Restore the files, or delete it with its data. - A network is not a boundary against whoever can use the Docker socket or deploy CI stacks. - More: [Security](./security.md). --- ## SFTP accounts The SFTP module creates SFTPGo users so that website owners can upload their files. SFTPGo is the only source of truth: the panel stores nothing. :::note Admin only. Requires a running [SFTPGo](https://github.com/drakkan/sftpgo) and `SFTP_ENABLED=true`. ::: ### Enable the module 1. In the SFTPGo admin interface, create an admin API key. 2. Add the variables to the API `.env`: ```env SFTP_ENABLED=true SFTP_DIRECTORY=/data/websites/hosted SFTP_API_URL=http://sftpgo:8080 SFTP_API_KEY= SFTP_DOMAIN=sftp.example.com SFTP_PORT=2222 SFTP_WEB_USER_INTERFACE=https://sftp.example.com/web/client ``` 3. Restart the API. **Hosting → SFTP** appears for admins. | Variable | Role | Default | |---|---|---| | `SFTP_DIRECTORY` | Base directory of the sites | required | | `SFTP_API_URL` | Internal URL of the SFTPGo admin API | required | | `SFTP_API_KEY` | SFTPGo admin API key | required | | `SFTP_DOMAIN`, `SFTP_PORT` | Connection details shown to users | none | | `SFTP_WEB_USER_INTERFACE` | URL of the SFTPGo web client shown to users | none | | `SFTP_INIT_DIRS` | Directories created in a new user's home (empty: none) | `web,logs` | Use the same directory as `HOSTING_SITES_DIRECTORY` so that SFTP users edit the files of the [websites](./hosting.md). Only SFTPGo users whose home is under `SFTP_DIRECTORY` are shown; the others are ignored. ### Sites and users - A **site** is a directory under `SFTP_DIRECTORY`. It is created with its first user and shared by all its users. - A **user** can be limited to a **Subdirectory** of the site (for example `clients/smith`). Leave it empty for the whole site. It is set at creation and cannot be changed. ### Create a user 1. Open **Hosting → SFTP** and click **New user** (or **Add user** on a site row). 2. Fill in the form: | Field | Notes | |---|---| | **Site** | Created if it does not exist | | **Subdirectory** | Optional, fixed after creation | | **Username** | | | **Password** | At least 8 characters. **Generate** creates a strong one | | **Public SSH key** | Optional: login without a password | | **Size quota**, **File quota**, **Bandwidth limit** | Empty: unlimited | | **Require password change at first login** | | | **Initialize directories** | Creates the `SFTP_INIT_DIRS` directories in the user's home | 3. Click **Create user**, then **Copy connection** to send the details to the owner. ```bash loch sftp create client1 --site my-site --subpath clients/smith ``` Every flag: [CLI SFTP guide](/cli/guides/sftp/). Directory initialization is best effort: if it fails, the user is still created and a warning is shown. ### Manage users | Action | Effect | |---|---| | **Edit** | Change the SSH key, quotas, bandwidth and password-change requirement. Site and subdirectory stay fixed | | **Reset password** | Set a new password (at least 8 characters) | | **Enable** / **Disable** | Allow or block logins | | **Delete** | Removes the account. The files stay on disk | ```bash loch sftp reset-password client1 ``` Without `--password`, the CLI asks for the new password interactively, so it does not appear in the shell history. ### Web client When `SFTP_WEB_USER_INTERFACE` is set, the page shows the **Client web interface** link: users can manage their files from a browser, without an SFTP client. ### Good to know - Passwords are never logged nor returned by the panel: send them to the owner yourself, over a secure channel. - Through the MCP server, a password generated when none is given is returned **once**. - The home directory is built by the API: neither the site nor the subdirectory can leave `SFTP_DIRECTORY` (no `.`, `..` or separators; a leading dot such as `.well-known` is allowed). - Every operation is recorded in the audit trail. --- ## Users and access Users come from your OIDC provider: an account is created at its first login. Only admins manage users, from **Administration → Users**. ### Profiles | Profile | Containers | Admin features | |---|---|---| | **Admin** | All | Users, domains, websites, SFTP, managed services, private networks, audit trail | | **Restricted** | Only those granted by an admin | None (403) | - The **first user** to log in becomes admin. - Every later user starts **restricted, with no container**. Every user, whatever the profile, manages their own API keys and can use scheduled tasks. ### Grant containers 1. In **Users**, click **Edit** on the user's row. 2. In the side panel, move the containers to the right-hand list (use **Search for a container** to filter). 3. Click **Confirm**. The **Permissions** column then lists the granted containers. A restricted user sees and acts only on those containers (logs, shell, file copy, stats, actions, scheduled tasks). ### Actions | Button | Effect | |---|---| | **Edit** | Choose the containers of a restricted user | | **Turn into admin** | Promote: access to every container and admin feature | | **Turn into restricted user** | Demote to restricted | | **Deny all containers** | Make restricted and remove every granted container | | **Delete** | Delete the user and revoke all their API keys | Your own row shows **Current user** instead of buttons: another admin must change your profile. ### From the CLI ```bash loch users ls loch users grant alice@example.com web-1 db-1 loch users revoke alice@example.com db-1 loch users promote alice@example.com loch users demote alice@example.com loch users rm alice@example.com ``` See the [Loch CLI](/cli/) documentation. ### Good to know - Access is granted by **container name**: a container recreated with the same name keeps its users; a renamed one does not. - Function containers (`fn-*`) can never be granted to a restricted user. - Containers with a `system` label are not listed in the panel. - An API key carries the profile of its user: a restricted user's key never reaches admin routes, whatever its scopes ([scopes](../reference/scopes.md)). - Deleting a user does not block their OIDC account: they get a new restricted account at their next login. Block them in the provider. - Every change is recorded in the [audit trail](./audit.md). --- ## Architecture Loch Panel is made of three projects, each in its own Git repository, grouped in one workspace directory. | Project | Role | Stack | |---|---|---| | `host-panel-api` | REST and WebSocket API, drives Docker (port 2538) | Go 1.27, standard `net/http`, Moby Docker SDK | | `host-panel-interface` | Web interface (port 5173 in development), in 10 languages | React 19, TypeScript, Vite, Ant Design 6, TanStack Query | | `host-panel-cli` | [`loch` CLI](/cli/) and MCP server | Go, Cobra, Viper | ### How the projects talk | From | To | Channel | Authentication | |---|---|---|---| | Interface | API | REST (Axios, `withCredentials: true`) | Session cookie | | Interface | API | WebSocket `/ws`: shell, live logs, live stats | Session cookie | | CLI | API | REST and WebSocket `/ws` (also the TCP port proxy) | `X-API-Key` header | | API | Docker | Docker socket | — | | API | SFTPGo | SFTPGo admin REST API (when `SFTP_ENABLED`) | `X-SFTPGO-API-KEY` header | **Session (browser).** The API runs the OIDC login itself (`/openid-connect/login`, then `/openid-connect/redirect`). It stores the token in a signed `authorization` cookie and checks it against the provider's JWKS on each request. When the API answers `401`, the interface redirects to `/openid-connect/login`. A session has every permission of its user. **API key (CLI, scripts, MCP).** The `X-API-Key` header is checked first. A key carries its user's profile and can be limited by scopes. See [API reference](../reference/api.md). ### Request path in the API 1. Global middlewares, in this order: `Recovery`, `Logging`, `AuditRequests`, `RateLimit` (120 requests per minute per IP), `SecurityHeaders`, `CORS(FRONT_URL)`, `SameOrigin(FRONT_URL)` (CSRF). 2. Route wrapper: `authenticated` (API key, otherwise JWT and user) or `adminOnly` (the same, plus the admin check). Routes are declared in `cmd/server/main.go`. 3. Handler: checks the API key scope with `middleware.RequireScope`, then answers with `writeJSON` / `writeError`. ### API packages | Package | Role | |---|---| | `cmd/server` | Entry point: configuration, wiring, routes, service engine registration (`serviceEngines()`) | | `internal/config`, `internal/env` | Environment loading, validation and lint | | `internal/auth` | OIDC providers, JWKS, signed cookies | | `internal/middleware` | Authentication, CSRF, CORS, rate limit, audit, logging | | `internal/handler` | HTTP and WebSocket handlers, error codes | | `internal/ws` | WebSocket hub: logs, shell, stats, port proxy | | `internal/docker` | Docker client, container cache (3 s), exec, networks | | `internal/store`, `internal/model` | Atomic JSON persistence; users, API keys | | `internal/cron` | Scheduled tasks: scheduler, executor, store | | `internal/audit` | Audit log writer and reader | | `internal/hosting`, `internal/composekit` | Web hosting; helpers to write and run compose projects | | `internal/sftpgo` | SFTPGo admin client | | `internal/services` | Managed services core; one sub-package per engine (`postgres`, `mysqlfamily`, `mongodb`, `redisfamily`, `nats`, `rabbitmq`, `elasticmq`, `mosquitto`, `s3`) | | `internal/networks` | Private networks: registry, verification, members | | `internal/functions` | Functions (work in progress): manager, compose rendering, code upload, runtime binary, invocation | | `internal/registry`, `internal/releases` | Image references (`mirror.gcr.io`, `ghcr.io`); version discovery | The API stores its state in JSON files under `DATA_DIR` (`users.json`, `apikeys.json`, `cron.json`, `networks.json`) and writes the audit trail to `audit.log` in its log directory. ### Interface structure | Path | Content | |---|---| | `src/config.ts` | Runtime configuration: `window.__APP_CONFIG__`, then `VITE_*` variables, then defaults | | `src/i18n/` | i18next setup, typed keys, locale files | | `src/Services//` | API calls (`service.ts`), models, React Query hooks | | `src/Pages//` | One folder per page | | `src/Components/` | Shared components; menu in `Components/Layout/MenuLayout.tsx` | | `src/App.tsx` | Routes | | `tests/` | Mocked API, Vitest setup, Playwright tests | ### Good to know - Each project has its own Git repository: commit in the project you changed. - The CLI has no access to Docker: everything goes through the API. - The detailed design notes stay in the API repository (`host-panel-api/docs/`). --- ## Conventions Follow these rules in every change to the API, the interface or the CLI. ### Docker images | Rule | Detail | |---|---| | Pull from `mirror.gcr.io`, never from Docker Hub directly | `mirror.gcr.io/library/` for official images, `mirror.gcr.io//` for the others | | In Go, build references with the helpers | `registry.Hub("postgres:17")`; `registry.GHCR(...)` for our own images (Loch S3), the only other exception | | Image missing from the mirror | Stays on Docker Hub, listed in `notMirrored` (`internal/hosting/catalog.go`) | This applies to engine catalogs, site templates, Dockerfiles, CI jobs, the development compose file and integration tests. ### Languages | What | Language | |---|---| | Code, identifiers, technical comments | English | | API errors, logs, CLI help and messages | English | | Interface texts | Translated in all 10 languages, English is the source. See [Translations](./translations.md) | ### API - Answer with `writeJSON` / `writeError` and a stable error code (`{"code": "...", "error": "..."}`). The interface translates the code (`errors` namespace). - Read every environment variable through an `env.Reader`, never `os.Getenv`, so the startup lint sees it. Add a new variable to `config.go` and `.env.example`; declare a removed one with `Reader.Removed`; add a new prefix to `lintedPrefixes` (`internal/config`). Never put a value in a lint message. - Stores write atomically (temporary file, then rename) behind a read-write lock. - Audit anything a user can trigger, never with a secret (password, token, header, body). Add every new audit event to `AUDIT_EVENTS` (`Services/Audit/Models/audit.model.ts`) and translate it in all 10 languages. - A new service engine: follow "Adding a service engine" in `host-panel-api/docs/development.md`, read `docs/services-security.md` first, and add an integration test. ### Interface - No hardcoded visible string: everything goes through `t()`. - Read `import.meta.env` only in `src/config.ts`. - Never display raw Docker `State` / `Status`: use `useContainerLabels()`. - Load pages with `lazyPage` and keep them inside the `ErrorBoundary`. - Add every new API call to the mocked API (`tests/mocks/api.ts`). See [Testing](./testing.md). ### Security rules worth knowing | Rule | Why | |---|---| | A new route that reads or changes data checks an API key scope (`middleware.RequireScope`); a new scope goes in `ValidScopes` (`internal/model/apikey.go`) | Keys can be limited; a session always has every permission of its user | | Write routes stay on POST, PUT, PATCH or DELETE | `SameOrigin(FRONT_URL)` blocks browser writes from another origin (CSRF, the session cookie is `SameSite=None`) | | Validate container paths on the API side (absolute, no null or newline, at most 4096 characters) | File copy and file tail run inside containers | | Pass commands to `docker exec` as `[]string`, never through a shell | No injection | | Never act on the API's own container (shell, copy, file tail, exec jobs) | It holds the audit trail (`SELF_CONTAINER_PROTECTED`) | | Every value rendered into a configuration comes from a fixed list or a validated identifier | Templates and compose files are executed | | Show a generated token or password only once, never log it | API key tokens are stored as SHA-256 hashes only | Full list of scopes: [API key scopes](../reference/scopes.md). ### Good to know - Document every feature in `host-panel-api/docs/` as well. - Each project is a separate Git repository. --- ## Local development Run the API and the interface from the workspace directory that holds the three projects. :::note You need Go 1.27 or later, Node.js 26 and Yarn 4 (`corepack enable`), Docker with `docker compose`, and an OIDC provider (Microsoft Entra ID, Keycloak or any compliant provider). ::: ### Start 1. Install the dependencies of the API and the interface: ```bash make install ``` 2. Create the API configuration, then set the OIDC client, `FRONT_URL` and the secrets: ```bash cp host-panel-api/.env.example host-panel-api/.env ``` 3. Optional: create the interface configuration (API URL, branding): ```bash cp host-panel-interface/.env.example host-panel-interface/.env ``` 4. Start SFTPGo, then the API and the interface: ```bash make dev ``` 5. Open http://localhost:5173 and log in. ### API `.env` for development The values below come from `.env.example`. Full list: [Configuration reference](../reference/configuration.md). ```env OIDC_PROVIDER=microsoft # microsoft, keycloak or generic CLIENT_ID=your-client-id CLIENT_SECRET=your-client-secret REDIRECT_URI=http://localhost:2538 FRONT_URL=http://localhost:5173 COOKIE_SECRET=a-secret-of-at-least-32-characters-here APP_ENV=development PORT=2538 DATA_DIR=./data ``` The API refuses to start on an invalid, missing, unknown or removed variable, and lists every problem at once. ### Workspace targets | Target | Description | |---|---| | `make dev` | SFTPGo, then the API and the interface in parallel. Ctrl-C stops the API and the interface | | `make api` / `make web` | The API only / the interface only | | `make sftpgo` / `make sftpgo-logs` / `make down` | Start SFTPGo / follow its logs / stop it | | `make install` | Install the dependencies | | `make clean` | Stop SFTPGo and delete its local data | In the API directory, `make dev` runs `go run ./cmd/server` without reload: restart it after a change. The interface (`yarn dev`) reloads on its own. ### Ports | Service | URL | |---|---| | API | http://localhost:2538 | | Interface | http://localhost:5173 | | SFTPGo web admin and API | http://localhost:8080 | ### Enable SFTP in development 1. Run `make sftpgo` (or `make dev`). 2. Create an SFTPGo admin API key: `cd host-panel-api && make sftp-key`. It asks for the SFTPGo admin password and needs `curl` and `jq`. 3. Add to `host-panel-api/.env`: ```env SFTP_ENABLED=true SFTP_DIRECTORY=/data/websites/hosted SFTP_API_URL=http://localhost:8080 SFTP_API_KEY= SFTP_DOMAIN=localhost SFTP_PORT=2222 SFTP_WEB_USER_INTERFACE=http://localhost:8080/web/client ``` 4. Restart the API: the **SFTP** menu entry appears for admins. ### The CLI The CLI is built separately, in `host-panel-cli`: | Command | Description | |---|---| | `make build` | Build `./loch` (version `dev`, so auto-update is skipped) | | `make run` | `go run .` | | `make mcp-inspect` | Open the MCP inspector on `loch mcp` | Point it to your local API with an API key created in the interface. See the [CLI documentation](/cli/). ### Good to know - The API needs access to the Docker socket. - `.env` files are not tracked: only `.env.example` is. - Your OIDC client must allow the `REDIRECT_URI` you set. --- ## Testing Each project has its own test suites. The interface tests never need a running API: they use a mocked API. ### API Run from `host-panel-api`: | Command | What it runs | |---|---| | `make test` | Unit tests (`go test ./...`) | | `make test-integration` | Integration tests (build tag `integration`): each service engine on its real images, and Docker exec. Needs Docker, takes a few minutes (timeout 20 min) | | `make vet` / `make fmt` | Static analysis / formatting | Add an integration test on real images for every new service engine. ### Interface Run from `host-panel-interface`: | Command | What it runs | |---|---| | `yarn typecheck` | Type-check of the app and the tests | | `yarn lint` | ESLint | | `yarn i18n:check` | Same keys, placeholders and tags in every language | | `yarn test` | Unit and component tests (Vitest, jsdom). `yarn test:watch` for watch mode | | `yarn test:e2e` | End-to-end tests (Playwright, Chromium) | Before a merge request, run: ```bash yarn lint && yarn i18n:check && yarn build ``` #### Vitest - Tests live next to the code: `src/**/*.test.ts(x)`. - MSW serves the mocked API (`tests/setup/msw.ts`). - `renderWithProviders` (`tests/setup/render.tsx`) mounts a component with React Query and a router. #### Playwright - Tests live in `tests/e2e/*.spec.ts`. - The app is built with `VITE_API_URL=http://api.test` into `dist-e2e/` and served by `vite preview` on port 4180. - `page.route()` answers every request to `http://api.test` (`tests/e2e/fixtures.ts`). The WebSocket is accepted without a server. Requests to any other origin are refused. - First run: `npx playwright install chromium`. - On failure, the trace is in `test-results/`: `npx playwright show-trace `. #### The mocked API Both suites share one in-memory mock of the API: `tests/mocks/api.ts`, with typed fixtures in `tests/mocks/fixtures.ts`. A test **fails if the interface calls a route the mock does not serve**. When you add an API call to the interface, add the route to `tests/mocks/api.ts`, or override it in the test: ```ts test("shows the API error", async ({ page, api }) => { api.on("POST", "/services/resources", { status: 409, body: { code: "RESOURCE_EXISTS" }, }); api.state.me = restrictedUser; // change the mocked state // ... expect(api.calls("POST", "/services/resources")[0].body).toEqual({ ... }); }); ``` | Helper | Use | |---|---| | `api.state` | Change the mocked data for one test | | `api.on(method, path, response)` | Override one route | | `api.calls(method, path)` | Inspect the requests the interface sent | ### CLI Run from `host-panel-cli`: | Command | What it runs | |---|---| | `make test` | `go test ./...` | | `make lint` | `go vet` and `golangci-lint` | ### Continuous integration | Project | Jobs | |---|---| | API | `go test ./...` | | Interface | `yarn lint`, `yarn typecheck`, `yarn i18n:check`, `yarn test`, `yarn build`; then `yarn test:e2e` | ### Good to know - Integration test images come from `mirror.gcr.io`, like every other image. - Playwright retries a failed test once in CI, never locally. --- ## Translations The interface is translated with i18next (react-i18next). English is the source: the other languages must have exactly the same keys. | Code | Language | |---|---| | `en` | English (source) | | `fr` | French | | `de` | German | | `es` | Spanish | | `it` | Italian | | `nl` | Dutch | | `pt-PT` / `pt-BR` | Portuguese (Portugal / Brazil) | | `ja` | Japanese | | `zh-CN` | Simplified Chinese | ### Files Translations live in `host-panel-interface/src/i18n/locales//.json`, one namespace per area of the app: `common`, `layout`, `home`, `containers`, `domains`, `cron`, `users`, `sftp`, `hosting`, `databases`, `networks`, `functions`, `apikeys`, `audit`, `errors`. ### Add or change a text 1. Add or change the key in the English file. Keys are typed from the English files: an unknown key fails the type-check. 2. Add the same key to the same file in the 9 other languages. 3. Use it in the code with `t("namespace:key")`. Outside a component, call `i18n.t("ns:key")` when the text is needed, not when the module loads. 4. Run `yarn i18n:check`. ### Rules checked by `yarn i18n:check` The check also runs in CI. | Rule | Example | |---|---| | Same keys as English in every language | — | | Same `{{placeholders}}` | `"{{count}} containers"` | | Same `` tags | `…`, `
` | | Plurals use i18next suffixes and define at least `_other` | `_one` / `_other`; `_many` in `fr`, `es`, `it`, `pt-*`; `_other` only in `ja` and `zh-CN` | | Every language of `SUPPORTED_LANGUAGES` has a folder, and every folder is declared | — | `{{count}}` may be left out of one plural form ("one item"). ### Add a namespace 1. Create `locales/en/.json` and the same file in every other language. 2. Import the English file in `src/i18n/index.ts` and add it to the `en` object. The other languages are loaded automatically. ### Add a language Example for Korean (`ko`): 1. Copy `src/i18n/locales/en/` to `src/i18n/locales/ko/` and translate the values. Keep the keys, `{{placeholders}}` and tags unchanged. 2. Add `"ko"` to `SUPPORTED_LANGUAGES` in `src/i18n/index.ts`. Add a `fallbackLng` entry if a bare browser code must map to a regional one (as `pt` maps to `pt-PT`). 3. Add the date-fns, antd and cronstrue locales in `src/i18n/useLocales.ts`. 4. Add the language name, written in that language, under `language` in every `common.json`. 5. Run `yarn i18n:check` and `yarn build`. ### Good to know - Use a regional code only when variants differ (`pt-PT` / `pt-BR`) or the script matters (`zh-CN`). - Browser codes resolve to the closest match (`fr-CA` → `fr`); unsupported languages fall back to English. - The chosen language is stored in `localStorage` (`lang`). - Machine-assisted translations are accepted; English is required. --- ## Loch Panel Loch Panel manages a Docker server from a web interface, the `loch` CLI or an AI assistant (MCP). | Component | Role | |---|---| | API | REST and WebSocket API, talks to Docker (port 2538) | | Interface | Web interface, in 10 languages | | [`loch` CLI](/cli/) | Command line and MCP server | ### Features | Feature | Who | Enabled by | |---|---|---| | Containers and stacks: logs, shell, file copy, stats, actions | Every user, on the containers they are granted | Always on | | Scheduled jobs | Every user | Always on | | API keys | Every user, for their own keys | Always on | | Users and access rights | Admin | Always on | | Websites (PHP, Nginx, Apache) | Admin | `HOSTING_ENABLED` | | SFTP accounts for websites | Admin | `SFTP_ENABLED` | | Managed services: databases, cache, messaging, storage | Admin | `SERVICES_ENABLED` | | Functions: handlers written for AWS Lambda™ (work in progress) | Admin | `FUNCTIONS_ENABLED` | | Private networks | Admin | Always on (the shared `services` network needs the services module) | | Audit trail | Admin | Always on | A menu entry appears only when its feature is enabled. --- ## API reference The API listens on port `2538` and answers in JSON. ### Authentication | Client | How | Permissions | |---|---|---| | Browser | Session after the OIDC login: signed `authorization` cookie (an `Authorization: Bearer ` header is also accepted) | Every permission of the user | | CLI, scripts, MCP | API key in the `X-API-Key` header | The user's profile, limited by the key's scopes | - **Profile**: a user is admin or restricted. A key carries its user's profile: an admin scope on a restricted user's key grants nothing. Restricted users only see the containers they are granted. - **Scopes**: a key without scopes (legacy) has full access. `*` grants everything and cannot be combined with other scopes. A key can only create keys within its own scopes. Full list: [API key scopes](./scopes.md). - **Tokens**: `loch_` followed by 64 hexadecimal characters, shown once, valid up to 365 days, at most 20 keys per user. - **CSRF**: write requests from a browser must carry the `Origin` of `FRONT_URL`, otherwise `403 FORBIDDEN_ORIGIN`. Clients without a browser are not concerned. - **Rate limit**: 120 requests per minute per IP. The IP is read from `X-Forwarded-For` only behind a proxy listed in `TRUSTED_PROXIES`. ### Errors Errors have the form: ```json {"code": "INSUFFICIENT_SCOPE", "error": "Insufficient API key scope"} ``` `code` is stable and translated by the interface; `error` is a readable fallback. | Status | Code | Meaning | |---|---|---| | 400 | `INVALID_REQUEST_BODY` | Malformed body; strict bodies also refuse unknown fields | | 401 | `UNAUTHORIZED` | No session, or unknown API key | | 401 | `API_KEY_EXPIRED` | The API key has expired | | 403 | `FORBIDDEN` | Invalid session, or not allowed (not admin, container not granted) | | 403 | `INSUFFICIENT_SCOPE` | The API key lacks the scope | | 403 | `FORBIDDEN_ORIGIN` | Browser write from another origin | | 429 | — | Rate limit, plain text answer | | 500 | `INTERNAL_ERROR` | Server error | ### Routes In the tables, **Scope** is the API key scope checked by the route; "—" means only the profile is checked. **Admin** means the route is reserved for admins. #### Public | Method | Path | Description | |---|---|---| | GET | `/_health` | Health check, includes the Docker status | | GET | `/openid-connect/login` | Start an OIDC login | | GET | `/openid-connect/redirect` | OIDC callback | | GET | `/openid-connect/logout` | Log out | #### Containers and stacks | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/docker` | `containers:read` | | Containers, filtered by permissions | | GET | `/docker/{id}` | `containers:read` | | Container details | | POST | `/docker/{id}/{action}` | `containers:actions` | | `start`, `stop`, `kill`, `restart`, `delete` | | POST | `/stacks/{name}/{action}` | `containers:actions` | | `start`, `stop`, `restart` every accessible container of a compose stack, in `depends_on` order | | GET | `/logs/{id}` | `containers:logs` | | Logs (text) | | GET | `/logs/{id}/json` | `containers:logs` | | Logs (JSON) | | GET | `/logs/{id}/file?path=&tail=` | `containers:logs` | | Last lines of a file inside the container | | GET | `/docker/{id}/cp?path=` | `containers:cp` | | Download a file or directory (tar stream) | | PUT | `/docker/{id}/cp?path=` | `containers:cp` | | Upload a file or directory (tar stream) | | GET | `/ws` | per event | | WebSocket, see [below](#websocket) | #### Domains | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/domains` | — | Yes | Domains routed by Traefik | #### Scheduled tasks | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/cron` | `cron` | | Tasks | | POST | `/cron` | `cron` | | Create | | GET | `/cron/{id}` | `cron` | | Details | | PUT / PATCH | `/cron/{id}` | `cron` | | Update / partial update | | DELETE | `/cron/{id}` | `cron` | | Delete | | POST | `/cron/{id}/run` | `cron` | | Run now (202) | Admins see every task. Restricted users see, change and delete every HTTP task and the Docker tasks of their containers; they can only create Docker tasks on those containers. Task types: `docker_action`, `docker_exec`, `http` and `function_invoke`. A `function_invoke` task (`functionInvoke: {function, event}`, event at most 64 KiB, `{}` by default) is admin only; creating, changing or running one also needs `functions:invoke`. At most 5 tasks per function. Every task carries `lastSkipped` (a run skipped because the previous one was still running) and `disabledReason` (`FUNCTION_DELETED` when the panel disabled it). #### Users and API keys | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/users/me` | — | | Current user | | GET | `/users/me/api-keys` | — | | Own keys, never the token | | POST | `/users/me/api-keys` | — | | Create a key; the token is returned once | | DELETE | `/users/me/api-keys/{id}` | — | | Revoke a key | | GET | `/users` | — | Yes | Users | | PATCH | `/users/{email}/switch` | — | Yes | Toggle admin / restricted | | PATCH | `/users/{email}/restrict` | — | Yes | Make restricted | | PATCH | `/users/{email}` | — | Yes | Update the container permissions | | DELETE | `/users/{email}` | — | Yes | Delete, and revoke the user's keys | #### SFTP users Available when `SFTP_ENABLED` is set (`/sftp/config` always answers). | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/sftp/config` | — | Yes | Plugin status and connection details | | GET | `/sftp/sites` | `sftp:read` | Yes | Sites | | GET | `/sftp/users` | `sftp:read` | Yes | Users | | POST | `/sftp/users` | `sftp:write` | Yes | Create | | PUT | `/sftp/users/{username}` | `sftp:write` | Yes | Update | | DELETE | `/sftp/users/{username}` | `sftp:write` | Yes | Delete | | POST | `/sftp/users/{username}/password` | `sftp:write` | Yes | New password | | POST | `/sftp/users/{username}/status` | `sftp:write` | Yes | Enable / disable | #### Web hosting Available when `HOSTING_ENABLED` is set (`/hosting/config` always answers). | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/hosting/config` | — | Yes | Plugin status, templates, PHP versions, defaults, limits, `defaultNetworks` | | GET | `/hosting/sites` | `hosting:read` | Yes | Site directories and the hosting using each | | GET | `/hosting/php-modules?template=&version=` | `hosting:read` | Yes | PHP modules of an image | | GET | `/hosting/routing-template` | `hosting:read` | Yes | Routing template | | GET | `/hosting/stacks` | `hosting:read` | Yes | Sites with their status | | GET | `/hosting/stacks/check-name?name=` | `hosting:read` | Yes | Name availability | | POST | `/hosting/stacks` | `hosting:write` | Yes | Create and deploy (202, asynchronous) | | GET | `/hosting/stacks/{name}` | `hosting:read` | Yes | Settings, containers, last operation. Without `hosting:write`, environment values and snippets are masked | | PUT | `/hosting/stacks/{name}[?force=true]` | `hosting:write` | Yes | Update and redeploy, rolled back on failure (202) | | POST | `/hosting/stacks/{name}/deploy[?pull=true][&force=true]` | `hosting:write` | Yes | Redeploy (202); `force` overwrites changes made outside the panel | | DELETE | `/hosting/stacks/{name}[?purgeData=true]` | `hosting:write` | Yes | Delete; site data kept unless `purgeData` (202) | #### Managed services Available when `SERVICES_ENABLED` is set (`/services/config` always answers). | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/services/config` | `services:read` | Yes | Module status, engines, versions, options, exposure policy, version discovery state | | GET | `/services/instances` | `services:read` | Yes | Instances with state, networks, exposure, end of support, available updates | | GET | `/services/instances/{instance}/resources` | `services:read` | Yes | Resources of an instance | | POST | `/services/resources` | `services:write` | Yes | Create `{engine, version, name, options, legacyAuth, networks?}`; returns the credentials once | | POST | `/services/instances/{instance}/resources/{name}/password` | `services:write` | Yes | New password, returned once | | DELETE | `/services/instances/{instance}/resources/{name}` | `services:write` | Yes | Delete a resource (a dedicated instance is deleted with it) | | PATCH | `/services/instances/{instance}` | `services:write` | Yes | Change the mutable options of a dedicated instance `{options}` | | POST | `/services/instances/{instance}/update` | `services:write` | Yes | Update a dedicated instance `{version?}`; returns `{updated, version, image}` | | POST | `/services/updates/check` | `services:write` | Yes | Discover versions and updates now, at most once per 30 seconds | | PUT | `/services/instances/{instance}/networks` | `networks:write` | Yes | Replace the networks `{networks}`; the container is recreated | | PUT | `/services/instances/{instance}/exposure` | `services:expose` | Yes | Publish `{address, port, tlsOptional?}` | | DELETE | `/services/instances/{instance}/exposure` | `services:expose` | Yes | Close the port | | DELETE | `/services/instances/{instance}[?purge=true]` | `services:write` | Yes | Remove an empty shared instance; `purge` deletes an instance edited by hand, with its data | `services:expose` is not implied by `services:write`. #### Private networks Bodies are strict: unknown fields are refused (`INVALID_REQUEST_BODY`). | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/networks` | `networks:read` | Yes | Private networks, the system network `services` first | | GET | `/networks/config` | `networks:read` | Yes | Creation rules | | GET | `/networks/docker` | `networks:read` | Yes | Docker networks the panel did not create (read-only) | | GET | `/networks/docker/{id}` | `networks:read` | Yes | One of them, with its members | | GET | `/networks/{name}` | `networks:read` | Yes | A private network with its members | | POST | `/networks` | `networks:write` | Yes | Create `{name, description?, subnet?}` (201) | | PATCH | `/networks/{name}` | `networks:write` | Yes | Change `{description}` | | DELETE | `/networks/{name}` | `networks:write` | Yes | Delete (204) | | POST | `/networks/{name}/members` | `networks:write` | Yes | Attach `{kind: "site" \| "instance" \| "function", name}`; a function also needs `functions:deploy` | | DELETE | `/networks/{name}/members/{kind}/{member}` | `networks:write` | Yes | Detach; a function also needs `functions:deploy` | | POST | `/networks/{name}/disconnect` | `networks:write` | Yes | Disconnect a container the panel does not manage `{containerId}` (204) | #### Functions (work in progress) Available when `FUNCTIONS_ENABLED` is set (`/functions/config` always answers). Bodies are strict. Creating with `env`, `networks`, `egress`, `sigv4` or `mcpInvocable`, or changing `env`, `handler` or `mcpInvocable`, needs `functions:deploy` on top of `functions:write`. | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/functions/config` | `functions:read` | Yes | Module status, runtimes and versions, runtime binary state (`runtime`, `runtimeReady`), limits, budget used, defaults | | GET | `/functions` | `functions:read` | Yes | Functions with their state | | POST | `/functions` | `functions:write` | Yes | Create `{name, runtime, runtimeVersion, handler, memoryMB?, cpus?, timeoutSeconds?, tmpSizeMB?, env?, networks?, egress?, sigv4?, mcpInvocable?}` (201); returns the invoke credentials once | | GET | `/functions/{name}` | `functions:read` | Yes | Details: image, variables, secret names, deployments, endpoint; the container is verified | | PATCH | `/functions/{name}` | `functions:write` | Yes | Change `{memoryMB?, cpus?, timeoutSeconds?, tmpSizeMB?, handler?, env?, mcpInvocable?}`; a running function is recreated | | DELETE | `/functions/{name}[?purge=true]` | `functions:write` | Yes | Delete; `purge` removes a function edited outside the panel without running its files | | POST | `/functions/{name}/code` | `functions:deploy` | Yes | Deploy a zip (`application/zip` body or `multipart/form-data` part `code`), activated at once (201) | | GET | `/functions/{name}/deployments` | `functions:read` | Yes | Kept deployments, newest first | | POST | `/functions/{name}/deployments/{id}/activate` | `functions:deploy` | Yes | Activate a kept deployment (204) | | PUT | `/functions/{name}/secrets/{key}` | `functions:deploy` | Yes | Set a secret `{value}` (204); never returned | | DELETE | `/functions/{name}/secrets/{key}` | `functions:deploy` | Yes | Remove a secret (204) | | POST | `/functions/{name}/credentials/rotate` | `functions:deploy` | Yes | New invoke credentials, returned once; optional `{sigv4}` | | POST | `/functions/{name}/invoke` | `functions:invoke` | Yes | Invoke with the body as event (one JSON value, at most 6 MiB); always 200 with `{statusCode, functionError?, requestId?, durationMs, payload? \| payloadBase64?, logTail}` | | POST | `/functions/{name}/{action}` | `functions:write` | Yes | `start`, `stop`, `restart` (204) | | PUT | `/functions/{name}/networks` | `functions:deploy` | Yes | Replace the networks `{networks}` (never `services`); the container is recreated | | PUT | `/functions/{name}/egress` | `functions:deploy` | Yes | Internet access `{enabled}` | What a function returns is untrusted: render it as text. Function containers are protected in the container routes: their environment is masked, and shell, file copy, file tail, port tunnel, container and stack actions and cron Docker tasks answer `403 FUNCTION_CONTAINER_PROTECTED`. Their stdout logs stay readable by admins. #### Audit trail | Method | Path | Scope | Admin | Description | |---|---|---|---|---| | GET | `/audit?from=&to=&user=&event=&exclude=&level=&q=&before=&limit=` | `audit:read` | Yes | Entries, newest first; `limit` up to 500 | One request reads at most 64 MB of the log, so a page can be shorter than `limit`, even empty. Continue while the response has a `next` cursor, passed as `before`. ### WebSocket Connect to `/ws` with the session cookie or the `X-API-Key` header. Messages are JSON `{"event": ..., "data": ...}`. The scope is checked on the events that open a session; a missing scope answers with the matching error event (`error`, `logsError`, `statsError`, `proxyError`, `streamError`). | Client event | Scope | Data | |---|---|---| | `attach` | `containers:shell` | `{"id": "container-id"}` | | `streamInput` | — (after `attach`) | `"command"` | | `resize` | — (after `attach`) | `{"cols": 120, "rows": 40}` | | `logsStart` | `containers:logs` | `{"id": "container-id", "tail": 100, "file": "/optional/path"}` | | `logsStop` | — | — | | `statsStart` | `containers:stats` | `{"ids": ["container-id-1", "container-id-2"]}` | | `statsStop` | — | — | | `proxyOpen` | `containers:proxy` | `{"id": "container-id", "mappings": [{"remote": 5432, "local": 15432}]}` | | `streamOpen` | `containers:proxy` | `{"streamID": 1, "remotePort": 5432}` | | `streamClose` | — | `{"streamID": 1}` | Server events: `attach`, `stdout`, `stderr`, `end`, `logsData`, `logsEnd`, `logsError`, `statsData`, `statsError`, `proxyReady`, `streamOpened`, `streamClose`, `streamError`, `proxyError`, `error`. | Limit | Value | |---|---| | Port proxy data | Binary messages `[streamID: uint32 big endian][payload]` | | Port proxy | At most 10 mappings per session and 256 concurrent streams | | Stats | Collected every 2 seconds, at most 20 containers per subscription, one `statsStart` every 3 seconds at most | --- ## Configuration reference Every environment variable read by the API. The API also loads a `.env` file from its working directory; variables already set in the environment win. For the essentials, read [Configuration](../admin/configuration.md). Every variable is checked at startup: a missing required variable, an invalid value, an out-of-range number, an unknown variable with a known prefix (`OIDC_`, `SFTP_`, `HOSTING_`, `SERVICES_`, `FUNCTIONS_`, `NETWORKS_`, `CRON_`, `COOKIE_`, `KEYCLOAK_`, `TRUSTED_`) or a removed variable stops the API, with every problem listed. Booleans accept `true`, `1`, `yes`, `on` and `false`, `0`, `no`, `off`. Lists are comma-separated. ### Core {#core} | Variable | Default | Description | |---|---|---| | `FRONT_URL` | required | URL of the web interface: the only origin allowed by CORS, the CSRF check and the WebSocket. Users are sent there after login | | `COOKIE_SECRET` | required | Secret signing the session cookies, at least 32 characters | | `PASSWORD_SECRET` | required | Must be set; not used by the current version | | `COOKIE_DOMAIN` | host of the API | Domain of the session cookies | | `APP_ENV` | `production` | `development` enables debug logs | | `PORT` | `2538` | Listen port (1 to 65535) | | `DATA_DIR` | `./data` | Users, API keys, scheduled tasks, networks registry (JSON files). `/app/data` in the image | | `API_LOG_DIR` | `./log` | Directory of `audit.log`. `/app/log` in the image: mount it to keep the trail | | `TRUSTED_PROXIES` | empty (loopback only) | Reverse proxies allowed to set `X-Forwarded-For`: CIDRs or addresses. The loopback is always trusted. A range wider than /8 (IPv4) or /32 (IPv6) is refused | ### Authentication (OIDC) {#oidc} | Variable | Default | Description | |---|---|---| | `OIDC_PROVIDER` | `microsoft` | `microsoft`, `keycloak` or `generic` | | `CLIENT_ID` | required | OAuth 2.0 client ID | | `CLIENT_SECRET` | required | OAuth 2.0 client secret | | `REDIRECT_URI` | required | Public URL of the API; the callback is `/openid-connect/redirect` | | `SCOPE` | required | OAuth scopes, for example `openid email profile` | | `TENANT_ID` | required with `microsoft` | Microsoft Entra ID tenant | | `APPLICATION_ID` | required with `microsoft` | Microsoft Entra ID application | | `KEYCLOAK_URL` | required with `keycloak` | Keycloak base URL | | `KEYCLOAK_REALM` | required with `keycloak` | Keycloak realm | | `OIDC_DISCOVERY_URL` | required with `generic` | Discovery URL (`.well-known/openid-configuration`) of any standard OIDC provider | ### Scheduled tasks {#cron} | Variable | Default | Description | |---|---|---| | `CRON_HTTP_TIMEOUT` | `900` | Default and maximum duration of an HTTP scheduled task, in seconds (1 to 86400) | | `CRON_MAX_JOBS` | `50` | Maximum number of scheduled tasks (1 to 10000) | ### Private networks {#networks} | Variable | Default | Description | |---|---|---| | `NETWORKS_MAX` | `50` | Maximum number of private networks, the system network not counted (1 to 500) | | `NETWORKS_RESERVED_RANGES` | empty | IPv4 ranges (at least /8) used outside Docker (LAN, VPN, static routes) that no private network may overlap | See [Private networks](../admin/networks.md). ### Websites (hosting) {#hosting} | Variable | Default | Description | |---|---|---| | `HOSTING_ENABLED` | `false` | Enables the websites module | | `HOSTING_STACKS_DIRECTORY` | `/data/compose` | Compose projects directory (may be shared with CI stacks). Also used by the services module. Same path on the host and in the API container | | `HOSTING_SITES_DIRECTORY` | `/data/websites/hosted` | Site data directory. Same path on the host and in the API container | | `HOSTING_TRAEFIK_NETWORK` | required when enabled | External Docker network joined by the web services | | `HOSTING_TRAEFIK_HTTP_ENTRYPOINT` | `web` | Traefik HTTP entrypoint | | `HOSTING_TRAEFIK_HTTPS_ENTRYPOINT` | `websecure` | Traefik HTTPS entrypoint | | `HOSTING_TRAEFIK_CERTRESOLVER` | `letsencrypt` | Traefik certificate resolver (empty: default certificate) | | `HOSTING_RUN_UID` | `1000` | Owner of the site directories created by the panel | | `HOSTING_RUN_GID` | `1000` | Group of the site directories created by the panel | | `HOSTING_DEFAULT_MEMORY_MB` | `0` | Default memory limit of a site, in MB (`0`: unlimited, otherwise at least 64) | | `HOSTING_MAX_MEMORY_MB` | `0` | Highest memory limit a site may request, in MB (`0`: the host memory, otherwise at least 64; capped to the host) | | `HOSTING_DEFAULT_CPUS` | `0` | Default CPU limit of a site (`0`: unlimited, otherwise at least 0.1) | | `HOSTING_MAX_CPUS` | `0` | Highest CPU limit a site may request (`0`: the host CPUs, otherwise at least 0.1; capped to the host) | A default above its maximum stops the API. See [Websites](../admin/hosting.md). ### SFTP {#sftp} | Variable | Default | Description | |---|---|---| | `SFTP_ENABLED` | `false` | Enables the SFTP module (SFTPGo) | | `SFTP_DIRECTORY` | required when enabled | Base directory of the sites | | `SFTP_API_URL` | required when enabled | Internal URL of the SFTPGo admin API, for example `http://sftpgo:8080` | | `SFTP_API_KEY` | required when enabled | SFTPGo admin API key | | `SFTP_DOMAIN` | empty | SFTP host shown to users | | `SFTP_PORT` | empty | SFTP port shown to users | | `SFTP_WEB_USER_INTERFACE` | empty | URL of the SFTPGo web client shown to users | | `SFTP_INIT_DIRS` | `web,logs` | Directories created in the home of a new user when requested (empty: none) | See [SFTP](../admin/sftp.md). ### Managed services {#services} | Variable | Default | Description | |---|---|---| | `SERVICES_ENABLED` | `false` | Enables the managed services module | | `SERVICES_DATA_DIRECTORY` | `/data/services` | Data of the instances. Absolute path, same on the host and in the API container, outside the stacks, sites and SFTP directories | | `SERVICES_NETWORK` | `loch-services` | Docker name of the system network `services`, shared by the instances and the sites (created `internal` if missing). Not `bridge`, `host`, `none`, `default`, the Traefik network nor a `loch-net-*` name | | `SERVICES_ENGINES` | empty (all) | Enabled engines: `postgres`, `mariadb`, `mysql`, `mongodb`, `redis`, `valkey`, `rabbitmq`, `nats`, `s3`, `elasticmq`, `mosquitto` | | `SERVICES__PINNED_VERSIONS` | empty | Versions always offered at creation, even out of support, for example `SERVICES_MYSQL_PINNED_VERSIONS=5.6`. One variable per engine | | `SERVICES_UPDATE_CHECK_INTERVAL` | `12` | Hours between two discoveries of versions and updates (0 to 168; `0`: on demand only) | | `SERVICES_MAX_RESOURCES_PER_INSTANCE` | `50` | Resources per shared instance (1 to 500) | | `SERVICES_MAX_DEDICATED_INSTANCES` | `20` | Dedicated instances per engine (1 to 200) | | `SERVICES_ARCHIVE_ON_RETIRE` | `false` | Rename the data of a removed instance instead of deleting it | | `SERVICES_EXPOSE_ALLOWED_CIDRS` | `127.0.0.1` | Host addresses a port may be published on: CIDRs or addresses. `0.0.0.0` must be listed alone | | `SERVICES_ALLOW_PUBLIC_BIND` | `false` | Required for any non-loopback address in `SERVICES_EXPOSE_ALLOWED_CIDRS` | | `SERVICES_TLS` | `auto` | `auto`: TLS required once an instance is published outside the loopback; `off`: TLS disabled | | `SERVICES_ALLOW_TLS_OPTIONAL` | `false` | Lets an exposure offer TLS without requiring it (PostgreSQL, MySQL 8.4, MariaDB) | | `SERVICES_PUBLIC_HOST` | empty | Host name or IP shown to clients and covered by the certificates (no scheme, no path) | | `SERVICES_TLS_CERT_FILE` | empty | Certificate to use instead of self-signed ones. Set together with the key | | `SERVICES_TLS_KEY_FILE` | empty | Key of that certificate | | `SERVICES_S3_ADMIN_API_KEY` | empty | Empty: one Loch S3 instance per resource (the only mode available). Set: shared mode, not available yet, the `s3` engine is disabled | Version discovery needs outbound HTTPS to `registry-1.docker.io`, `auth.docker.io`, `ghcr.io`, `mirror.gcr.io` and `endoflife.date`. See [Managed services](../admin/services.md). ### Functions (work in progress) {#functions} | Variable | Default | Description | |---|---|---| | `FUNCTIONS_ENABLED` | `false` | Enables the functions module. Needs `HOSTING_STACKS_DIRECTORY` | | `FUNCTIONS_DATA_DIRECTORY` | `/data/functions` | Code, runtime binaries, secrets and references. Same path on the host and in the API container; letters, digits and `_ . / -` only; outside the stacks, sites, SFTP, services and log directories; a subdirectory of `DATA_DIR` is allowed, not `DATA_DIR` itself | | `FUNCTIONS_MAX` | `50` | Maximum number of functions (1 to 1000) | | `FUNCTIONS_MAX_MEMORY` | `3008` | Highest memory of one function, in MB (128 to 10240) | | `FUNCTIONS_MAX_CODE_SIZE` | `50` | Highest size of an uploaded zip, in MB (1 to 250) | | `FUNCTIONS_DEPLOYMENTS_KEPT` | `3` | Deployments kept per function for a rollback, the active one included (1 to 20) | | `FUNCTIONS_TMP_SIZE` | `512` | Default `/tmp` size of a function, in MB (16 to 10240) | | `FUNCTIONS_MAX_TOTAL_MEMORY` | `0` | Sum of the memory limits of every function, in MB (`0`: the memory of the Docker host) | | `FUNCTIONS_MAX_TOTAL_CPUS` | `0` | Sum of the CPU limits of every function (`0`: 4 × the CPUs of the Docker host) | | `FUNCTIONS_RUNTIME_DOWNLOAD` | `true` | Download the Loch Lambda Runtime binary from GitHub when it is missing; `false` for offline installations | | `FUNCTIONS_RUNTIME_SOURCE_DIR` | empty | Absolute path of a directory holding `loch-lambda-runtime-linux-`, read instead of downloading (wins over `FUNCTIONS_RUNTIME_DOWNLOAD`). It and its files must be owned by the panel's user or root and writable by nobody else | Sizes are in MB and accept a binary unit (`512M`, `1G`). The download needs outbound HTTPS to `github.com`, `release-assets.githubusercontent.com` and `objects.githubusercontent.com`. See [Functions module](../admin/functions.md). ### Removed variables {#removed} The API refuses to start while one of these is set, and says what replaces it. | Variable | Replaced by | |---|---| | `SERVICES_EXPOSED_PORTS`, `SERVICES_PUBLISH_ADDRESS` | Ports chosen in the panel, within `SERVICES_EXPOSE_ALLOWED_CIDRS` | | `SERVICES_TLS_OPTIONAL` | Optional TLS chosen per exposure, allowed by `SERVICES_ALLOW_TLS_OPTIONAL` | | `SERVICES__VERSIONS` | Versions discovered on the registries; `SERVICES__PINNED_VERSIONS` | ### Docker connection {#docker} The API connects to Docker with the standard Docker client environment (`DOCKER_HOST` and related variables). Without them, it uses the local socket `/var/run/docker.sock`. ### Interface {#interface} The web interface container has its own variables (`API_URL`, `WS_URL`, `APP_NAME`, `LOGO_URL`, `FAVICON_URL`, `SUPPORT_PHONE`, `SUPPORT_URL`, `STATUS_URL`): see [Installation](../admin/installation.md#interface-settings). --- ## API key scopes A scope limits what an API key can do. The interface calls them **Permissions**. Browser sessions always have every permission of their user. To create a key, see [API keys](../user-guide/api-keys.md). | Scope | Interface label | Allows | Admin only | |---|---|---|---| | `*` | **All permissions** | Everything the user's profile allows. Cannot be combined with other scopes | No | | `containers:read` | **Read (list, inspect)** | List containers and read their details (`GET /docker`, `GET /docker/{id}`) | No | | `containers:logs` | **Logs (download, streaming)** | Download logs, stream live logs, read a file inside a container (`/logs/**`, WebSocket `logsStart`) | No | | `containers:actions` | **Actions (start, stop, restart, kill, delete)** | Container actions and stack actions (`POST /docker/{id}/{action}`, `POST /stacks/{name}/{action}`) | No | | `containers:shell` | **Shell (remote terminal)** | Open a terminal in a container (WebSocket `attach`) | No | | `containers:stats` | **Stats (real-time monitoring)** | Live CPU, RAM, network and PIDs (WebSocket `statsStart`) | No | | `containers:proxy` | **Proxy (port tunnel)** | Open a port tunnel to a container (WebSocket `proxyOpen` / `streamOpen`) | No | | `containers:cp` | **File copy (cp)** | Download and upload files (`GET` / `PUT /docker/{id}/cp`) | No | | `cron` | **Cron jobs** | Every scheduled task route (`/cron/**`) | No | | `sftp:read` | **SFTP (read)** | Read SFTP accounts and settings | Yes | | `sftp:write` | **SFTP (create, edit, delete)** | Change SFTP accounts | Yes | | `hosting:read` | **Hosting (read)** | Read websites | Yes | | `hosting:write` | **Hosting (create, edit, deploy, delete)** | Change and deploy websites | Yes | | `services:read` | **Databases (read)** | Read managed services | Yes | | `services:write` | **Databases (create, new password, delete)** | Change managed services | Yes | | `services:expose` | **Databases (publish a port)** | Publish and close the port of an instance. Not implied by `services:write` | Yes | | `networks:read` | **Private networks (read)** | Read private networks | Yes | | `networks:write` | **Private networks (create, attach, detach, delete)** | Change private networks, including the networks of a service instance | Yes | | `functions:read` | — | Read functions and the module configuration (`GET /functions/**`) | Yes | | `functions:write` | — | Create, resize, start, stop, restart and delete functions | Yes | | `functions:deploy` | — | Code and deployments, secrets, invoke credentials, variables, handler, networks, Internet access and MCP opt-in of a function | Yes | | `functions:invoke` | — | Invoke a function (`POST /functions/{name}/invoke`), and with `cron`, create, change or run a scheduled function invocation. Implied by no other scope but `*` | Yes | | `audit:read` | **Audit trail (read)** | Read the audit trail (`GET /audit`) | Yes | ### Rules - **Profile first**: a key carries its user's profile. An admin-only scope on a user's key grants nothing. - **Container permissions still apply**: a container scope only reaches the containers the user can access. - **No scope needed**: your own profile and keys (`/users/me`, `/users/me/api-keys`), and the admin routes for users and `/domains`, which check the profile only. - **No escalation**: a key can only create keys within its own scopes. - **Functions (work in progress)**: creating a function with variables, networks, Internet access, SigV4 or the MCP opt-in, or changing its variables, handler or MCP opt-in, needs both `functions:write` and `functions:deploy`. - **WebSocket stops**: `logsStop` and `statsStop` need no scope: they only stop a stream the connection opened. - **Validation**: at least one scope is required, unknown scopes are refused. Keys created before scopes existed have no scope and keep full access. ### In the interface The **Permissions** list of the **New key** form only shows the scopes that apply: | Scopes | Shown when | |---|---| | `sftp:*`, `hosting:*`, `services:*` | You are an admin and the module is enabled (`SFTP_ENABLED`, `HOSTING_ENABLED`, `SERVICES_ENABLED`) | | `networks:*`, `audit:read` | You are an admin | The `functions:*` scopes are not in the form yet ("—" above): a key with **All permissions** covers them, or create the key through the API (`POST /users/me/api-keys`). --- ## Services engines Full catalog of the [managed services](../admin/services.md) module. Run `loch services engines` to see what your server offers right now. ### Engines | Engine | ID | Menu | Topology | Port | Built-in versions | Oldest supported | |---|---|---|---|---|---|---| | PostgreSQL | `postgres` | Databases | shared | 5432 | 18, 17, 16, 15 | 15 | | MariaDB | `mariadb` | Databases | shared | 3306 | 11.8, 11.4, 10.11 | 10.11 | | MySQL | `mysql` | Databases | shared | 3306 | 8.4 | 5.6 (offered only when pinned) | | MongoDB | `mongodb` | Databases | shared | 27017 | 8.0, 7.0, 6.0 | 6.0 | | RabbitMQ | `rabbitmq` | Messaging | shared | 5672 | 4.2, 4.1, 4.0 | 4.0 | | Redis | `redis` | Cache | dedicated | 6379 | 8.4, 8.2, 7.4 | 7.2 | | Valkey | `valkey` | Cache | dedicated | 6379 | 9.0, 8.1, 7.2 | 7.2 | | NATS | `nats` | Messaging | dedicated | 4222 | 2.12, 2.11, 2.10 | 2.10 | | ElasticMQ (SQS-compatible) | `elasticmq` | Queues | dedicated | 9324 | 1.7 | 1.7 | | Eclipse Mosquitto (MQTT) | `mosquitto` | Queues | dedicated | 1883 | 2.0 | 2.0 | | Loch S3 (S3-compatible) | `s3` | Storage | dedicated | 8080 | 1.4 | 1.4 | - **Shared**: one instance per version; a resource is a database and its user (a virtual host and its user for RabbitMQ). - **Dedicated**: one container per resource, named and sized at creation. - **Built-in versions** are used until the first version discovery succeeds. After that, creation offers the latest supported version of each of the 3 latest majors, plus the versions pinned with `SERVICES__PINNED_VERSIONS`. - Existing instances stay managed whatever their version, from the oldest supported one. ### Images and version sources | Engine | Tags listed on | Pulled from | Support dates | |---|---|---|---| | PostgreSQL | `docker.io/library/postgres` (`17`) | `mirror.gcr.io` | endoflife.date | | MariaDB | `docker.io/library/mariadb` (`11.4`) | `mirror.gcr.io` | endoflife.date | | MySQL | `docker.io/library/mysql` (`8.4`) | `mirror.gcr.io` | endoflife.date | | MongoDB | `docker.io/library/mongo` (`8.0`) | `mirror.gcr.io` | endoflife.date | | RabbitMQ | `docker.io/library/rabbitmq` (`4.2`) | `mirror.gcr.io` | endoflife.date | | Redis | `docker.io/library/redis` (`8.4-alpine`) | `mirror.gcr.io` | endoflife.date | | Valkey | `docker.io/valkey/valkey` (`9.0-alpine`) | `mirror.gcr.io` | endoflife.date | | NATS | `docker.io/library/nats` (`2.12-alpine`) | `mirror.gcr.io` | none: published means supported | | ElasticMQ | `docker.io/softwaremill/elasticmq-native` (`1.7.1`) | `mirror.gcr.io` | none | | Mosquitto | `docker.io/library/eclipse-mosquitto` (`2.0.22`) | `mirror.gcr.io` | none | | Loch S3 | `ghcr.io/loch-sh/s3` (`1.4.0`) | `ghcr.io` | none | ElasticMQ, Mosquitto and Loch S3 use exact patch tags: a version runs its highest patch tag. ### Options of dedicated engines Pass them with `--option key=value` in the CLI. "Changeable" means editable later (**Edit** button, `loch services update`); the others are set at creation. | Engine | Option | Allowed values | Default | Changeable | |---|---|---|---|---| | Redis, Valkey | `memory` (container MB; data gets half) | 128, 256, 512, 1024, 2048, 4096 | 256 | yes | | | `policy` (when memory is full) | `noeviction`, `allkeys-lru`, `allkeys-lfu`, `volatile-lru`, `volatile-lfu`, `volatile-ttl` | `noeviction` | yes | | | `persistence` | `aof`, `rdb`, `none` | `aof` | no | | | `scripting` (Lua) | `on`, `off` | `on` | yes | | Redis only | `modules` (JSON, search, time series…; Redis 8 and later) | `off`, `on` | `off` | yes | | NATS | `memory` (container MB; in-memory streams get a quarter) | 128, 256, 512, 1024 | 256 | yes | | | `storage` (JetStream disk, MB) | 1024, 5120, 10240, 51200 | 1024 | yes | | | `payload` (largest message, MB) | 1, 8, 64 | 1 | yes | | ElasticMQ | `memory` (container MB) | 128, 256, 512, 1024 | 256 | yes | | | `limits` (`strict`: Amazon SQS limits; `relaxed`: larger messages, longer delays) | `strict`, `relaxed` | `strict` | yes | | Mosquitto | `memory` (container MB) | 64, 128, 256, 512 | 128 | yes | | | `persistence` (retained messages, persistent sessions) | `on`, `off` | `on` | yes | | | `message-size` (largest message, MB) | 1, 10, 100 | 1 | yes | | | `queued` (messages kept per offline or slow client) | 1000, 10000, 100000 | 1000 | yes | | Loch S3 | `memory` (container MB) | 128, 256, 512, 1024 | 256 | yes | | | `quota` (MB) | 1024, 5120, 10240, 51200, 102400, 512000 | 10240 | yes | | | `encryption` (SSE-S3 on every object; Loch S3 1.4 and later) | `off`, `on` | `off` | no | #### Custom sizes Besides the listed values, these options accept any multiple of the step within the range (**Custom** card in GB, `--option memory=3G` in the CLI): | Engine | Option | Range | Step | |---|---|---|---| | Redis, Valkey | `memory` | 128 MB to 64 GB | 128 MB | | NATS | `memory` | 128 MB to 16 GB | 128 MB | | | `storage` | 1 GB to 1 TB | 1 GB | | ElasticMQ | `memory` | 128 MB to 8 GB | 128 MB | | Mosquitto | `memory` | 64 MB to 4 GB | 64 MB | | Loch S3 | `memory` | 128 MB to 8 GB | 128 MB | | | `quota` | 1 GB to 10 TB | 1 GB | ### Shared engines Shared engines have no options. One setting is chosen per database: | Setting | Engines | Effect | |---|---|---| | **Legacy authentication** (`--legacy-auth`) | MySQL 8.4 to 8.x, PostgreSQL | `mysql_native_password` (MySQL) or `md5` (PostgreSQL) for old clients. Fixed at creation | ### Names and limits | Item | Rule | |---|---| | Shared resource name | `[a-z][a-z0-9_]`, 32 characters max (16 on MySQL 5.6) | | Dedicated instance name | `[a-z][a-z0-9]`, 24 characters max (it becomes a host name) | | Reserved names (all engines) | `admin`, `all`, `default`, `healthcheck`, `loch`, `none`, `root`, `test`, `user`, and the `loch_` prefix | | Resources per shared instance | `SERVICES_MAX_RESOURCES_PER_INSTANCE` (default 50) | | Dedicated instances per engine | `SERVICES_MAX_DEDICATED_INSTANCES` (default 20) | ### Engine notes | Engine | Note | |---|---| | ElasticMQ | No authentication. Refused on the `services` network, loopback-only when published | | MySQL 5.6, MongoDB 6.0 | Out of support: loopback-only when published | | MongoDB | Cannot require TLS: loopback-only when published while `SERVICES_TLS=auto` | | Loch S3 | Path-style URLs only; no TLS (publishing on a public address requires `SERVICES_TLS=off`). `SERVICES_S3_ADMIN_API_KEY` (shared mode) is not available yet and disables the engine | | Mosquitto | One MQTT user, no anonymous access | --- ## API keys An API key lets the [`loch` CLI](/cli/), its MCP server or a script act on the panel in your name. Every user manages their own keys on the **API keys** page (menu **Administration**). ### Create a key 1. Click **New key**. 2. Fill in the form: | Field | Value | |---|---| | **Name** | A name to recognize the key, for example `CLI MacBook Pro` | | **Validity period (days)** | From 1 to 365 days (90 by default) | | **Permissions** | **All permissions**, or only the ones the key needs | 3. Click **Create**. 4. Copy the key, then click **I have copied my key**. :::warning The key is shown only once. If you lose it, revoke it and create a new one. ::: The dialog also shows the commands to configure the CLI: ```bash loch config my-server --url https://panel-api.example.com loch login ``` Paste the key when `loch login` asks for it. See [Install the CLI](/cli/install/). ### Permissions A key never grants more than your profile: an administrator permission on a user's key gives nothing. Choose the narrowest set, for example **Read (list, inspect)** and **Logs (download, streaming)** for a monitoring script. Full list: [API key scopes](../reference/scopes.md). ### The key list | Column | Content | |---|---| | **Prefix** | The first characters of the key, to identify it | | **Permissions** | The granted permissions, or **All** | | **Expires on** | Expiry date; **(expired)** once it has passed | | **Last used** | Last request made with the key | To revoke a key, click **Revoke** on its row and confirm. This cannot be undone. ### Good to know - 20 keys at most per user. - A key created with a limited key cannot have more permissions than that key. - Deleting your user revokes all your keys. - Store the key like a password: anyone who has it acts as you. --- ## Manage containers The **Containers** page (menu **Compute**) lists the containers you can access, grouped by compose stack. Every user can use it, on the containers granted to them. ### Find a container | Control | Effect | |---|---| | **Search a container, a stack...** | Filters by container or stack name | | **Stack type** | **All**, **Hosting**, **Services** or **Other** | | **Active** / **All** | Hides or shows the stacks with no running container | | **Errors** | Shows only the stacks with a container that exited abnormally | Click a stack row to expand it and see its containers. Containers without a compose project are grouped under **No stack**. ### Container actions The **Actions** column shows the buttons that match the container's state. | Button | Shown when | Confirmation | |---|---|---| | **Start** | Not running | No | | **Restart** | Running | No | | **Stop** | Not exited | Yes | | **Kill** | Not exited | Yes: stops at once, without a clean shutdown | | **Delete** | Exited | Yes | | **Detached terminal** | Running | No: opens a terminal in a popup window | | **Details and terminal** | Always | No: opens the details panel | To act on every container of a stack at once, see [Stack actions](./stacks.md). ### Details, logs and terminal Click **Details and terminal** to open the **Container details** panel. | Section | Content | |---|---| | Details | Compose project, identifier, image, state, start date, restart count, network, mount points | | **Log path** | **Download** the full container log | | **Live logs** | **Show logs** streams the last 100 lines, then new ones | | **Interactive Docker terminal** | **Open a terminal** starts `/bin/sh` in the container; **Detach** moves it to a popup | **Full screen** widens the panel. **Console only** hides the details and keeps the terminal. ### Resource usage In an expanded stack, click **Monitoring** to show live CPU, RAM, network and PIDs for its containers. Click **Close monitoring** to stop. ### Good to know - The terminal needs a TTY. If the panel says it is not available, add `tty: true` to the service in the compose file, or start the container with `-t`. - Containers marked as system containers are hidden from everyone, and nobody can open a shell in the panel's own API container. - Function containers (`fn-*`) are managed from **Serverless → Functions**: shell, file copy and actions are refused on them. - A user profile sees only the containers listed in its permissions; ask your administrator for more. - The same actions exist in the [`loch` CLI](/cli/): `loch ps`, `loch logs`, `loch shell`, `loch stats`, `loch inspect`, `loch start` / `stop` / `restart`. The CLI also copies files (`loch cp`) and opens port tunnels (`loch proxy`). --- ## Functions :::caution Work in progress Functions are a work in progress: the API, the interface and the CLI exist, but the feature can still change. Runtime updates, external secret stores, public HTTP access and a per-function disk quota are not available yet. ::: A function runs a handler written for AWS Lambda™ (Node.js, Python, or a compiled `bootstrap` for Go, Rust…) in its own container on the server, started by [Loch Lambda Runtime](/lambda-runtime/). The same code also runs on AWS Lambda without changes. :::note Admin only. The administrator enables the module first: see [Functions module](../admin/functions.md). The page is **Serverless → Functions**. ::: ### Create a function 1. Click **New function**. 2. Fill in the sections: | Section | What you choose | |---|---| | **Name** | 1 to 21 lowercase letters, digits or dashes, starting with a letter. It is also the host name `fn-`. Cannot be changed | | **Runtime** | **Node.js**, **Python** or **Go, Rust… (custom runtime)**, and the **Version** | | **Code** | The **Handler** and, optionally, the **Code (zip)** to deploy right away | | **Resources** | **Memory**, **CPU**, **Timeout**, **/tmp size** | | **Variables and secrets** | **Variables** (written in the compose file) and **Secrets** (kept apart, never shown again) | | **Network access** | The **Dedicated network** `fn-`, **Other private networks**, **Internet access** | | **Invocation** | **SigV4 callers (AWS SDK)**, on top of the bearer token | 3. Click **Create the function**. 4. Copy the invoke credentials, then click **I have copied the credentials**. They are shown once. Nothing runs until code is deployed. Every CLI flag: [CLI functions guide](/cli/guides/functions/). #### Handler | Runtime | Handler | Example | |---|---|---| | Node.js | `.` | `index.handler` (exported by `index.js` or `index.mjs`) | | Python | `.` | `lambda_function.lambda_handler`, `app.main.handler` | | Custom runtime | Optional, passed to the bootstrap as `_HANDLER` | Empty: `bootstrap` | #### Resources | Setting | Range | Default | |---|---|---| | **Memory** | 128 MB to the server maximum | 512 MB | | **CPU** | 0.1 to 16, in steps of 0.01 | 1 | | **Timeout** | 1 to 900 seconds per invocation | 30 s | | **/tmp size** | 16 MB to 10 GB | Set by the server (512 MB by default) | Files in `/tmp` count against the memory limit. The list page shows how much of the server budget the functions already use. ### Deploy code The code is a zip with the handler and its dependencies at its root (`node_modules`, packages installed with `pip install -t .`). Nothing is built on the server. 1. Open the function, tab **Code**. 2. Drop the zip under **Deploy new code**. The zip becomes a new deployment, active at once. If it does not start, the previous one is started again. The **Deployments** table keeps the last ones (3 by default): click **Roll back** (or **Activate**) to go back to one. For the custom runtime, the zip holds an executable named `bootstrap` at its root, built for Linux and the server's architecture. See [Custom runtimes](/lambda-runtime/guides/custom-runtimes/). ### Test In the **Test** tab, type an **Event (JSON)**, then click **Invoke**. The **Result** shows the status, duration, request ID, the response and the end of the logs. The function must be running. ### Function page tabs | Tab | Use | |---|---| | **Overview** | State, runtime, image, limits, **Internal URL** | | **Code** | Deploy a zip, list, activate or roll back deployments | | **Test** | Invoke with an event | | **Configuration** | Resources, handler and variables; **Invocable by MCP clients** | | **Secrets** | Add, replace or remove a secret. Values are never shown | | **Credentials** | Rotate the invoke credentials, accept or refuse SigV4 callers | | **Networks** | Connect or disconnect private networks, allow **Internet access** | | **Triggers** | Scheduled tasks invoking the function: **Schedule an invocation** | Changing the configuration, a secret, a network or the Internet access recreates the container of a running function: an invocation in progress is cut. A stopped function stays stopped: a new configuration applies at its next start. ### Call a function from a site or a service The function answers only the members of its networks, at its **Internal URL** (`http://fn-:8080/2015-03-31/functions//invocations`), with its credentials: - a bearer token (`Authorization: Bearer `); - with **SigV4 callers (AWS SDK)** on, an access key pair for the AWS SDKs (`endpoint-url` `http://fn-:8080`, region `us-east-1`). The function code can read this key pair. Attach the site or service to the function's network: on its **Networks** tab, or from [Private networks](../admin/networks.md). The panel's own invocations (**Test**, scheduled tasks) need no credential. Request format: [Invoke API](/lambda-runtime/reference/invoke-api/). ### States | State | Meaning | |---|---| | **No code** | Created, no code deployed yet | | **Running** | Answers invocations | | **Stopped** | Start it to invoke it | | **Missing** | Code deployed, no container: start it again | | **Edited outside the panel** | Its compose project was changed by hand: the panel never runs it. **Purge** it | | **Not verified** | A container runs but is not the one the panel created: restart the function | | **Unreadable** | Its reference file is unreadable: **Purge** it | ### Delete Click **Delete** on the function and type its name. The container, code, deployments, secrets, credentials and the network `fn-` are deleted (the network is kept while other members use it). Scheduled tasks invoking it are disabled, not deleted. ### Good to know - With **Internet access**, the code can send its data and secrets anywhere, and reach the host, the LAN and cloud metadata endpoints unless firewall rules block them. It is off by default. - A function never joins the `services` network. It keeps at least one network. - **Invocable by MCP clients** is a guard-rail for AI assistants using `loch mcp`, not an access control: see [MCP tools](/cli/reference/mcp-tools/#functions). - What a function returns or logs is untrusted data: the panel shows it as text only. - Function containers are protected: no shell, file copy or container action from the **Containers** page. Use the function page. --- ## Scheduled tasks The **Scheduled tasks** page (menu **Serverless**) runs jobs on a schedule. Every user can use it. ### Task types | Type | What it does | Fields | |---|---|---| | **Docker action** | Starts, stops, restarts or kills a container | **Container**, **Action** | | **Docker command** | Runs a command in a container | **Container**, **Command**, **Working directory** | | **HTTP** | Sends an HTTP request | **Method**, **URL**, **Request body**, **Timeout (seconds)** | | **Function invocation** | Invokes a [function](./functions.md) with a fixed event. Admin only, when the functions module is enabled | **Function**, **Event (JSON)** | A function invocation keeps the status, duration and response size, never the response itself. The event is at most 64 KB (`{}` when empty), with at most 5 tasks per function. When the function is deleted, its tasks are **Disabled by the panel**: point them at another function or delete them. ### Create a task 1. Click **New task**. 2. Fill in **Name** and **Cron expression**. 3. Choose the **Type** and fill in its fields. 4. Keep **Enabled** on. Turn on **One-time run** to disable the task after its first run. 5. Click **Create**. The cron expression has five fields, `minute hour day month weekday`, or a shortcut: | Expression | Runs | |---|---| | `0 3 * * *` | Every day at 03:00 | | `@every 5m` | Every 5 minutes (60 seconds minimum) | | `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly` | Every hour, day, week, month or year | ### Manage tasks | Button | Effect | |---|---| | **Edit** | Opens the task form; save with **Update** | | **Run now** | Runs the task at once | | **Suspend** / **Enable** | Stops or resumes the schedule | | **Delete** | Deletes the task, after confirmation | Expand a row to see the **Error:** and **Output:** of the last run. A run never overlaps the previous one of the same task: it is skipped, and the row says when the last run was skipped. ### What a user profile can do - Create HTTP tasks, and Docker tasks on the containers granted to you only. - See, edit and delete every HTTP task, whoever created it, and the Docker tasks of your containers. - The error and output of the last run are shown to administrators only. ### Good to know - **Command** is split on spaces and run without a shell: quotes, pipes and `&&` do not work. Put them in a script inside the container and run `sh /path/to/script.sh`. - HTTP tasks cannot reach private or internal addresses. In the form, **Timeout (seconds)** goes from 1 to 300. - The output kept for a run is limited to 10 KB. - Docker commands are refused on the panel's own API container, Docker actions and commands on function containers. - The administrator sets the maximum number of tasks on the server (`CRON_MAX_JOBS`). In the CLI: `loch cron`, see the [`loch` CLI](/cli/). --- ## Sign in and the home page Loch Panel has no password of its own: you sign in with your organization account (single sign-on). ### Sign in 1. Open the panel address given by your administrator. 2. The panel sends you to your organization's sign-in page. Sign in there. 3. You come back to the panel, on the **Home** page. To sign out, click **Log out** at the bottom of the menu. ### Your profile | Profile | What you see | |---|---| | Administrator | Every container, except system ones, and the administration menus | | User | Only the containers your administrator granted you | The first person to sign in becomes administrator. Everyone after is a user with no container until an administrator grants some. ### The home page | Card | Content | |---|---| | **Number of containers** | Containers you can see | | **User profile** / **Administrator profile** | Your profile and the containers you can access | | **Resource overview** | How many containers, stacks, scheduled tasks and API keys you have; admins also see sites, services, functions, private networks, SFTP users and domains of the enabled modules. Click a tile to open its page | | **Latest deployed stacks** | The 5 most recent stacks, with their state | | **Containers that exited with an error** | Up to 5 containers stopped with a non-zero exit code; click one to find it in **Containers** | | **Latest domains** | Admin only: the 5 latest routed domains | | **Technical support** | Admin only, when your organization configured it | | **Service status** | When your organization configured a status page | ### Good to know - **No access has been set up by your administrator yet.** means your profile has no container: ask your administrator. - The menu shows only what your profile and the enabled features allow. - The language selector changes the interface language (10 languages). - A browser session lasts one hour. After that, the panel sends you through the sign-in page again. --- ## Stack actions A stack is a group of containers started by the same compose project. You can start, stop or restart it from its row on the **Containers** page. ### Act on a stack 1. Open **Containers** and find the stack row. 2. In the **Actions** column, click a button: | Button | Shown when | Confirmation | |---|---|---| | **Start the stack** | At least one container is not running | No | | **Stop the stack** | At least one container is running | Yes | | **Restart the stack** | At least one container is running | Yes: its services are briefly unavailable | A message confirms the result. If some containers fail, it says how many: for example **2 of 5 containers could not be processed**. ### How it works - Containers start in dependency order (`depends_on`) and stop in reverse order. Containers at the same level run in parallel. - **Start** skips containers already running; **Stop** skips those already stopped. - One failure does not stop the other containers. - With a user profile, only the containers granted to you are processed. ### Good to know - Containers under **No stack** have no compose project: act on them one by one. - System containers and the panel's own API container are never processed. - In the CLI: `loch stack start`, `loch stack stop` or `loch stack restart`, with the stack name or the active namespace. See the [`loch` CLI](/cli/). --- ## Containers These commands act on the containers of the active namespace (see [Endpoints and namespaces](./endpoints-and-namespaces.md)). Name a container by its Compose service name, e.g. `php`. A restricted user only sees the containers an admin granted. ### Commands | Command | Does | |---|---| | `loch ps` | Lists the containers of the active namespace (`-a`: all namespaces). | | `loch inspect ` | Shows the full details of a container. | | `loch start ` | Starts one or more containers. | | `loch stop ` | Stops one or more containers. | | `loch restart ` | Restarts one or more containers. | | `loch kill ` | Kills one or more containers. | | `loch rm ` | Removes one or more containers. | | `loch stack start\|stop\|restart [stack]` | Acts on every container of a stack, in dependency order (`depends_on`). Default: the active namespace. | | `loch stats [service...]` | Live CPU, memory, network and PID usage. Default: every running container of the namespace. | ```bash loch ps loch inspect php loch restart php worker loch stack restart blog loch stats ``` ### Permissions | Command | Scope | |---|---| | `ps`, `inspect` | `containers:read` | | `start`, `stop`, `restart`, `kill`, `rm`, `stack` | `containers:actions` | | `stats` | `containers:stats` | Commands that take a service name also need `containers:read`, to find the container. ### Good to know - `loch stack` refuses the `default` namespace: it only works on Docker Compose stacks. - Admins can list the Traefik domains with `loch domains`. --- ## Scheduled jobs A scheduled job runs on a cron schedule. It can be a container action (`docker_action`), a command run in a container (`docker_exec`), an HTTP request (`http`) or a function invocation (`function_invoke`, admin only). ### Commands | Command | Does | |---|---| | `loch cron ls` | Lists the jobs, why the panel disabled one and when a run was last skipped. | | `loch cron create` | Creates a job. Asks for what `--name`, `--schedule` and `--function` do not give. | | `loch cron run ` | Runs a job now. | | `loch cron enable ` | Enables a job. | | `loch cron disable ` | Disables a job. | | `loch cron rm ` | Deletes a job. | ```bash loch cron create # Name: nightly-sync # Type (docker_action / docker_exec / http / function_invoke): docker_exec # Schedule (cron expression or @every 5m): 0 3 * * * # Container: myapp-php-1 # Command: php bin/console app:sync loch cron run nightly-sync loch cron create --name nightly-report --schedule '0 3 * * *' --function report --event-json '{"period": "day"}' ``` ### Permissions | Command | Scope | Admin | |---|---|---| | `cron …` | `cron` | No | | `cron create --function`, `cron run` on a `function_invoke` job | `cron` and `functions:invoke` | Yes | A restricted user can only target the containers an admin granted. ### Good to know - The schedule is a 5-field cron expression (`0 3 * * *`) or a descriptor (`@daily`, `@every 5m`). - `docker_action` accepts `start`, `stop`, `restart` and `kill`. - A `function_invoke` job keeps the status, duration and response size, never the response. The event is at most 64 KiB (`--event ` for one holding tokens); at most 5 jobs per function. See [Functions](./functions.md). - To change a job, use the panel or the `cron_update` MCP tool: see [MCP tools](../reference/mcp-tools.md#cron). --- ## Endpoints and namespaces An **endpoint** is a Loch Panel server. A **namespace** is a Docker Compose stack on that server. Most commands act on the active endpoint and the active namespace. ### Add a server 1. Add the endpoint. On a terminal, `loch` then asks for the API key (leave it empty to skip): ```bash loch config my-server --url https://panel.example.com ``` 2. Make it the active endpoint: ```bash loch use my-server ``` 3. Save the API key of the active endpoint, if you skipped it: ```bash loch login ``` Create the API key in the panel, under **API keys**. Its scopes decide which commands work: see [API key scopes](/panel/reference/scopes/). ### Commands | Command | Does | |---|---| | `loch config ` | Adds or changes an endpoint (`--url`, `--name`). Without flags, shows it. | | `loch config ls` | Lists the endpoints. The active one is marked. | | `loch use ` | Switches the active endpoint. Without argument, lists the endpoints. | | `loch login` | Saves an API key for the active endpoint (hidden input). | | `loch ns` | Lists the namespaces of the active endpoint. | | `loch ns ` | Switches the active namespace. | ```bash loch config ls loch use staging loch ns # list the stacks loch ns blog # work on the "blog" stack ``` ### Permissions | Command | Scope | |---|---| | `config`, `use`, `login` | None (local configuration) | | `ns` | `containers:read` | ### Good to know - The configuration is stored in `~/.config/loch/config.yaml`, readable by your user only. - A namespace is the `com.docker.compose.project` label of the containers. Containers without it are in `default`. - The active namespace is shared by all endpoints: run `loch ns` again after `loch use`. - `--token` also sets the key, but it is visible in the process list. Prefer the prompt or `loch login`. --- ## Files, shell, logs and tunnels These commands work on a container of the active namespace. All traffic goes through the panel over HTTPS (WebSocket), so they work behind a firewall or a corporate proxy. ### Copy files Use `service:path` for the container side. Files and directories are copied as a tar stream. ```bash loch cp php:/var/www/html ./html # container to local loch cp ./file.txt php:/tmp/file.txt # local to container ``` ### Open a shell ```bash loch shell php ``` The terminal supports resizing and passes Ctrl+C to the container. ### Read logs | Command | Shows | |---|---| | `loch logs ` | The container output (stdout/stderr) and exits. | | `loch logs -f ` | Follows the output in real time. | | `loch logs ` | The last 1000 lines of a file inside the container. | | `loch logs -f ` | Follows the file. | `-n ` limits the number of lines (`0` = all). ```bash loch logs -n 200 php loch logs -f php /var/log/app.log ``` Reading a file needs the `tail` command in the container. ### Open a tunnel to a port `loch proxy` forwards a container port to a local port, even if the port is not published. ```bash loch proxy postgres 5432:15432 # container port 5432 on localhost:15432 psql -h 127.0.0.1 -p 15432 -U myuser mydb ``` Give several `remote:local` pairs to open several ports over one connection. Stop the tunnel with Ctrl+C. ### Permissions | Command | Scope | |---|---| | `cp` | `containers:cp` | | `shell` | `containers:shell` | | `logs` | `containers:logs` | | `proxy` | `containers:proxy` | Each command also needs `containers:read`, to find the container. ### Good to know - A tunnel accepts at most 10 port mappings and 256 simultaneous connections. - `loch cp` to a container fails if the destination directory does not exist. - A shell runs any command in the container: give `containers:shell` only to keys that need it. --- ## Functions (Guides) :::caution Work in progress Functions are a work in progress: the API, the interface and the CLI exist, but the feature can still change. Runtime updates, external secret stores, public HTTP access and a per-function disk quota are not available yet. ::: `loch functions` (alias `loch fn`) runs handlers written for AWS Lambda™ (Node.js, Python, or a compiled `bootstrap` for Go, Rust…) on a Loch Panel server, on [Loch Lambda Runtime](/lambda-runtime/). Admin only; the functions module must be enabled. Panel side: [Functions](/panel/user-guide/functions/). ### Commands | Command | Does | |---|---| | `loch functions config` | Shows the runtimes and versions, limits, defaults and whether the runtime binary is ready. | | `loch functions ls` | Lists the functions and their state. | | `loch functions show ` | Shows the configuration, variables, secret names, deployments and endpoint. | | `loch functions create --runtime --version ` | Creates a function. Shows the invoke credentials once. | | `loch functions deploy ` | Deploys a directory (zipped here) or a zip, and activates it. | | `loch functions deployments ` | Lists the kept deployments. | | `loch functions activate ` | Activates a kept deployment (alias `rollback`). | | `loch functions invoke ` | Invokes with a JSON event and prints the response. | | `loch functions logs ` | Shows the function output (`-f` to follow). | | `loch functions update ` | Changes resources, handler, variables or the MCP opt-in. | | `loch functions secrets ` | Lists the secret names (`set`, `rm` to change them). | | `loch functions credentials rotate ` | Generates new invoke credentials (`--sigv4` / `--no-sigv4`). | | `loch functions networks [--network …]` | Shows or replaces the networks. | | `loch functions egress on\|off` | Allows or blocks outbound Internet access. | | `loch functions start\|stop\|restart ` | Starts, stops or restarts a function. | | `loch functions delete ` | Deletes a function, its code, secrets and credentials (`--purge` for one edited outside the panel). | | `loch functions dev` | Runs a function on your machine (below). | Every flag: [`loch functions` reference](../reference/commands/loch_functions.md). ### Create, deploy, invoke ```bash loch functions create thumbnail --runtime nodejs --version 22 --handler index.handler \ --memory 512 --env BUCKET=media --secret S3_SECRET --network media \ --credentials-file ~/.config/loch/thumbnail.credentials.json loch functions deploy thumbnail ./build loch functions invoke thumbnail --event-json '{"key": "cat.jpg"}' loch functions logs thumbnail -f ``` - **Runtimes**: `nodejs`, `python`, `provided` (custom runtime, version `al2023`). `loch functions config` lists the versions of the server. - **Code**: install the dependencies first (`node_modules`, `pip install -t .`): nothing is built on the server. A directory is zipped here, without `.git` and `*.credentials.json`. - **Invocation**: the event comes from `--event-json`, `--event ` or stdin (empty means `{}`), one JSON value of 6 MiB at most. The response goes to stdout, the end of the logs and the status to stderr. Exit code 1 when the function returns an error. For a Go or Rust function, build a Linux executable named `bootstrap` for the server architecture (`arch` in `loch functions config`) and put it at the root of the code. See [Custom runtimes](/lambda-runtime/guides/custom-runtimes/). ### Secrets and credentials - Secret values are never taken from the command line: `--secret NAME` and `loch functions secrets set NAME` read the value without echo, or from stdin; `--secret-file NAME=path` reads a file. - The invoke credentials (a bearer token, and with `--sigv4` an AWS SDK key pair the function code can read) are returned once, at creation and rotation. They are printed only on a terminal; `--credentials-file` writes them to a new file with mode 0600, outside the code directory. - If the answer is lost (network error, Ctrl+C), check with `loch functions show`, then rotate the credentials. ```bash printf '%s\n' "$TOKEN" | loch functions secrets set thumbnail API_TOKEN loch functions credentials rotate thumbnail --credentials-file ~/.config/loch/thumbnail.credentials.json ``` ### Run a function locally `loch functions dev` runs a function on your machine with Docker, in a container that mirrors the deployed one (same AWS base image, limits and hardening). It needs no panel nor API key. It downloads the Loch Lambda Runtime release this `loch` build trusts, checks its SHA-256 and keeps it in the cache directory (`$XDG_CACHE_HOME/loch/lambda-runtime/`). | Command | Does | |---|---| | `loch functions dev [dir] --runtime ` | Starts the function in the foreground, streams its logs; Ctrl-C removes it. `--detach` leaves it running. | | `loch functions dev invoke [name]` | Invokes it through `docker exec` (no credential). | | `loch functions dev info [name]` | Prints the local endpoint, credentials and ready-to-use `curl` / `aws` commands. | | `loch functions dev logs [name]` | Shows its logs. | | `loch functions dev ls` | Lists the local functions. | | `loch functions dev down [name]` | Removes the container, network and cache files. | | `loch functions dev event ` | Prints a sample event: `s3-put`, `sqs`, `api-gateway-v2`, `schedule`. | | `loch functions dev runtime` | Shows the runtime binary used and how to verify it by hand. | ```bash loch functions dev --runtime nodejs:24 --handler index.handler --watch loch functions dev event s3-put --bucket media --key in/cat.jpg | loch functions dev invoke ``` - The invoke port is published on `127.0.0.1` only (default 9000) and needs the credentials printed at start. `--no-auth` turns them off. - Outbound access is on by default locally, unlike a deployed function. `--egress=false` gives an internal network, as deployed by default. - `--watch` restarts the function when the code changes. `--from ` takes the settings of a deployed function (never its values; give them with `--env` / `--env-file`). - `--runtime-binary ` or `LOCH_RUNTIME_BINARY` runs a local build of the runtime instead. Every flag: [`loch functions dev` reference](../reference/commands/loch_functions_dev.md). ### Permissions | Command | Scope | Admin | |---|---|---| | `config`, `ls`, `show`, `deployments`, `dev --from` | `functions:read` | Yes | | `create`, `update` (resources), `start`, `stop`, `restart`, `delete` | `functions:write` | Yes | | `deploy`, `activate`, `secrets`, `credentials`, `networks`, `egress`, `update` (handler, variables, MCP opt-in) | `functions:deploy` | Yes | | `invoke` | `functions:invoke` | Yes | | `logs` | `containers:read` and `containers:logs` (container routes, no `functions:*` scope) | Yes | | `dev` (except `--from`) | None: local only | No | Creating a function with variables, networks, `--egress`, `--sigv4` or `--mcp-invocable` needs `functions:deploy` too. ### Good to know - Names: 1 to 21 lowercase letters, digits or dashes, starting with a letter. - A function never joins the `services` network. Attach the sites that call it to its network `fn-`. - `--egress` lets the code reach the Internet, the host, the LAN and metadata endpoints unless firewall rules block them. - What a function prints and returns is untrusted: control characters are replaced unless `--raw`. - `--mcp-invocable` lets AI assistants invoke the function through `loch mcp`: see [MCP tools](../reference/mcp-tools.md#functions). --- ## Web hosting `loch hosting` (alias `loch host`) manages the hosted websites of the panel: PHP with Nginx or Apache, or static Nginx or Apache sites. Admin only; the hosting module must be enabled on the server. Panel side: [Web hosting](/panel/admin/hosting/). ### Commands | Command | Does | |---|---| | `loch hosting list` | Lists the sites with their status, domains and HTTPS. | | `loch hosting get ` | Shows the settings, containers and last operation (`-v`: environment values, snippets, compose output). | | `loch hosting templates` | Lists the templates, PHP versions and server defaults. | | `loch hosting sites` | Lists the site directories and the site using each. | | `loch hosting php-modules` | Lists the PHP modules of an image. | | `loch hosting check-name ` | Checks that a name is free (exit code 1 if not). | | `loch hosting create ` | Creates and deploys a site. Unset settings take the server defaults. | | `loch hosting update ` | Changes the given settings only, then redeploys. | | `loch hosting deploy ` | Redeploys the site as it is on disk (`--pull`: pull the image first). | | `loch hosting delete ` | Deletes the site. The files are kept unless `--purge-data`. | ```bash loch hosting create blog --template php-nginx --php 8.4 \ --domain blog.example.com --https --redirect-https loch hosting update blog --domain www.blog.example.com --env APP_ENV=prod loch hosting deploy blog --pull loch hosting delete blog ``` `create`, `update`, `deploy` and `delete` wait for the end of the operation. On failure, they print the error and the end of the Docker Compose output, and exit with code 1. `--no-wait` returns as soon as the operation starts. ### Permissions | Command | Scope | Admin | |---|---|---| | `list`, `get`, `templates`, `sites`, `php-modules`, `check-name` | `hosting:read` | Yes | | `create`, `update`, `deploy`, `delete` | `hosting:read` and `hosting:write` | Yes | ### Good to know - A failed `update` restores the previous configuration. - If files were edited outside the panel, `update` and `deploy` refuse. `--force` overwrites them (`update`) or deploys them as they are (`deploy`). - `--network` sets the networks the site joins (repeatable). See [Networks](./networks.md). - Turn on `--https` once the DNS of the domains points to the server. - `--purge-data` also deletes the site directory: its SFTP users lose their files. --- ## Private networks (Guides) `loch networks` (alias `loch net`) manages the internal Docker networks, without Internet access, that connect hosted sites and service instances. A site only reaches the instances of its networks. Admin only. Panel side: [Networks](/panel/admin/networks/). `services` is the shared network of the services module. It cannot be deleted. A private network isolates a group, e.g. a site and its queue. ### Commands | Command | Does | |---|---| | `loch networks ls` | Lists the networks (`--all`: also the other Docker networks). | | `loch networks show ` | Shows a network and its members. | | `loch networks config` | Shows the creation rules: limits, subnet sizes, reserved ranges. | | `loch networks docker [name]` | Shows the Docker networks the panel did not create (read-only). | | `loch networks create ` | Creates a private network (`--subnet`, `--description`). | | `loch networks update --description ` | Changes the description. The subnet cannot change. | | `loch networks delete ` | Deletes a network without members. | | `loch networks attach --site \|--instance ` | Connects a site or an instance. | | `loch networks detach --site \|--instance ` | Disconnects a site or an instance. | | `loch networks disconnect ` | Removes a container the panel does not manage. | ```bash loch networks create shop --description "Shop and its queue" loch networks attach shop --site shop loch networks attach shop --instance elasticmq-jobs loch networks show shop ``` ### Permissions | Command | Scope | Admin | |---|---|---| | `ls`, `show`, `config`, `docker` | `networks:read` | Yes | | `create`, `update`, `delete`, `attach`, `detach`, `disconnect` | `networks:write` | Yes | ### Good to know - Attaching or detaching recreates the member's container: a few seconds of downtime for it only. A site is redeployed (`--no-wait` to not wait). - An instance keeps at least one network. - Names: lowercase letters, digits and dashes, starting with a letter, 24 characters max. - The subnet is optional. It must be a private IPv4 range from /16 to /28, outside the reserved ranges shown by `loch networks config`. - You can also set the networks at creation: `loch hosting create --network`, `loch services create --network` and `loch functions create --network`. The networks of a function are changed with `loch functions networks`: see [Functions](./functions.md). --- ## Databases and services `loch services` (alias `loch db`) manages the services module. Admin only; the module must be enabled on the server. Panel side: [Services](/panel/admin/services/). - **Shared engines** (MySQL, MariaDB, PostgreSQL, MongoDB, RabbitMQ): one instance per version. You create a database (or vhost) with a user of the same name. The first one starts the instance. - **Dedicated engines** (Redis, Valkey, NATS, Loch S3, ElasticMQ, Mosquitto): one container per resource, sized with options. ### Commands | Command | Does | |---|---| | `loch services engines` | Lists the engines, versions, options and the port publishing policy. | | `loch services instances` | Lists the instances and their state. | | `loch services ls` | Lists the resources (`--instance` to filter). | | `loch services create --engine --version ` | Creates a resource. Shows the password once. | | `loch services reset-password ` | Generates a new password. Shows it once. | | `loch services delete ` | Deletes a resource and its data. | | `loch services update --option k=v` | Resizes a dedicated instance (restarts it). | | `loch services updates` | Lists the updates of the dedicated instances (`--check`: check now). | | `loch services upgrade ` | Updates a dedicated instance to the latest image of its version (`--version`: move to a newer version). | | `loch services networks --network ` | Replaces the networks of an instance. | | `loch services expose :` | Publishes the port of an instance on the host. | | `loch services unexpose ` | Closes the published port. | | `loch services retire ` | Removes an empty instance. | | `loch services purge ` | Deletes an instance edited by hand, with all its data. | ```bash loch services create shop --engine postgres --version 17 loch services create cache --engine redis --version 8.4 --option memory=512 loch services reset-password postgres-17 shop loch services expose postgres-17 127.0.0.1:5417 ``` ### Permissions | Command | Scope | Admin | |---|---|---| | `engines`, `instances`, `ls`, `updates` | `services:read` | Yes | | `create`, `reset-password`, `delete`, `update`, `upgrade`, `retire`, `purge`, `updates --check` | `services:write` | Yes | | `networks` | `networks:write` | Yes | | `expose`, `unexpose` | `services:expose` (not included in `services:write`) | Yes | ### Good to know - The password is printed only on a terminal. In a pipe or a CI log, add `--show-password`. - `update`, `upgrade`, `networks` and `expose` recreate the instance: its clients are disconnected for a few seconds. The data is kept. - `upgrade` makes no backup and never goes back to an older version. Back up first when it matters. - ElasticMQ has no authentication: it is refused on the shared network `services`. Give it a private network with `--network` (see [Networks](./networks.md)). - Deleting a resource, publishing a port and moving to another version are CLI or panel only: the MCP server does not offer them. --- ## SFTP users `loch sftp` manages the SFTP accounts that give access to the files of a site. Admin only; the SFTP module must be enabled on the server. Panel side: [SFTP](/panel/admin/sftp/). ### Commands | Command | Does | |---|---| | `loch sftp sites` | Lists the sites. | | `loch sftp users` | Lists the users (`--site` to filter). | | `loch sftp create --site ` | Creates a user in a site. Asks for the password. | | `loch sftp reset-password ` | Sets a new password. | | `loch sftp enable ` | Enables a user. | | `loch sftp disable ` | Disables a user: it can no longer log in. | | `loch sftp delete ` | Deletes the account. | ```bash loch sftp create alice --site blog --subpath web/uploads loch sftp users --site blog loch sftp reset-password alice loch sftp delete alice ``` ### Permissions | Command | Scope | Admin | |---|---|---| | `sites`, `users` | `sftp:read` | Yes | | `create`, `reset-password`, `enable`, `disable`, `delete` | `sftp:write` | Yes | ### Good to know - `--subpath` confines the user to a sub-directory of the site. It cannot change later. - `create` also accepts an SSH key (`--ssh-key`), quotas and a bandwidth limit. - `--password` is visible in the process list: prefer the prompt. - Deleting a user keeps the files of its home directory. --- ## Shell completion `loch completion ` prints a completion script. Once loaded, Tab completes commands, flags, container names, namespaces and endpoints. | Shell | Load it | |---|---| | bash | `source <(loch completion bash)` | | zsh | `echo 'source <(loch completion zsh)' >> ~/.zshrc` | | fish | `loch completion fish \| source` | | PowerShell | `loch completion powershell \| Out-String \| Invoke-Expression` | To load it in every bash session, add the line to `~/.bashrc`. For fish, save it once: ```bash loch completion fish > ~/.config/fish/completions/loch.fish ``` ### Good to know - Container names and namespaces are read from the active endpoint: completion needs a working API key with `containers:read`. - No scope is needed to generate the script. --- ## Users `loch users` manages the panel users. Admin only. A restricted user only sees and manages the containers it was granted. ### Commands | Command | Does | |---|---| | `loch users ls` | Lists the users. | | `loch users grant ` | Gives access to one or more containers. | | `loch users revoke ` | Removes access to one or more containers. | | `loch users promote ` | Makes the user an admin. | | `loch users demote ` | Makes the user restricted. | | `loch users rm ` | Deletes the user. | ```bash loch users grant alice@example.com myapp-web-1 myapp-php-1 loch users revoke alice@example.com myapp-php-1 loch users promote bob@example.com ``` ### Permissions | Command | Scope | Admin | |---|---|---| | `users …` | None: only the profile of the key's user is checked | Yes | | `domains` | None: only the profile of the key's user is checked | Yes | ### Good to know - `loch domains` lists the Traefik domains and their routing. - A key carries the profile of its user: an admin scope on a restricted user's key grants nothing. - A key can only create keys within its own scopes. - Function containers (`fn-*`) cannot be granted to a user. --- ## Install ### macOS The universal binary runs on Intel and Apple Silicon. It is signed and notarized. ```bash curl -L -o loch https://lab.nitrique.net/api/v4/projects/299/packages/generic/loch/v1.5.0/loch-darwin-universal chmod +x loch sudo mv loch /usr/local/bin/ ``` If macOS blocks a binary downloaded with a browser: ```bash xattr -d com.apple.quarantine /usr/local/bin/loch ``` ### Linux ```bash ARCH=amd64 # or arm64 curl -L -o loch https://lab.nitrique.net/api/v4/projects/299/packages/generic/loch/v1.5.0/loch-linux-$ARCH chmod +x loch sudo mv loch /usr/local/bin/ ``` ### Updates `loch` updates itself every 12 hours. To update now: ```bash loch update ``` ### Build from source ```bash go build -ldflags "-X loch/cmd.Version=1.5.0" -o loch . ``` --- ## Loch CLI `loch` manages one or more Loch Panel servers from a terminal. It also runs an MCP server, so an AI assistant can use the same features. 1. [Install `loch`](./install.md). 2. Connect it to a server: ```bash loch config my-server --url https://panel-api.example.com # add an endpoint loch use my-server # make it active loch login # paste an API key loch ps # list containers ``` 3. Optional: [connect an AI assistant](./mcp.md). Every command and flag: [command reference](./reference/commands/loch.md). The configuration is stored in `~/.config/loch/config.yaml`. Create the API key in the panel, under **API keys**. Its scopes decide which commands work. --- ## Connect an AI assistant `loch mcp` starts an MCP server (stdio transport). It uses the endpoints and API keys of your `loch` configuration. **Before you start:** [install `loch`](./install.md), then run `loch config`, `loch use` and `loch login`. Use the full path of the binary (`which loch`): desktop apps often do not see your `PATH`. **Claude Code** ```bash claude mcp add --scope user loch /usr/local/bin/loch mcp ``` Check with `claude mcp list`. **Codex** ```bash codex mcp add loch -- /usr/local/bin/loch mcp ``` Or in `~/.codex/config.toml`: ```toml [mcp_servers.loch] command = "/usr/local/bin/loch" args = ["mcp"] ``` **OpenCode** In `opencode.json` (project) or `~/.config/opencode/opencode.json` (global): ```json { "$schema": "https://opencode.ai/config.json", "mcp": { "loch": { "type": "local", "command": ["/usr/local/bin/loch", "mcp"], "enabled": true } } } ``` **LM Studio** In LM Studio, open **Program → Install → Edit mcp.json**, then add: ```json { "mcpServers": { "loch": { "command": "/usr/local/bin/loch", "args": ["mcp"] } } } ``` Choose a model that supports tool calls. ### Try it Ask your assistant: *"List my loch endpoints, then the stacks on the first one."* ### Good to know - The assistant can do only what your API key allows. Use a key with limited scopes for daily work. - Passwords are never returned to the assistant. To reset one, run `loch services reset-password` yourself. - Deleting a database and publishing ports are CLI-only. So are deploying functions, their secrets and credentials: an assistant only invokes the functions marked `--mcp-invocable`. - `loch ai:howto` prints these instructions in the terminal. Full list of tools: [MCP tools](./reference/mcp-tools.md). --- ## loch CLI to manage your containers via Host Panel #### Synopsis Manage your Docker containers through the Host Panel API, no technical interface needed. #### Options ``` --debug Show HTTP requests -h, --help help for loch ``` #### SEE ALSO * [loch ai:howto](loch_ai-howto.md) - Display how to use this tool with AI * [loch completion](loch_completion.md) - Generate shell completion script * [loch config](loch_config.md) - Configure an endpoint * [loch cp](loch_cp.md) - Copy files between local filesystem and a container * [loch cron](loch_cron.md) - Manage scheduled jobs * [loch domains](loch_domains.md) - Show Traefik domains (admin) * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) * [loch inspect](loch_inspect.md) - Display detailed information on a container * [loch kill](loch_kill.md) - Kill one or more containers * [loch login](loch_login.md) - Configure API token interactively * [loch logs](loch_logs.md) - Display container logs * [loch networks](loch_networks.md) - Manage the private networks between sites and services * [loch ns](loch_ns.md) - List or switch namespace (Docker Compose stack) * [loch proxy](loch_proxy.md) - Forward local ports to a container via WebSocket tunnel (i.e. get container port 80 on port 8000 in your machine) * [loch ps](loch_ps.md) - List containers in the active namespace * [loch restart](loch_restart.md) - Restart one or more containers * [loch rm](loch_rm.md) - Remove one or more containers * [loch services](loch_services.md) - Manage services: databases, caches and message brokers * [loch sftp](loch_sftp.md) - Manage SFTPGo users * [loch shell](loch_shell.md) - Open an interactive shell in a container * [loch stack](loch_stack.md) - Start, stop or restart a whole Docker Compose stack * [loch start](loch_start.md) - Start one or more containers * [loch stats](loch_stats.md) - Live resource monitoring for the active namespace * [loch stop](loch_stop.md) - Stop one or more containers * [loch update](loch_update.md) - Update loch to the latest version * [loch use](loch_use.md) - Switch active endpoint * [loch users](loch_users.md) - Manage users (admin) * [loch version](loch_version.md) - Show loch version --- ## loch ai:howto Display how to use this tool with AI ``` loch ai:howto [flags] ``` #### Options ``` -h, --help help for ai:howto ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch completion Generate shell completion script #### Synopsis Generate the autocompletion script for your shell. To load completions: ``` bash: source <(loch completion bash) zsh: echo 'source <(loch completion zsh)' >> ~/.zshrc fish: loch completion fish | source powershell: loch completion powershell | Out-String | Invoke-Expression ``` ``` loch completion [bash|zsh|fish|powershell] ``` #### Options ``` -h, --help help for completion ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch config Configure an endpoint #### Synopsis Configure an API endpoint. Creates it if it doesn't exist. ``` loch config [flags] ``` #### Options ``` -h, --help help for config --name string Display name --token string API key (unsafe: visible in 'ps'; omit to be prompted securely, or use 'loch login') --url string Endpoint URL ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel * [loch config ls](loch_config_ls.md) - List configured endpoints --- ## loch config ls List configured endpoints ``` loch config ls [flags] ``` #### Options ``` -h, --help help for ls ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch config](loch_config.md) - Configure an endpoint --- ## loch cp Copy files between local filesystem and a container #### Synopsis Copy files between the local filesystem and a container. Use the service:path syntax to refer to a container path: loch cp php:/var/www/html ./html loch cp ./file.txt php:/tmp/file.txt ``` loch cp [flags] ``` #### Options ``` -h, --help help for cp ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch cron Manage scheduled jobs #### Options ``` -h, --help help for cron ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel * [loch cron create](loch_cron_create.md) - Create a scheduled job (interactive, or with flags for a function invocation) * [loch cron disable](loch_cron_disable.md) - Disable a scheduled job * [loch cron enable](loch_cron_enable.md) - Enable a scheduled job * [loch cron ls](loch_cron_ls.md) - List scheduled jobs * [loch cron rm](loch_cron_rm.md) - Delete a scheduled job * [loch cron run](loch_cron_run.md) - Run a scheduled job immediately --- ## loch cron create Create a scheduled job (interactive, or with flags for a function invocation) #### Synopsis Create a scheduled job. Questions are asked for what the flags do not give. Types: docker_action, docker_exec, http and function_invoke (invokes a function with a fixed JSON event; needs the functions:invoke scope; the job keeps the status, duration and size of the response, never the payload; the event is at most 64 KiB). An event holding tokens belongs in a file (--event), not in --event-json (shell history, process list). ``` loch cron create [flags] ``` #### Examples ``` loch cron create loch cron create --name nightly-report --schedule '0 3 * * *' --function report --event-json '{"period": "day"}' ``` #### Options ``` --event string function_invoke: file holding the JSON event --event-json string function_invoke: the JSON event (default {}) --function string Create a function_invoke job for this function -h, --help help for create --name string Job name (asked when not given) --schedule string Cron expression or @every 5m (asked when not given) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch cron](loch_cron.md) - Manage scheduled jobs --- ## loch cron disable Disable a scheduled job ``` loch cron disable [flags] ``` #### Options ``` -h, --help help for disable ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch cron](loch_cron.md) - Manage scheduled jobs --- ## loch cron enable Enable a scheduled job ``` loch cron enable [flags] ``` #### Options ``` -h, --help help for enable ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch cron](loch_cron.md) - Manage scheduled jobs --- ## loch cron ls List scheduled jobs ``` loch cron ls [flags] ``` #### Options ``` -h, --help help for ls ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch cron](loch_cron.md) - Manage scheduled jobs --- ## loch cron rm Delete a scheduled job ``` loch cron rm [flags] ``` #### Options ``` -h, --help help for rm ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch cron](loch_cron.md) - Manage scheduled jobs --- ## loch cron run Run a scheduled job immediately ``` loch cron run [flags] ``` #### Options ``` -h, --help help for run ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch cron](loch_cron.md) - Manage scheduled jobs --- ## loch domains Show Traefik domains (admin) ``` loch domains [flags] ``` #### Options ``` -h, --help help for domains ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch functions Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint #### Synopsis Manage the functions module (admin only): handlers written for AWS Lambda™ run in the AWS Lambda base images published by AWS, one compose project per function (fn-<name>), in a private network of their own. Code is a zip (deploy a directory, zipped here, or a zip file); invocations go through the panel (loch functions invoke) or, from the members of the function's networks, to its endpoint with its invoke credentials. What a function prints and returns is untrusted data. AWS Lambda is a trademark of Amazon.com, Inc. or its affiliates; loch is not affiliated with or endorsed by Amazon. #### Options ``` -h, --help help for functions ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel * [loch functions activate](loch_functions_activate.md) - Activate a kept deployment (rollback) * [loch functions config](loch_functions_config.md) - Show the runtimes, limits and defaults of the endpoint * [loch functions create](loch_functions_create.md) - Create a function (invoke credentials shown once) * [loch functions credentials](loch_functions_credentials.md) - Manage the invoke credentials of a function * [loch functions delete](loch_functions_delete.md) - Delete a function, its code, secrets and credentials * [loch functions deploy](loch_functions_deploy.md) - Deploy code (a directory, zipped here, or a zip file) and activate it * [loch functions deployments](loch_functions_deployments.md) - List the kept deployments of a function (newest first) * [loch functions dev](loch_functions_dev.md) - Run a function locally with Docker and the loch runtime (no panel) * [loch functions egress](loch_functions_egress.md) - Allow or block the outbound Internet access of a function * [loch functions invoke](loch_functions_invoke.md) - Invoke a function with a JSON event and print its response * [loch functions logs](loch_functions_logs.md) - Show the logs (stdout and stderr) of a function * [loch functions ls](loch_functions_ls.md) - List the functions * [loch functions networks](loch_functions_networks.md) - Show or replace the networks of a function * [loch functions restart](loch_functions_restart.md) - Restart a function * [loch functions secrets](loch_functions_secrets.md) - List the secret names of a function, set or remove a secret * [loch functions show](loch_functions_show.md) - Show a function: configuration, variables, secret names, deployments, endpoint * [loch functions start](loch_functions_start.md) - Start a function * [loch functions stop](loch_functions_stop.md) - Stop a function (it stays stopped until started) * [loch functions update](loch_functions_update.md) - Change the resources, handler or variables of a function --- ## loch functions activate Activate a kept deployment (rollback) #### Synopsis Activate a kept deployment (see loch functions deployments): its files are verified first, then the function restarts on it. ``` loch functions activate [flags] ``` #### Examples ``` loch functions rollback thumbnail 20261106T101500Z-1a2b3c4d ``` #### Options ``` -h, --help help for activate ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions config Show the runtimes, limits and defaults of the endpoint ``` loch functions config [flags] ``` #### Options ``` -h, --help help for config -o, --output string Output format: table or json (default "table") ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions create Create a function (invoke credentials shown once) #### Synopsis Create a function: a handler written for AWS Lambda (Node.js or Python), or a compiled bootstrap on the custom runtime (provided, version al2023: Go, Rust, ...), run in the AWS Lambda base image published by AWS, in its own private network fn-<name> (never the shared services network). Nothing runs until code is deployed (--code here, or loch functions deploy). Names: 1 to 21 lowercase letters, digits or dashes, starting with a letter. Handler: <file>.<function> for Node.js (index.handler), <module>.<function> for Python (lambda_function.lambda_handler), required for both; optional for provided (default bootstrap, exported as _HANDLER): the code must hold an executable named bootstrap at its root, a Linux binary of the endpoint's architecture (loch functions config), e.g. for Go: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags lambda.norpc -o bootstrap . Secrets (--secret NAME, repeatable) are typed without echo, or read from stdin one line per name in order; --secret-file NAME=path reads one from a file. Values are never taken from the command line. The invoke credentials (bearer token, and a SigV4 key pair with --sigv4) are shown once: on a terminal, or saved with --credentials-file in a new 0600 file (outside the code directory, e.g. ~/.config/loch/). --mcp-invocable lets AI agents using loch mcp invoke the function (off by default). --egress gives outbound Internet access: it also reaches the host gateway, the LAN and metadata endpoints unless DOCKER-USER rules say otherwise. ``` loch functions create --runtime nodejs|python|provided --version [--handler ] [flags] ``` #### Examples ``` loch functions create thumbnail --runtime nodejs --version 22 --handler index.handler --code ./build loch functions create resize --runtime python --version 3.13 --handler app.handler --memory 1G \ --env BUCKET=media --secret AWS_SECRET_ACCESS_KEY --network media \ --credentials-file ~/.config/loch/resize.credentials.json loch functions create hello-go --runtime provided --version al2023 --code ./hello-go # ./hello-go/bootstrap ``` #### Options ``` --code string Code to deploy after the creation: a directory (zipped here) or a zip file --cpus float CPU limit (0.1 to 16, in steps of 0.01) --credentials-file string Save the invoke credentials (JSON) in this new file, mode 0600, instead of printing them --egress Allow outbound Internet access (off by default) --env stringArray Variable NAME=value (repeatable; not secret: shown by loch functions show) --handler string Handler: index.handler (Node.js), lambda_function.lambda_handler (Python); optional for provided (default bootstrap) -h, --help help for create --mcp-invocable Let AI agents (loch mcp, functions_invoke) invoke it (off by default) --memory string Memory limit in MB, or with a unit (512M, 1G); see loch functions config --network stringArray Private network to join besides fn- (repeatable; never services) --no-mcp-invocable Do not let AI agents invoke it (the default) --runtime string Runtime: nodejs, python or provided (custom runtime: Go, Rust, a bootstrap) --secret stringArray Secret NAME, its value typed without echo or read from stdin (repeatable) --secret-file stringArray Secret NAME=path, its value read from the file (repeatable) --show-credentials Print the credentials even when stdout is not a terminal --sigv4 Also accept SigV4 callers (AWS SDKs): a key pair readable by the function code --timeout int Timeout of an invocation in seconds (1 to 900) --tmp-size string Size of /tmp in MB, or with a unit; counted in the memory limit --version string Runtime version (see loch functions config), e.g. 22, 3.13 or al2023 ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions credentials Manage the invoke credentials of a function #### Options ``` -h, --help help for credentials ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint * [loch functions credentials rotate](loch_functions_credentials_rotate.md) - Generate new invoke credentials (shown once; the old ones stop working) --- ## loch functions credentials rotate Generate new invoke credentials (shown once; the old ones stop working) #### Synopsis Generate a new bearer token (and, with SigV4 enabled, a new key pair). The old credentials stop working and the function restarts. --sigv4 also accepts AWS SDK (SigV4) callers: the key pair is then readable by the function code (it only lets the function invoke itself); --no-sigv4 removes it. The credentials are printed on a terminal, or saved with --credentials-file in a new 0600 file. ``` loch functions credentials rotate [--sigv4|--no-sigv4] [--credentials-file ] [flags] ``` #### Examples ``` loch functions credentials rotate thumbnail --credentials-file ~/.config/loch/thumbnail.credentials.json ``` #### Options ``` --credentials-file string Save the invoke credentials (JSON) in this new file, mode 0600, instead of printing them -h, --help help for rotate --no-sigv4 Remove the SigV4 key pair (bearer token only) --show-credentials Print the credentials even when stdout is not a terminal --sigv4 Also accept SigV4 callers (AWS SDKs): a key pair readable by the function code -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions credentials](loch_functions_credentials.md) - Manage the invoke credentials of a function --- ## loch functions delete Delete a function, its code, secrets and credentials #### Synopsis Delete a function: its container, compose project, every deployment, secrets, invoke credentials and its network fn-<name> (kept while other members are attached). This cannot be undone: type the name to confirm, or pass --yes. --purge deletes a function whose compose project was edited outside the panel (shown as modified) or whose reference is unreadable, without running any of its files. ``` loch functions delete [--purge] [--yes] [flags] ``` #### Examples ``` loch functions delete thumbnail loch functions delete thumbnail --purge --yes ``` #### Options ``` -h, --help help for delete --purge Delete a function edited outside the panel, without running its files -y, --yes Skip the confirmation (typing the name) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions deploy Deploy code (a directory, zipped here, or a zip file) and activate it #### Synopsis Deploy the code of a function: a zip of the handler and its dependencies (never built on the server). A directory is zipped here, deterministically: entries sorted, fixed dates, normalized modes (files 0644, or 0755 when executable; directories 0755), .git skipped; relative symlinks staying inside the directory are kept, absolute or escaping ones are refused; *.credentials.json files are skipped and dotenv files warned about. The zip is reproducible with the same loch build (the API identifies a deployment by the hash of its extracted tree). Install the dependencies first (npm ci, pip install -t .). The new deployment is activated at once; the previous ones are kept for a rollback (loch functions activate). ``` loch functions deploy [flags] ``` #### Examples ``` loch functions deploy thumbnail ./build loch functions deploy thumbnail function.zip ``` #### Options ``` -h, --help help for deploy ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions deployments List the kept deployments of a function (newest first) ``` loch functions deployments [flags] ``` #### Options ``` -h, --help help for deployments -o, --output string Output format: table or json (default "table") ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions dev Run a function locally with Docker and the loch runtime (no panel) #### Synopsis Run a function on this machine, in a container that mirrors the one the panel runs: same AWS base image (from mirror.gcr.io), loch function runtime, environment, limits and hardening. The code directory is bind-mounted read-only (no zip); dir defaults to the current directory and the name to its name (normalized to the panel's rules, so the same name can be deployed; --name otherwise). In the foreground (default) it starts the function, streams its logs, and Ctrl-C removes it. --detach leaves it running: loch functions dev invoke|logs|info|down then work on it. The invoke port is published on 127.0.0.1 only and requires the credentials generated at start (printed, and by loch functions dev info): a web page could otherwise POST to it. --no-auth turns that off. --egress=false gives an internal network, as deployed by default: no port can be published then, invoke with loch functions dev invoke. Docker objects: compose project, container and network loch-dev-<name> (label sh.loch.dev-function), never the panel's fn-*. Files: the variables and credentials in 0600 files of the loch cache directory (--print-compose shows their paths, never their values), removed by loch functions dev down. A remote Docker daemon (DOCKER_HOST or a context on tcp:// or ssh://) is refused. Runtime binary: the loch function runtime release (github.com/loch-sh/lambda-runtime) of the architecture, the newest this loch build trusts (--runtime-version vX.Y.Z for another trusted one). It is downloaded once over HTTPS, its sha256 checked against the table built into loch before it is kept in the cache directory ($XDG_CACHE_HOME/loch/lambda-runtime, else the OS cache directory), and checked again on every run (a modified copy is downloaded again). --runtime-binary <path> or LOCH_RUNTIME_BINARY run a local build as given (warned when its sha256 is not a trusted release). loch functions dev runtime shows the binary used and how to verify a release by hand (cosign, gh attestation). Variables: --env NAME=value (repeatable) and --env-file (NAME=value lines, # comments, raw values: no quotes nor interpolation); --env wins. Names the panel reserves (AWS_LAMBDA_*, LOCH_RUNTIME_*, PATH, ...) are refused. A *.credentials.json file in the code directory is refused (the function could read it). Dependencies: the panel never installs them, they are part of the code. Install them with the runtime image and architecture, in the code directory (on an amd64 daemon: linux/amd64 and the -x86_64 tag): docker run --rm --platform linux/arm64 -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$PWD":/var/task -w /var/task --entrypoint npm mirror.gcr.io/amazon/aws-lambda-nodejs:24-arm64 ci --omit=dev docker run --rm --platform linux/arm64 -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$PWD":/var/task -w /var/task --entrypoint pip mirror.gcr.io/amazon/aws-lambda-python:3.13-arm64 install -r requirements.txt -t . (Linux with SELinux: add :z to the -v option.) Custom runtime (provided:al2023, Go, Rust): the code directory holds an executable named bootstrap at its root, checked as the panel does at deploy time (a Linux ELF of --arch, static or glibc, or a #! script) and for its mode bits (o+x: the function runs as uid 10001; Docker Desktop hides a missing exec bit, a Linux host does not). The handler is optional (default bootstrap, exported as _HANDLER). For Go: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags lambda.norpc -o bootstrap . Stopping: Ctrl-C (or SIGTERM, SIGHUP) removes the function, giving it its stop grace period (min(timeout, 30) + 10 s). After loch was killed (SIGKILL), run loch functions dev down <name>. --env values show in the process list and the shell history: prefer --env-file for secrets. --watch (foreground only: --watch --detach is refused) polls the code directory every 500 ms (mtime, size, file count), skipping node_modules, .venv, site-packages, __pycache__, .git and editor swap files; the contents of symlinked directories are not watched, and a rewrite keeping both the size and the mtime can be missed. Above 50000 entries it polls every 2 s and watches only the top-level entries and the files one directory down. 300 ms after a burst of changes (at most 2 s after its first change) it restarts the container (docker restart -t 2: a new cold start loads the code again) and prints "reloaded (cold start)". Enter restarts it too, when stdin is a terminal (after a dependency change). --from FUNCTION reads a deployed function from the active endpoint (functions:read): runtime, version, handler, memory, timeout, /tmp, CPUs, architecture, egress, its name, and the NAMES of its variables and secrets. Values are never kept nor printed: each name not given with --env/--env-file is warned about. Flags given explicitly win. The endpoint's limits may differ. --exact-image also runs the deployed image, pinned by its digest, instead of the line tag. loch functions dev event s3-put|sqs|api-gateway-v2|schedule prints a sample event to pipe into dev invoke. ``` loch functions dev [dir] (--runtime nodejs:24|python:3.13|provided | --from FUNCTION) [flags] ``` #### Examples ``` loch functions dev --runtime nodejs:24 --handler index.handler loch functions dev ./thumbnail --runtime python:3.13 --handler app.handler --env-file .env.local --detach loch functions dev invoke thumbnail --event-json '{"key": "cat.jpg"}' loch functions dev ./logs --runtime nodejs:24 # a directory named like a subcommand loch functions dev --runtime nodejs:24 --print-compose > compose.yaml loch functions dev --runtime nodejs:24 --watch loch functions dev ./hello-go --runtime provided # ./hello-go/bootstrap built for the daemon's arch loch functions dev --from thumbnail --env-file .env.local --exact-image loch functions dev event s3-put --bucket media --key 'in/cat.jpg' | loch functions dev invoke thumbnail ``` #### Options ``` --arch string Architecture: amd64 or arm64 (default: the Docker daemon's) --cpus float CPU limit (default 1) -d, --detach Leave the function running in the background --egress Outbound Internet access (--egress=false: internal network, no port published) (default true) --env stringArray Variable NAME=value (repeatable) --env-file string File of NAME=value lines (raw values, # comments) --exact-image With --from: run the deployed image, pinned by its digest --from string Take the settings of a deployed function of the active endpoint (its values are never kept nor printed: give them with --env/--env-file) --handler string Handler: . (default index.handler for Node.js, lambda_function.lambda_handler for Python, bootstrap for provided) -h, --help help for dev --memory string Memory limit, MB or with a unit (1G) (default "512") --name string Function name (default: the directory name, normalized) --no-auth Invoke port without credentials (any local process or web page can invoke) --port int Invoke port on 127.0.0.1 (default 9000) --print-compose Print the compose file without starting, downloading nor writing anything (its env files exist once the function was started; the runtime binary once downloaded) --pull Pull the runtime image again (refresh the line tag) --runtime string Runtime and version: nodejs:26|24|22, python:3.14|3.13|3.12, provided:al2023 (Go, Rust: a bootstrap; no version = the newest) --runtime-binary string A local build of the loch function runtime (default: $LOCH_RUNTIME_BINARY, else the trusted release, downloaded and verified) --runtime-version string Trusted loch function runtime release vX.Y.Z (default: the newest this loch build trusts) --timeout int Timeout of an invocation, seconds (default 30) --tmp string Size of /tmp (counted in the memory), MB or with a unit (default "512") -w, --watch Restart the function when the code changes (polling, foreground only; Enter restarts it too when stdin is a terminal) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint * [loch functions dev down](loch_functions_dev_down.md) - Remove a local function: container, network and cache files * [loch functions dev event](loch_functions_dev_event.md) - Print a sample event (JSON) to pipe into loch functions dev invoke * [loch functions dev info](loch_functions_dev_info.md) - Print the endpoint, credentials and invoke commands of a local function * [loch functions dev invoke](loch_functions_dev_invoke.md) - Invoke a local function (docker exec, no credential) * [loch functions dev logs](loch_functions_dev_logs.md) - Show the logs of a local function * [loch functions dev ls](loch_functions_dev_ls.md) - List the local functions, and the files left in the loch cache * [loch functions dev runtime](loch_functions_dev_runtime.md) - Show the loch function runtime binary loch functions dev uses, and where it comes from --- ## loch functions dev down Remove a local function: container, network and cache files #### Synopsis Remove a local function: its container and network (only objects labeled sh.loch.dev-function), and its variables and credentials files. NAME defaults to the name of the current directory. ``` loch functions dev down [NAME] [flags] ``` #### Options ``` -h, --help help for down ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions dev](loch_functions_dev.md) - Run a function locally with Docker and the loch runtime (no panel) --- ## loch functions dev event Print a sample event (JSON) to pipe into loch functions dev invoke #### Synopsis Print a sample event on stdout, shaped like the AWS Lambda event examples, with fake values (account 000000000000, region us-east-1, the current time): s3-put S3 ObjectCreated:Put notification (--bucket, --key; the key URL-encoded as S3 does) sqs SQS message batch of one record (--body) api-gateway-v2 API Gateway HTTP API request, payload format 2.0 (--method, --path with an optional query string, --body) schedule EventBridge scheduled event ``` loch functions dev event [flags] ``` #### Examples ``` loch functions dev event s3-put --bucket media --key 'uploads/red flower.jpg' | loch functions dev invoke thumbnail loch functions dev event api-gateway-v2 --method POST --path '/items?full=1' --body '{"name":"a"}' > event.json loch functions dev invoke --event event.json ``` #### Options ``` --body string api-gateway-v2, sqs: request or message body --bucket string s3-put: bucket name (default example-bucket) -h, --help help for event --key string s3-put: object key (default uploads/example.jpg) --method string api-gateway-v2: HTTP method (default GET) --path string api-gateway-v2: path and optional query string (default /) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions dev](loch_functions_dev.md) - Run a function locally with Docker and the loch runtime (no panel) --- ## loch functions dev info Print the endpoint, credentials and invoke commands of a local function #### Synopsis Print the endpoint on 127.0.0.1, the generated credentials and ready-to-use curl / aws commands of a local function. The credentials only reach this function. NAME defaults to the name of the current directory. ``` loch functions dev info [NAME] [flags] ``` #### Options ``` -h, --help help for info -o, --output string Output format: table or json (default "table") ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions dev](loch_functions_dev.md) - Run a function locally with Docker and the loch runtime (no panel) --- ## loch functions dev invoke Invoke a local function (docker exec, no credential) #### Synopsis Invoke a local function through docker exec of the runtime's invoke subcommand, as the panel does: same event rules and output as loch functions invoke (payload on stdout, log tail and status on stderr, exit code 1 on a function error). NAME defaults to the name of the current directory. ``` loch functions dev invoke [NAME] [--event file.json | --event-json '' | < event.json] [flags] ``` #### Examples ``` loch functions dev invoke --event-json '{"name": "world"}' loch functions dev invoke thumbnail --event s3-put.json --raw > response.json ``` #### Options ``` --event string File holding the JSON event (- for stdin) --event-json string The JSON event itself -h, --help help for invoke -o, --output string Output format: table or json (default "table") --raw Write the payload bytes exactly as returned (no newline added, no character replaced) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions dev](loch_functions_dev.md) - Run a function locally with Docker and the loch runtime (no panel) --- ## loch functions dev logs Show the logs of a local function #### Synopsis Show the stdout/stderr logs of a local function (written by the function code). NAME defaults to the name of the current directory. ``` loch functions dev logs [NAME] [-f] [flags] ``` #### Options ``` -f, --follow Follow the logs -h, --help help for logs -n, --tail int Number of lines to show (0 = all) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions dev](loch_functions_dev.md) - Run a function locally with Docker and the loch runtime (no panel) --- ## loch functions dev ls List the local functions, and the files left in the loch cache ``` loch functions dev ls [flags] ``` #### Options ``` -h, --help help for ls -o, --output string Output format: table or json (default "table") ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions dev](loch_functions_dev.md) - Run a function locally with Docker and the loch runtime (no panel) --- ## loch functions dev runtime Show the loch function runtime binary loch functions dev uses, and where it comes from #### Synopsis Show the loch function runtime binary of loch functions dev: --runtime-binary, else LOCH_RUNTIME_BINARY (both used as given, flagged when their sha256 is not a trusted release), else the newest release this loch build trusts (--runtime-version for another), from the cache directory when it is there and its sha256 matches. Nothing is downloaded unless --download. It also prints the commands to verify the release by hand (shasum, cosign verify-blob with the signing identity built into loch, gh attestation verify). ``` loch functions dev runtime [flags] ``` #### Options ``` --arch string Architecture: amd64 or arm64 (default: the Docker daemon's) --download Download and verify the release when it is not cached -h, --help help for runtime -o, --output string Output format: table or json (default "table") --runtime-binary string A local build of the loch function runtime (default: $LOCH_RUNTIME_BINARY) --runtime-version string Trusted release vX.Y.Z (default: the newest) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions dev](loch_functions_dev.md) - Run a function locally with Docker and the loch runtime (no panel) --- ## loch functions egress Allow or block the outbound Internet access of a function #### Synopsis Turn the outbound Internet access of a function on or off (off by default). When on, the function also reaches the host gateway, the LAN and metadata endpoints unless DOCKER-USER rules say otherwise. The function restarts. ``` loch functions egress on|off [flags] ``` #### Options ``` -h, --help help for egress ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions invoke Invoke a function with a JSON event and print its response #### Synopsis Invoke a function. The event is one JSON value (at most 6 MiB): --event <file> (- for stdin), --event-json, stdin when it is not a terminal (empty: \{\}), or \{\}. An event holding tokens belongs in a file or on stdin, not in --event-json (shell history, process list). The response (payload) is printed on stdout, the end of the invocation's logs and the status on stderr. Both were written by the function code: control characters are replaced unless --raw (payload bytes exactly as returned, e.g. to redirect them to a file). The exit code is 1 when the function returned an error (functionError) or the invocation failed. ``` loch functions invoke [--event file.json | --event-json '' | < event.json] [flags] ``` #### Examples ``` loch functions invoke thumbnail --event-json '{"key": "photos/cat.jpg"}' loch functions invoke thumbnail --event s3-put.json echo '{}' | loch functions invoke thumbnail --raw > response.json ``` #### Options ``` --event string File holding the JSON event (- for stdin) --event-json string The JSON event itself -h, --help help for invoke -o, --output string Output format: table or json (default "table") --raw Write the payload bytes exactly as returned (no newline added, no character replaced) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions logs Show the logs (stdout and stderr) of a function #### Synopsis Show the stdout/stderr logs of the function container (admins only). The lines were written by the function code. -f follows them. ``` loch functions logs [-f] [-n lines] [flags] ``` #### Options ``` -f, --follow Follow the logs -h, --help help for logs -n, --tail int Number of lines to show (0 = all; 100 when following) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions ls List the functions ``` loch functions ls [flags] ``` #### Options ``` -h, --help help for ls -o, --output string Output format: table or json (default "table") ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions networks Show or replace the networks of a function #### Synopsis Without --network, show the networks of a function. With --network (repeatable), replace them: the function restarts on the new list (removed networks are cut first). Keep its own network fn-<name> to stay reachable by name; never services. Members of these networks can call the function with its credentials. ``` loch functions networks [--network n]... [flags] ``` #### Examples ``` loch functions networks thumbnail --network fn-thumbnail --network media ``` #### Options ``` -h, --help help for networks --network stringArray Network of the function (repeatable; replaces the list) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions restart Restart a function ``` loch functions restart [flags] ``` #### Options ``` -h, --help help for restart -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions secrets List the secret names of a function, set or remove a secret #### Synopsis List the secret names of a function (values are never shown, nor readable back). Secrets are environment variables kept out of compose.yaml (a 0400 file of the panel). ``` loch functions secrets [flags] ``` #### Options ``` -h, --help help for secrets ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint * [loch functions secrets rm](loch_functions_secrets_rm.md) - Remove a secret * [loch functions secrets set](loch_functions_secrets_set.md) - Set a secret (value typed without echo, or read from stdin) --- ## loch functions secrets rm Remove a secret ``` loch functions secrets rm [flags] ``` #### Options ``` -h, --help help for rm ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions secrets](loch_functions_secrets.md) - List the secret names of a function, set or remove a secret --- ## loch functions secrets set Set a secret (value typed without echo, or read from stdin) #### Synopsis Set a secret of a function. The value is typed without echo on a terminal, or read from stdin (a trailing newline is removed): never from the command line. One line of UTF-8, at most 4096 bytes. A running function restarts to use it. ``` loch functions secrets set [flags] ``` #### Examples ``` loch functions secrets set thumbnail AWS_SECRET_ACCESS_KEY pass show s3/thumbnail | loch functions secrets set thumbnail AWS_SECRET_ACCESS_KEY ``` #### Options ``` -h, --help help for set ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions secrets](loch_functions_secrets.md) - List the secret names of a function, set or remove a secret --- ## loch functions show Show a function: configuration, variables, secret names, deployments, endpoint #### Synopsis Show a function. Secret values and invoke credentials are never shown (they cannot be read back: rotate the credentials with loch functions credentials rotate). ``` loch functions show [flags] ``` #### Options ``` -h, --help help for show -o, --output string Output format: table or json (default "table") ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions start Start a function ``` loch functions start [flags] ``` #### Options ``` -h, --help help for start ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions stop Stop a function (it stays stopped until started) ``` loch functions stop [flags] ``` #### Options ``` -h, --help help for stop -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch functions update Change the resources, handler or variables of a function #### Synopsis Change a function: only the given flags change. --env sets a variable (others are kept), --unset-env removes one. A running function restarts with the new configuration; a stopped one stays stopped. --mcp-invocable / --no-mcp-invocable decide whether AI agents (loch mcp) may invoke it. Secrets are changed with loch functions secrets set. On the custom runtime (provided), --handler "" resets the handler to its default, bootstrap. ``` loch functions update [--memory] [--timeout] [--cpus] [--tmp-size] [--handler] [--env K=V]... [--unset-env K]... [flags] ``` #### Examples ``` loch functions update thumbnail --memory 1G --timeout 60 loch functions update thumbnail --env LOG_LEVEL=debug --unset-env OLD_FLAG ``` #### Options ``` --cpus float CPU limit (0.1 to 16, in steps of 0.01) --env stringArray Set a variable NAME=value (repeatable) --handler string New handler (provided: "" resets it to bootstrap) -h, --help help for update --mcp-invocable Let AI agents (loch mcp, functions_invoke) invoke it --memory string Memory limit in MB, or with a unit (512M, 1G); see loch functions config --no-mcp-invocable Stop letting AI agents invoke it --timeout int Timeout of an invocation in seconds (1 to 900) --tmp-size string Size of /tmp in MB, or with a unit; counted in the memory limit --unset-env stringArray Remove a variable (repeatable) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch functions](loch_functions.md) - Manage functions: handlers written for AWS Lambda (Node.js, Python, Go/Rust bootstraps) run on the endpoint --- ## loch hosting Manage hosted web stacks (admin) #### Synopsis Manage hosted web stacks (PHP / Nginx / Apache sites) through the hosting plugin. Requires an admin API key with the hosting:read / hosting:write scopes. #### Options ``` -h, --help help for hosting ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel * [loch hosting check-name](loch_hosting_check-name.md) - Check whether a stack name is available * [loch hosting create](loch_hosting_create.md) - Create and deploy a hosted stack * [loch hosting delete](loch_hosting_delete.md) - Delete a hosted stack * [loch hosting deploy](loch_hosting_deploy.md) - Redeploy a hosted stack as it is on disk * [loch hosting get](loch_hosting_get.md) - Show the settings, containers and last operation of a stack * [loch hosting list](loch_hosting_list.md) - List hosted stacks * [loch hosting php-modules](loch_hosting_php-modules.md) - List the PHP modules of an image (may pull the image on first use) * [loch hosting sites](loch_hosting_sites.md) - List site directories and the stack using each * [loch hosting templates](loch_hosting_templates.md) - List the available templates and PHP versions * [loch hosting update](loch_hosting_update.md) - Change the settings of a hosted stack and redeploy it --- ## loch hosting check-name Check whether a stack name is available ``` loch hosting check-name [flags] ``` #### Options ``` -h, --help help for check-name ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch hosting create Create and deploy a hosted stack #### Synopsis Create a hosted stack and deploy it. Settings that are not given default to the server defaults (see `loch hosting templates`). ``` loch hosting create [flags] ``` #### Examples ``` loch hosting create blog --template php-nginx --php 8.4 --domain blog.example.com --https --redirect-https loch hosting create docs --template nginx --docroot public --domain docs.example.com loch hosting create shop --network services --network shop-backend ``` #### Options ``` --asset-dir stringArray Symfony routing: directory served without front controller fallback (repeatable, replaces the list) --cpus float CPU limit of the web container, e.g. 0.5 or 1 (0 = unlimited) --disable-module loch hosting php-modules Disable a PHP module (repeatable, see loch hosting php-modules) --display-errors PHP display_errors --docroot string Document root, relative to the site directory -d, --domain stringArray Domain served by the stack (repeatable) --enable-module stringArray Re-enable a disabled PHP module, e.g. ioncube (repeatable) -e, --env stringArray Environment variable KEY=VALUE (repeatable) -h, --help help for create --https Serve the domains over HTTPS --max-execution-time int PHP max_execution_time of web requests in seconds (the PHP CLI is never time-limited) --memory int Memory limit of the web container in MB (0 = unlimited) --memory-limit string PHP memory_limit of the CLI (crons, console), also the default for web requests (e.g. 256M) --network loch networks ls Network the site joins (repeatable; see loch networks ls). Default: the server default, "services" when the services module is enabled --no-network Join no network (not even the default one) --no-wait Return as soon as the operation is started --php loch hosting templates PHP version (see loch hosting templates) --php-ini-file string File holding extra php.ini directives (empty file clears them) --post-max string PHP post_max_size (e.g. 64M) --redirect-https Redirect HTTP to HTTPS (requires --https) --routing string Routing: default, symfony or laravel (PHP templates; use the framework's public directory as --docroot), or custom (see --routing-file) --routing-file string File holding custom routing rules (implies --routing custom) -s, --site string Site directory (default: the stack name) -t, --template string Template: php-nginx, php-apache, nginx, apache (default "php-nginx") --timezone string PHP date.timezone (e.g. Europe/Paris) --upload-max string PHP upload_max_filesize (e.g. 64M) --vhost-file string File holding extra vhost directives (empty file clears them) --web-memory-limit string PHP memory_limit of web requests only (empty = same as --memory-limit) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch hosting delete Delete a hosted stack #### Synopsis Delete a hosted stack. The site files are kept unless --purge-data is given. ``` loch hosting delete [flags] ``` #### Options ``` -h, --help help for delete --no-wait Return as soon as the operation is started --purge-data Also delete the site directory and all its files -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch hosting deploy Redeploy a hosted stack as it is on disk ``` loch hosting deploy [flags] ``` #### Options ``` --force Deploy a compose file modified outside the panel as is -h, --help help for deploy --no-wait Return as soon as the operation is started --pull Pull the latest image before deploying ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch hosting get Show the settings, containers and last operation of a stack ``` loch hosting get [flags] ``` #### Options ``` --compose Print only the compose.yaml of the stack, as it is on disk -h, --help help for get -v, --verbose Show environment values, snippet contents and the compose output of the last operation ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch hosting list List hosted stacks ``` loch hosting list [flags] ``` #### Options ``` -h, --help help for list ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch hosting php-modules List the PHP modules of an image (may pull the image on first use) ``` loch hosting php-modules [flags] ``` #### Options ``` -h, --help help for php-modules --php string PHP version (default: the server default) -t, --template string PHP template: php-nginx or php-apache (default "php-nginx") ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch hosting sites List site directories and the stack using each ``` loch hosting sites [flags] ``` #### Options ``` -h, --help help for sites ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch hosting templates List the available templates and PHP versions ``` loch hosting templates [flags] ``` #### Options ``` -h, --help help for templates ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch hosting update Change the settings of a hosted stack and redeploy it #### Synopsis Change the settings of a hosted stack and redeploy it. Only the given flags are changed; the other settings are kept. If the redeployment fails, the previous configuration is restored. ``` loch hosting update [flags] ``` #### Examples ``` loch hosting update blog --domain www.blog.example.com --remove-domain old.example.com loch hosting update blog --php 8.5 --memory-limit 512M --env APP_ENV=prod --unset-env DEBUG loch hosting update blog --network services --network blog-queue ``` #### Options ``` --asset-dir stringArray Symfony routing: directory served without front controller fallback (repeatable, replaces the list) --cpus float CPU limit of the web container, e.g. 0.5 or 1 (0 = unlimited) --disable-module loch hosting php-modules Disable a PHP module (repeatable, see loch hosting php-modules) --display-errors PHP display_errors --docroot string Document root, relative to the site directory -d, --domain stringArray Add a domain (repeatable) --enable-module stringArray Re-enable a disabled PHP module, e.g. ioncube (repeatable) -e, --env stringArray Set an environment variable KEY=VALUE (repeatable) --force Overwrite changes made to the compose file outside the panel -h, --help help for update --https Serve the domains over HTTPS --max-execution-time int PHP max_execution_time of web requests in seconds (the PHP CLI is never time-limited) --memory int Memory limit of the web container in MB (0 = unlimited) --memory-limit string PHP memory_limit of the CLI (crons, console), also the default for web requests (e.g. 256M) --network loch networks ls Network the site joins (repeatable, replaces the list; see loch networks ls) --no-network Leave every network --no-wait Return as soon as the operation is started --php loch hosting templates PHP version (see loch hosting templates) --php-ini-file string File holding extra php.ini directives (empty file clears them) --post-max string PHP post_max_size (e.g. 64M) --redirect-https Redirect HTTP to HTTPS (requires --https) --remove-domain stringArray Remove a domain (repeatable) --routing string Routing: default, symfony or laravel (PHP templates; use the framework's public directory as --docroot), or custom (see --routing-file) --routing-file string File holding custom routing rules (implies --routing custom) -t, --template string Template: php-nginx, php-apache, nginx, apache --timezone string PHP date.timezone (e.g. Europe/Paris) --unset-env stringArray Remove an environment variable by name (repeatable) --upload-max string PHP upload_max_filesize (e.g. 64M) --vhost-file string File holding extra vhost directives (empty file clears them) --web-memory-limit string PHP memory_limit of web requests only (empty = same as --memory-limit) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch hosting](loch_hosting.md) - Manage hosted web stacks (admin) --- ## loch inspect Display detailed information on a container ``` loch inspect [flags] ``` #### Options ``` -h, --help help for inspect ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch kill Kill one or more containers ``` loch kill [flags] ``` #### Options ``` -h, --help help for kill ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch login Configure API token interactively ``` loch login [flags] ``` #### Options ``` -h, --help help for login ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch logs Display container logs #### Synopsis Display container logs (stdout/stderr). If a filepath is specified, display the content of that file inside the container using docker exec tail. Requires the 'tail' command to be available in the container. Examples: loch logs php # Docker stdout/stderr logs loch logs php /var/log/app.log # Internal file (last 1000 lines) loch logs -f php /var/log/app.log # Follow internal file in real-time loch logs -n 50 php /var/log/app.log # Last 50 lines of internal file ``` loch logs [filepath] [flags] ``` #### Options ``` -f, --follow Follow logs in real-time -h, --help help for logs -n, --tail int Number of lines to show (0 = all) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch networks Manage the private networks between sites and services #### Synopsis Manage the private networks (admin only): internal Docker networks, without Internet access, that connect hosted sites and service instances. A site only reaches the instances of its networks. "services" is the shared network of the services module: every site on it reaches the instances on it (it cannot be deleted). A private network isolates a group, e.g. a site and its queue. Attaching or detaching a member recreates its container (a few seconds of downtime for it only). #### Options ``` -h, --help help for networks ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel * [loch networks attach](loch_networks_attach.md) - Connect a site or a service instance to a network * [loch networks config](loch_networks_config.md) - Show the creation rules of the networks * [loch networks create](loch_networks_create.md) - Create a private network * [loch networks delete](loch_networks_delete.md) - Delete a private network without members * [loch networks detach](loch_networks_detach.md) - Disconnect a site or a service instance from a network * [loch networks disconnect](loch_networks_disconnect.md) - Disconnect a container the panel does not manage from a network * [loch networks docker](loch_networks_docker.md) - Show the other Docker networks (read-only) * [loch networks ls](loch_networks_ls.md) - List the networks * [loch networks show](loch_networks_show.md) - Show a network and its members * [loch networks update](loch_networks_update.md) - Change the description of a network --- ## loch networks attach Connect a site or a service instance to a network #### Synopsis Connect a hosted site or a service instance to a network. Its container is recreated: a few seconds of downtime for it only. A site is redeployed (followed until it finishes, unless --no-wait); an instance is recreated before the command returns (up to a few minutes). ``` loch networks attach --site | --instance [flags] ``` #### Examples ``` loch networks attach shop --site shop loch networks attach shop --instance elasticmq-jobs ``` #### Options ``` -h, --help help for attach -i, --instance string Service instance (e.g. postgres-17, see loch services instances) --no-wait Site: return as soon as its redeployment is started -s, --site string Hosted site (stack name, see loch hosting ls) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch networks config Show the creation rules of the networks #### Synopsis Show the creation rules of the private networks: how many exist out of the limit, the subnet sizes, the private ranges a subnet must be in and the reserved ranges (LAN, VPN: NETWORKS_RESERVED_RANGES) it must not overlap. ``` loch networks config [flags] ``` #### Options ``` -h, --help help for config ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch networks create Create a private network #### Synopsis Create an internal network (no Internet access) named loch-net-<name> in Docker. Names: lowercase letters, digits and dashes, starting with a letter (24 characters max). The subnet is optional (Docker picks one): a private IPv4 range from /16 to /28, outside the reserved ranges (LAN, VPN) and the other Docker networks; it cannot change afterwards. See the rules with loch networks config. ``` loch networks create [--subnet CIDR] [--description TEXT] [flags] ``` #### Examples ``` loch networks create shop --description "Shop and its queue" loch networks create crm --subnet 10.42.0.0/24 ``` #### Options ``` --description string Free description -h, --help help for create --subnet string IPv4 subnet in CIDR notation, e.g. 10.42.0.0/24 (default: chosen by Docker) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch networks delete Delete a private network without members #### Synopsis Delete a private network. It must have no member left (detach them first); the network "services" cannot be deleted. ``` loch networks delete [flags] ``` #### Examples ``` loch networks delete shop ``` #### Options ``` -h, --help help for delete -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch networks detach Disconnect a site or a service instance from a network #### Synopsis Disconnect a hosted site or a service instance from a network: it no longer reaches (or is no longer reachable from) the other members. Its container is recreated. An instance keeps at least one network. ``` loch networks detach --site | --instance [flags] ``` #### Examples ``` loch networks detach services --site blog loch networks detach shop --instance elasticmq-jobs ``` #### Options ``` -h, --help help for detach -i, --instance string Service instance (e.g. postgres-17, see loch services instances) --no-wait Site: return as soon as its redeployment is started -s, --site string Hosted site (stack name, see loch hosting ls) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch networks disconnect Disconnect a container the panel does not manage from a network #### Synopsis Disconnect from a network a container the panel does not manage (shown as unmanaged by loch networks show, e.g. a container connected by hand or by a CI job). Sites and instances leave a network with loch networks detach. ``` loch networks disconnect [flags] ``` #### Examples ``` loch networks disconnect shop 3f2a9c1d7e4b ``` #### Options ``` -h, --help help for disconnect -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch networks docker Show the other Docker networks (read-only) #### Synopsis Without argument, list the Docker networks the panel did not create: Docker's own networks (bridge, host, none), those of compose projects (CI/CD stacks, the reverse proxy...) and those created by hand. With a name or an ID prefix, show one of them and its containers. Read-only: the panel never changes them. ``` loch networks docker [] [flags] ``` #### Examples ``` loch networks docker loch networks docker traefik_default loch networks docker 3f2a9c ``` #### Options ``` -h, --help help for docker ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch networks ls List the networks #### Synopsis List the networks of the panel. --all also lists the other Docker networks (read-only: created by Docker, by a compose project or by hand), as loch networks docker does. ``` loch networks ls [--all] [flags] ``` #### Examples ``` loch networks ls loch networks ls --all ``` #### Options ``` -a, --all Also list the other Docker networks (read-only) -h, --help help for ls ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch networks show Show a network and its members ``` loch networks show [flags] ``` #### Examples ``` loch networks show services loch networks show shop ``` #### Options ``` -h, --help help for show ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch networks update Change the description of a network #### Synopsis Change the description of a network (empty clears it). Docker cannot change the subnet of a network. ``` loch networks update --description TEXT [flags] ``` #### Examples ``` loch networks update shop --description "Shop, its queue and its cache" ``` #### Options ``` --description string New description (empty clears it) -h, --help help for update ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch networks](loch_networks.md) - Manage the private networks between sites and services --- ## loch ns List or switch namespace (Docker Compose stack) #### Synopsis Without arguments, lists available stacks. With an argument, switches the active stack. ``` loch ns [stack] [flags] ``` #### Options ``` -h, --help help for ns ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch proxy Forward local ports to a container via WebSocket tunnel (i.e. get container port 80 on port 8000 in your machine) #### Synopsis Create a TCP-over-WebSocket tunnel to a container. Remote ports become accessible locally, even if they are not exposed. Examples: loch proxy postgres 5432:15432 loch proxy myapp 8080:8080 5432:15432 ``` loch proxy [ ...] [flags] ``` #### Options ``` -h, --help help for proxy ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch ps List containers in the active namespace ``` loch ps [flags] ``` #### Options ``` -a, --all Show all namespaces -h, --help help for ps ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch restart Restart one or more containers ``` loch restart [flags] ``` #### Options ``` -h, --help help for restart ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch rm Remove one or more containers ``` loch rm [flags] ``` #### Options ``` -h, --help help for rm ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch services Manage services: databases, caches and message brokers #### Synopsis Manage the services module (admin only): MySQL, MariaDB, PostgreSQL, MongoDB, RabbitMQ (shared: one instance per version, started with its first resource and removed, with its data, with its last one) and Redis, Valkey, NATS, Loch S3 object storage, ElasticMQ (SQS) and Mosquitto (MQTT) queues (dedicated: one container per resource, deleted with it). #### Options ``` -h, --help help for services ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel * [loch services create](loch_services_create.md) - Create a database, cache or message broker resource (password shown once) * [loch services delete](loch_services_delete.md) - Delete a resource (database, vhost or dedicated instance) * [loch services engines](loch_services_engines.md) - List the engines and versions available on the endpoint * [loch services expose](loch_services_expose.md) - Publish the port of an instance on the host (admin, services:expose scope) * [loch services instances](loch_services_instances.md) - List the instances (shared: one per engine version; dedicated: one per resource) * [loch services ls](loch_services_ls.md) - List the resources (databases, vhosts, dedicated instances) * [loch services networks](loch_services_networks.md) - Replace the networks of an instance (it is recreated) * [loch services purge](loch_services_purge.md) - Delete an instance edited by hand, with its data * [loch services reset-password](loch_services_reset-password.md) - Generate a new password for a resource user (shown once) * [loch services retire](loch_services_retire.md) - Remove an empty instance (containers and data) * [loch services unexpose](loch_services_unexpose.md) - Close the published port of an instance (admin, services:expose scope) * [loch services update](loch_services_update.md) - Change the options of a dedicated instance (Redis, Valkey, NATS, S3, ElasticMQ, Mosquitto) * [loch services updates](loch_services_updates.md) - List the updates available for the dedicated instances * [loch services upgrade](loch_services_upgrade.md) - Update a dedicated instance to the latest image of its version, or to a newer version --- ## loch services create Create a database, cache or message broker resource (password shown once) #### Synopsis Shared engines (databases, RabbitMQ): create a database (or vhost) and a user of the same name, with privileges on it only; the first resource of a version starts its instance. Names: lowercase letters, digits and _, starting with a letter (32 characters max, 16 on MySQL 5.6). Dedicated engines (Redis, Valkey, NATS, S3, ElasticMQ, Mosquitto): create a container of its own, sized with --option (see loch services engines). Names: lowercase letters and digits, starting with a letter (24 characters max). S3 (Loch S3): the access key and secret are the credentials, URLs are path-style (Laravel: AWS_USE_PATH_STYLE_ENDPOINT=true); names have at least 3 characters. Networks (--network, repeatable): the networks the instance joins when this command creates it (shared engines: the first resource of a version; dedicated engines: always). Default: the shared network "services". ElasticMQ has no authentication: give it the private network(s) of the sites allowed to reach it (see loch networks). Starting an instance can take a couple of minutes. ``` loch services create --engine --version [--option key=value ...] [flags] ``` #### Examples ``` loch services create shop --engine postgres --version 17 loch services create cache --engine redis --version 8.4 --option memory=512 --option policy=allkeys-lru loch services create media --engine s3 --version 1.4 --option quota=10240 loch services create oldcms --engine mysql --version 8.4 --legacy-auth loch services create jobs --engine elasticmq --version 1.7 --network shop ``` #### Options ``` -e, --engine string Engine: mysql, mariadb, postgres, mongodb, redis, valkey, rabbitmq, nats, s3, elasticmq or mosquitto -h, --help help for create --legacy-auth Older password mechanism for clients that lack the default one: mysql_native_password on MySQL 8.4, md5 on PostgreSQL (see LEGACY AUTH in loch services engines) --network stringArray Network the new instance joins (repeatable; default: the shared network services; required for ElasticMQ). See loch networks ls -o, --option stringToString Option of a dedicated engine, key=value (repeatable; see loch services engines) (default []) --show-password Print the password even when stdout is not a terminal (e.g. to capture it) --version string Engine version (see loch services engines) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services delete Delete a resource (database, vhost or dedicated instance) #### Synopsis Delete a database (or vhost) and its user permanently; when it is the last one of its shared instance, the instance is removed too, with its data. A dedicated instance (Redis, Valkey, NATS, S3, ElasticMQ, Mosquitto) is deleted with its data. ``` loch services delete [flags] ``` #### Examples ``` loch services delete postgres-17 shop ``` #### Options ``` -h, --help help for delete -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services engines List the engines and versions available on the endpoint ``` loch services engines [flags] ``` #### Options ``` -h, --help help for engines ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services expose Publish the port of an instance on the host (admin, services:expose scope) #### Synopsis Publish the port of an instance on an address of the host, so that clients outside the panel reach it (e.g. 127.0.0.1:5417 for an SSH tunnel, [::1]:5417 in IPv6). The address must be allowed by the API configuration and the port free (see the policy at the end of loch services engines). Replaces the current published port. The instance is recreated: its clients are disconnected for a few seconds. Outside the loopback, TLS is required from every client of the instance (internal sites included) unless TLS is off on the endpoint; --tls-optional offers it without requiring it (PostgreSQL, MySQL 8.4 and MariaDB, when the endpoint allows it). Instances out of support and engines without authentication or TLS can only be published on the loopback. ``` loch services expose
: [flags] ``` #### Examples ``` loch services expose postgres-17 127.0.0.1:5417 loch services expose mariadb-11-4 10.0.0.5:3307 --tls-optional loch services expose redis-cache [::1]:6380 ``` #### Options ``` -h, --help help for expose --tls-optional Offer TLS without requiring it (PostgreSQL, MySQL 8.4, MariaDB; only when the endpoint allows it) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services instances List the instances (shared: one per engine version; dedicated: one per resource) ``` loch services instances [flags] ``` #### Options ``` -h, --help help for instances ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services ls List the resources (databases, vhosts, dedicated instances) ``` loch services ls [flags] ``` #### Options ``` -h, --help help for ls -i, --instance string Only the resources of this instance (e.g. postgres-16) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services networks Replace the networks of an instance (it is recreated) #### Synopsis Replace the networks an instance joins: only the sites on one of them can reach it. "services" is the shared network of the module (refused for engines without authentication, such as ElasticMQ). The instance is recreated: its clients are disconnected for a few seconds, the data is kept. To add or remove a single network, see also loch networks attach / detach --instance. ``` loch services networks --network [--network ...] [flags] ``` #### Examples ``` loch services networks postgres-17 --network services --network shop loch services networks elasticmq-jobs --network shop ``` #### Options ``` -h, --help help for networks -n, --network stringArray Network the instance joins (repeatable, replaces the list) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services purge Delete an instance edited by hand, with its data #### Synopsis Delete an instance whose files were edited by hand (shown as modified), with all its data: every database or resource it held is lost (the data is archived instead with SERVICES_ARCHIVE_ON_RETIRE). The edited files are never run: the containers are removed through Docker. ``` loch services purge [flags] ``` #### Examples ``` loch services purge redis-cache ``` #### Options ``` -h, --help help for purge -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services reset-password Generate a new password for a resource user (shown once) #### Synopsis Generate a new password: the old one stops working and open connections are closed. Dedicated instances (Redis, Valkey, NATS, S3, Mosquitto) are restarted to apply it (a few seconds of unavailability). ``` loch services reset-password [flags] ``` #### Examples ``` loch services reset-password postgres-17 shop ``` #### Options ``` -h, --help help for reset-password --show-password Print the password even when stdout is not a terminal (e.g. to capture it) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services retire Remove an empty instance (containers and data) ``` loch services retire [flags] ``` #### Examples ``` loch services retire mariadb-11-4 ``` #### Options ``` -h, --help help for retire -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services unexpose Close the published port of an instance (admin, services:expose scope) #### Synopsis Close the published port of an instance: clients outside the panel can no longer reach it. The instance is recreated: its clients are disconnected for a few seconds. Sites keep reaching it on its networks. ``` loch services unexpose [flags] ``` #### Examples ``` loch services unexpose postgres-17 ``` #### Options ``` -h, --help help for unexpose -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services update Change the options of a dedicated instance (Redis, Valkey, NATS, S3, ElasticMQ, Mosquitto) #### Synopsis Change the size or settings of a dedicated instance (see loch services engines for the options and which ones can change). The instance is recreated with them: a few seconds of unavailability, data kept. On failure, the previous options are restored. ``` loch services update --option key=value [...] [flags] ``` #### Examples ``` loch services update redis-cache --option memory=1024 --option policy=allkeys-lru ``` #### Options ``` -h, --help help for update -o, --option stringToString Option to change, key=value (repeatable) (default []) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services updates List the updates available for the dedicated instances #### Synopsis List the dedicated instances (Redis, Valkey, NATS, Loch S3, ElasticMQ, Mosquitto) with a newer image of their version or newer versions, with the end of support of each. The API discovers the versions on the registries periodically; --check asks for a discovery now. Apply one with loch services upgrade. ``` loch services updates [flags] ``` #### Examples ``` loch services updates --check ``` #### Options ``` --check Check the registries now (otherwise the last periodic check is shown) -h, --help help for updates ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch services upgrade Update a dedicated instance to the latest image of its version, or to a newer version #### Synopsis Update a dedicated instance (Redis, Valkey, NATS, Loch S3, ElasticMQ, Mosquitto): the new image is pulled, then the instance is recreated with it (its clients are disconnected for a few seconds, the data is kept). Without --version: the latest image of its current version. With --version: a newer version offered by loch services updates. An instance never goes back to an older version (data written by a newer version may not be readable by an older one). If the new image does not start, the previous one is restored. Shared instances (databases, RabbitMQ) are not updated this way. ``` loch services upgrade [--version ] [flags] ``` #### Examples ``` loch services upgrade redis-cache loch services upgrade redis-cache --version 8.10 ``` #### Options ``` -h, --help help for upgrade --version string Newer version to move to (see loch services updates; default: the latest image of the current version) -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch services](loch_services.md) - Manage services: databases, caches and message brokers --- ## loch sftp Manage SFTPGo users #### Options ``` -h, --help help for sftp ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel * [loch sftp create](loch_sftp_create.md) - Create an SFTP user * [loch sftp delete](loch_sftp_delete.md) - Delete an SFTP user * [loch sftp disable](loch_sftp_disable.md) - Disable an SFTP user * [loch sftp enable](loch_sftp_enable.md) - Enable an SFTP user * [loch sftp reset-password](loch_sftp_reset-password.md) - Reset an SFTP user's password * [loch sftp sites](loch_sftp_sites.md) - List hosted sites * [loch sftp users](loch_sftp_users.md) - List SFTP users --- ## loch sftp create Create an SFTP user ``` loch sftp create [flags] ``` #### Options ``` --bandwidth int Bandwidth limit in KB/s (0 = unlimited) -h, --help help for create --init-dirs Initialize the configured directories (e.g. web, logs) in the user's home (default true) -m, --must-change-password Require a password change at next login -p, --password string Password (unsafe: visible in 'ps'; prefer the interactive prompt) --quota-files int Quota in number of files (0 = unlimited) --quota-size int Quota size in bytes (0 = unlimited) -s, --site string Site name (required) --ssh-key string SSH public key --subpath string Restrict the user to a sub-directory of the site (default: whole site) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch sftp](loch_sftp.md) - Manage SFTPGo users --- ## loch sftp delete Delete an SFTP user ``` loch sftp delete [flags] ``` #### Options ``` -h, --help help for delete -y, --yes Skip the confirmation prompt ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch sftp](loch_sftp.md) - Manage SFTPGo users --- ## loch sftp disable Disable an SFTP user ``` loch sftp disable [flags] ``` #### Options ``` -h, --help help for disable ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch sftp](loch_sftp.md) - Manage SFTPGo users --- ## loch sftp enable Enable an SFTP user ``` loch sftp enable [flags] ``` #### Options ``` -h, --help help for enable ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch sftp](loch_sftp.md) - Manage SFTPGo users --- ## loch sftp reset-password Reset an SFTP user's password ``` loch sftp reset-password [flags] ``` #### Options ``` -h, --help help for reset-password -p, --password string New password (unsafe: visible in 'ps'; prefer the interactive prompt) ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch sftp](loch_sftp.md) - Manage SFTPGo users --- ## loch sftp sites List hosted sites ``` loch sftp sites [flags] ``` #### Options ``` -h, --help help for sites ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch sftp](loch_sftp.md) - Manage SFTPGo users --- ## loch sftp users List SFTP users ``` loch sftp users [flags] ``` #### Options ``` -h, --help help for users -s, --site string Filter by site ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch sftp](loch_sftp.md) - Manage SFTPGo users --- ## loch shell Open an interactive shell in a container ``` loch shell [flags] ``` #### Options ``` -h, --help help for shell ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch stack Start, stop or restart a whole Docker Compose stack #### Options ``` -h, --help help for stack ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel * [loch stack restart](loch_stack_restart.md) - Restart every container of a stack * [loch stack start](loch_stack_start.md) - Start every container of a stack * [loch stack stop](loch_stack_stop.md) - Stop every container of a stack --- ## loch stack restart Restart every container of a stack #### Synopsis Restart every container of a stack. Without argument, the active namespace is used. Containers are processed in dependency order (depends_on). ``` loch stack restart [stack] [flags] ``` #### Options ``` -h, --help help for restart ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch stack](loch_stack.md) - Start, stop or restart a whole Docker Compose stack --- ## loch stack start Start every container of a stack #### Synopsis Start every container of a stack. Without argument, the active namespace is used. Containers are processed in dependency order (depends_on). ``` loch stack start [stack] [flags] ``` #### Options ``` -h, --help help for start ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch stack](loch_stack.md) - Start, stop or restart a whole Docker Compose stack --- ## loch stack stop Stop every container of a stack #### Synopsis Stop every container of a stack. Without argument, the active namespace is used. Containers are processed in dependency order (depends_on). ``` loch stack stop [stack] [flags] ``` #### Options ``` -h, --help help for stop ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch stack](loch_stack.md) - Start, stop or restart a whole Docker Compose stack --- ## loch start Start one or more containers ``` loch start [flags] ``` #### Options ``` -h, --help help for start ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch stats Live resource monitoring for the active namespace ``` loch stats [service...] [flags] ``` #### Options ``` -h, --help help for stats ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch stop Stop one or more containers ``` loch stop [flags] ``` #### Options ``` -h, --help help for stop ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch update Update loch to the latest version ``` loch update [flags] ``` #### Options ``` --check Only check for updates, don't install -h, --help help for update ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch use Switch active endpoint ``` loch use [flags] ``` #### Options ``` -h, --help help for use ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## loch users Manage users (admin) #### Options ``` -h, --help help for users ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel * [loch users demote](loch_users_demote.md) - Demote a user to restricted * [loch users grant](loch_users_grant.md) - Grant access to one or more services * [loch users ls](loch_users_ls.md) - List users * [loch users promote](loch_users_promote.md) - Promote a user to admin * [loch users revoke](loch_users_revoke.md) - Revoke access to one or more services * [loch users rm](loch_users_rm.md) - Delete a user --- ## loch users demote Demote a user to restricted ``` loch users demote [flags] ``` #### Options ``` -h, --help help for demote ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch users](loch_users.md) - Manage users (admin) --- ## loch users grant Grant access to one or more services ``` loch users grant [flags] ``` #### Options ``` -h, --help help for grant ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch users](loch_users.md) - Manage users (admin) --- ## loch users ls List users ``` loch users ls [flags] ``` #### Options ``` -h, --help help for ls ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch users](loch_users.md) - Manage users (admin) --- ## loch users promote Promote a user to admin ``` loch users promote [flags] ``` #### Options ``` -h, --help help for promote ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch users](loch_users.md) - Manage users (admin) --- ## loch users revoke Revoke access to one or more services ``` loch users revoke [flags] ``` #### Options ``` -h, --help help for revoke ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch users](loch_users.md) - Manage users (admin) --- ## loch users rm Delete a user ``` loch users rm [flags] ``` #### Options ``` -h, --help help for rm ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch users](loch_users.md) - Manage users (admin) --- ## loch version Show loch version ``` loch version [flags] ``` #### Options ``` -h, --help help for version ``` #### Options inherited from parent commands ``` --debug Show HTTP requests ``` #### SEE ALSO * [loch](loch.md) - CLI to manage your containers via Host Panel --- ## MCP tools The full list of tools that `loch mcp` offers to an AI assistant. To set it up, see [Connect an AI assistant](../mcp.md). ### How the tools behave - Every tool acts on the **active endpoint** of the `loch` configuration. Text results start with `[endpoint: ]`. - Container tools act on the **active namespace**. No tool changes it: run `loch ns ` in a terminal, or pass `stack` to `stack_action`. - An argument the tool does not declare is refused with an error. - **Kind** gives the MCP annotations: *Read* (read-only), *Write* (changes something, not destructive), *Destructive* (the client should ask for confirmation). - Tools that name a container by its service name also need `containers:read`, to find it. - Scopes are described in [API key scopes](/panel/reference/scopes/). An admin tool fails for a restricted user, whatever the scopes of the key. ### Endpoints and containers {#containers} | Tool | Does | Main parameters | Kind | Scope | |---|---|---|---|---| | `list_endpoints` | Lists the configured endpoints and the active one. | – | Read | None | | `switch_endpoint` | Switches the active endpoint. | `name` (required) | Write | None | | `list_namespaces` | Lists the Docker Compose namespaces of the active endpoint. | – | Read | `containers:read` | | `list_containers` | Lists the containers. | `showAll` (default `false`: active namespace only) | Read | `containers:read` | | `inspect_container` | Shows the details of a container. | `container` (required) | Read | `containers:read` | | `container_action` | Starts, stops or restarts a container. | `container`, `action`: `start`, `stop`, `restart` (required) | Destructive | `containers:actions` | | `stack_action` | Starts, stops or restarts every container of a stack, in dependency order. | `action` (required), `stack` (default: active namespace) | Destructive | `containers:actions` | | `get_log` | Returns the last lines of the container output, or of a file inside it. | `container` (required), `lines` (default 10), `filePath` | Read | `containers:logs` | | `get_stats` | One-shot CPU, memory, network and PID snapshot of the running containers of the namespace. | `container` (default: all) | Read | `containers:stats` | | `get_domains` | Lists the Traefik domains and their routing. | – | Read | Admin (profile only) | ### Scheduled jobs {#cron} Not admin only. A restricted user can only target the containers it was granted. Scope `cron` for every tool. | Tool | Does | Main parameters | Kind | |---|---|---|---| | `cron_list` | Lists the jobs. Last errors are hidden: see `cron_get`. | – | Read | | `cron_get` | Shows a job: schedule, settings, last run. | `name` (name or ID, required), `reveal_secrets` (HTTP header values and body, event of a `function_invoke` job), `include_output` (last output and error) | Read | | `cron_create` | Creates a job. Names are unique. | `name`, `schedule`, `type` (required); settings of the type (below); `enabled` (default `true`), `once` (default `false`) | Write | | `cron_update` | Changes the given settings of a job. Changing `type` needs the settings of the new type. | `name` (required), `new_name`, any setting of `cron_create` | Destructive | | `cron_toggle` | Enables or disables a job. | `name`, `enabled` (required) | Destructive | | `cron_run` | Runs a job now, in the background. Check the result with `cron_get`. | `name` (required) | Destructive | | `cron_delete` | Deletes a job (irreversible). | `name` (required) | Destructive | Job settings: | Parameter | Applies to | Value | |---|---|---| | `schedule` | All | 5-field cron expression (`0 3 * * *`) or descriptor (`@hourly`, `@daily`, `@every 5m`); at most once a minute | | `type` | All | `docker_action`, `docker_exec` or `http` (`function_invoke` jobs are created with the panel or `loch cron create --function`, not through MCP) | | `container` | `docker_action`, `docker_exec` | Container name, as in `list_containers` (e.g. `myapp-web-1`) | | `action` | `docker_action` | `start`, `stop`, `restart`, `kill` | | `command` | `docker_exec` | Array, run without shell, e.g. `["php", "bin/console", "app:sync"]`; use `["sh", "-c", "..."]` for shell syntax | | `working_dir` | `docker_exec` | Working directory in the container | | `url` | `http` | http(s) URL; private and internal addresses are blocked by the server | | `method` | `http` | `GET` (default), `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS` | | `headers` | `http` | Object `{Name: value}`; replaces all the headers | | `body` | `http` | Request body | | `timeout` | `http` | Seconds; `0` = server default; capped by the server (`CRON_HTTP_TIMEOUT`, 900 by default) | The settings of another type are refused. ### Web hosting {#hosting} Admin only; the hosting module must be enabled. Reading needs `hosting:read`; changing needs `hosting:read` and `hosting:write`. Panel side: [Web hosting](/panel/admin/hosting/). | Tool | Does | Main parameters | Kind | Scope | |---|---|---|---|---| | `hosting_list` | Lists the sites with their status. | – | Read | `hosting:read` | | `hosting_get` | Shows a site: settings, containers, routing, last operation and its compose output. Secrets are hidden by default. | `name` (required), `reveal_env` (environment values, snippets, routing rules), `include_compose` | Read | `hosting:read` | | `hosting_templates` | Lists the templates, PHP versions with support dates, defaults and limits. | – | Read | `hosting:read` | | `hosting_sites` | Lists the site directories and the site using each. | – | Read | `hosting:read` | | `hosting_php_modules` | Lists the PHP modules of an image (pulls it on first use, may take a minute). | `template`: `php-nginx`, `php-apache`; `php_version` (required) | Read | `hosting:read` | | `hosting_check_name` | Checks whether a name is free. | `name` (required) | Read | `hosting:read` | | `hosting_create` | Creates and deploys a site. Unset settings take the server defaults. | `name`, `template`: `php-nginx`, `php-apache`, `nginx`, `apache` (required); `site`; settings (below) | Write | `hosting:write` | | `hosting_update` | Changes the given settings and redeploys. Rolled back on failure. | `name` (required), `template`, `force`; settings (below); `add_domains`, `remove_domains`, `set_env`, `unset_env`, `disable_modules`, `enable_modules` | Destructive | `hosting:write` | | `hosting_deploy` | Redeploys the site as it is on disk (recreates its container). | `name` (required), `pull`, `force`, `wait` | Destructive | `hosting:write` | | `hosting_action` | Starts, stops or restarts the containers of a site. | `name`, `action`: `start`, `stop`, `restart` (required) | Destructive | `hosting:read` and `containers:actions` | | `hosting_delete` | Deletes the site (containers and compose project). Files are kept unless `purge_data`. | `name` (required), `purge_data`, `wait` | Destructive | `hosting:write` | Site settings of `hosting_create` and `hosting_update`: | Parameter | Value | |---|---| | `php_version` | PHP version tag (see `hosting_templates`); PHP templates only | | `document_root` | Web root relative to the site directory, e.g. `web/public`; empty = site root | | `domains` | Domains served (on update, replaces the list: prefer `add_domains` / `remove_domains`) | | `https`, `redirect_https` | Serve over HTTPS (once DNS points to the server); redirect HTTP to HTTPS (needs `https`) | | `networks` | Networks the site joins (replaces the list). Default at creation: the server default (`["services"]` when the services module is enabled); `[]` joins none | | `routing` | `default`, `symfony`, `laravel` (PHP templates) or `custom` | | `routing_rules` | Custom rules (implies `routing=custom`): Nginx rules replacing `location /`, or Apache directives | | `asset_dirs` | Symfony routing: directories served without front controller fallback (default `[bundles]`) | | `memory_limit`, `web_memory_limit` | PHP `memory_limit` of the CLI (also the web default), and of web requests only | | `upload_max_filesize`, `post_max_size` | PHP upload limits (`post_max_size` ≥ `upload_max_filesize`) | | `max_execution_time` | PHP web request time limit in seconds (0–3600); the PHP CLI is never limited | | `display_errors`, `timezone` | PHP `display_errors` and `date.timezone` | | `disabled_modules` | PHP modules to disable (on update, replaces the list); `ioncube` is disabled by default | | `memory_mb`, `cpus` | Container memory (MB) and CPU limits; `0` = unlimited | | `env` | Environment variables `{NAME: value}` (on update, replaces them all: prefer `set_env` / `unset_env`) | | `php_ini`, `vhost` | Extra php.ini directives and vhost directives (replace the snippet) | | `wait` | Wait for the deployment (default `true`, at most 5 minutes) | PHP settings are refused on the `nginx` and `apache` templates. When files were edited outside the panel, the tools fail: check them with `hosting_get`, then retry with `force=true` if overwriting them is acceptable. ### SFTP users {#sftp} Admin only; the SFTP module must be enabled. Panel side: [SFTP](/panel/admin/sftp/). | Tool | Does | Main parameters | Kind | Scope | |---|---|---|---|---| | `sftp_sites` | Lists the sites with their user count and used space. | – | Read | `sftp:read` | | `sftp_users` | Lists the users: site, sub-directory, status, quotas, last login. Passwords are never returned. | `site` | Read | `sftp:read` | | `sftp_create` | Creates a user in a site. The site directory is created with its first user. | `username`, `site` (required); `sub_path` (cannot change later), `password` (8 characters min; omit to generate one), `initialize_dirs` (default `true`), limits (below) | Write | `sftp:write` | | `sftp_update` | Changes the given limits of a user. | `username` (required), limits (below) | Destructive | `sftp:write` | | `sftp_reset_password` | Sets a new password. | `username` (required), `password` (omit to generate one) | Destructive | `sftp:write` | | `sftp_set_enabled` | Enables or disables a user. A disabled user cannot log in. | `username`, `enabled` (required) | Write | `sftp:write` | | `sftp_delete` | Deletes the account. The files of its home directory are kept. | `username` (required) | Destructive | `sftp:write` | Limits: `public_key` (a single SSH key, replacing all of them; empty removes them on update), `quota_size_mb`, `quota_files`, `bandwidth_kbps` (`0` = unlimited), `require_password_change`. A generated password is returned once as `generatedPassword` and enters the conversation transcript. Pass a password instead if that is not acceptable. A given password is never echoed. ### Databases and services {#services} Admin only; the services module must be enabled. Panel side: [Services](/panel/admin/services/). | Tool | Does | Main parameters | Kind | Scope | |---|---|---|---|---| | `services_engines` | Lists the engines, their options and versions, with their end of support. | – | Read | `services:read` | | `services_instances` | Lists the instances: state, networks, published port, end of support, available update (dedicated ones). | – | Read | `services:read` | | `services_databases` | Lists the resources of an instance. Passwords are never returned. | `instance` (required, e.g. `postgres-17`) | Read | `services:read` | | `services_create_database` | Creates a database or RabbitMQ vhost with its user (shared engines), or a dedicated instance. | `engine`, `version`, `name` (required); `options` (`key=value,...`, dedicated engines), `legacy_auth`, `networks` | Write | `services:write` | | `services_update_instance` | Changes the options of a dedicated instance, e.g. its memory. The instance is recreated; data kept. | `instance`, `options` (required) | Write | `services:write` | | `services_upgrade_instance` | Updates a dedicated instance to the latest image of its current version. The instance is recreated; data kept. | `instance` (required) | Write | `services:write` | Engines: `mysql`, `mariadb`, `postgres`, `mongodb`, `rabbitmq` (shared) and `redis`, `valkey`, `nats`, `s3`, `elasticmq`, `mosquitto` (dedicated). ElasticMQ has no authentication: it is refused on the `services` network, so pass a private network in `networks`. Passwords are never returned to the assistant. The user runs `loch services reset-password ` in a terminal. ### Private networks {#networks} Admin only. Panel side: [Networks](/panel/admin/networks/). | Tool | Does | Main parameters | Kind | Scope | |---|---|---|---|---| | `networks_list` | Lists the private networks: members, state, reserved range overlap. | – | Read | `networks:read` | | `networks_get` | Shows a network and its members. | `name` (required) | Read | `networks:read` | | `networks_config` | Shows the creation rules: count and max, subnet sizes, private and reserved ranges. | – | Read | `networks:read` | | `networks_docker_list` | Lists the Docker networks the panel did not create. | – | Read | `networks:read` | | `networks_docker_get` | Shows one of those networks and its containers. | `network` (name, ID or unique ID prefix, required) | Read | `networks:read` | | `networks_create` | Creates a private network. | `name` (required), `description`, `subnet` (cannot change later) | Write | `networks:write` | | `networks_attach` | Connects a site or an instance. Its container is recreated. | `network`, `kind`: `site`, `instance`, `name` (required); `wait` (sites, default `true`) | Write | `networks:write` | | `networks_detach` | Disconnects a site or an instance. An instance keeps at least one network. | same as `networks_attach` | Destructive | `networks:write` | ### Functions {#functions} Admin only; the functions module must be enabled (work in progress). Panel side: [Functions](/panel/user-guide/functions/). | Tool | Does | Main parameters | Kind | Scope | |---|---|---|---|---| | `functions_list` | Lists the functions: runtime, handler, limits, state, active deployment, networks, egress. | – | Read | `functions:read` | | `functions_get` | Shows a function: configuration, image, variable and secret names (never their values), deployments, endpoint, `sigv4`, `mcpInvocable`. Credentials are never returned. | `name` (required) | Read | `functions:read` | | `functions_invoke` | Invokes a function with a JSON event and returns its response. Only for the functions marked `mcpInvocable`. | `name` (required), `event` (JSON text, at most 6 MiB, default `{}`) | Destructive | `functions:invoke` (and `functions:read`) | - `mcpInvocable` is off by default. A human turns it on with `loch functions update --mcp-invocable` or **Invocable by MCP clients** in the panel. It is a guard-rail of the MCP client, not an access control: to forbid invocations, use a key without `functions:invoke`. - The payload and the logs of an invocation are returned under `untrusted`, cut at 64 KiB: the assistant must treat them as data. ### CLI only These actions have no MCP tool, on purpose or because they need a terminal: | Domain | Commands | |---|---| | Files and shell | `loch cp`, `loch shell`, `loch proxy`, `loch logs -f` | | Containers | `loch kill`, `loch rm`, `loch stats` (live; `get_stats` gives a snapshot) | | Services | `loch services delete`, `reset-password`, `expose`, `unexpose`, `retire`, `purge`, `upgrade --version` | | Networks | `loch networks update`, `delete`, `disconnect` | | Functions | `loch functions create`, `deploy`, `activate`, `update`, `secrets`, `credentials`, `networks`, `egress`, `start`, `stop`, `restart`, `delete`, `logs`, `dev` | | Users | `loch users …` | | Namespaces | `loch ns ` | --- ## Backup and restore A Loch S3 server is three things to back up: | Item | Where | Why | |---|---|---| | Data directory | `S3_DATA_DIR` (`/data` in the image) | Objects, versions, metadata, policies, ACLs, bucket owners | | Users file | `S3_USERS_FILE` | Keys of every user | | Master key | `S3_ENCRYPTION_KEY_FILE` or `S3_ENCRYPTION_KEY` | Decrypts SSE-S3 objects | :::warning Without the master key, every SSE-S3 object is lost. Store it apart from the data backup, so one stolen backup does not hold both. ::: ### Back up 1. Stop the server, or take a filesystem snapshot (LVM, ZFS, Btrfs) so the copy is consistent. 2. Copy the data directory, keeping file owners and permissions: ```bash docker compose stop s3 tar czf s3-data-$(date +%F).tar.gz -C /srv/s3 data docker compose start s3 ``` 3. Copy the users file and the master key to a separate, protected location. ### Restore 1. Stop the server. 2. Put back the data directory, the users file and the master key at the same paths. 3. Make the data directory writable by the container user: `chown -R 100:101 /srv/s3/data`. 4. Start the server. It removes leftover temporary files and recounts the storage used. ### Good to know - SSE-C objects need the client's key: the server never had it. - Restore on a case-sensitive filesystem (Linux). The server warns at startup otherwise. - Do not edit the data directory by hand: each bucket keeps its metadata, versions and settings in hidden files (`.meta/`, `.versions/`, `.policy.json`...). --- ## Production deployment ### Reverse proxy {#reverse-proxy} Loch S3 speaks plain HTTP. Put a TLS reverse proxy in front and keep the S3 port off the network. Example with Caddy: ```text s3.example.com { # The admin API is protected by a single key: keep it off the Internet @admin path /_loch/users /_loch/users/* respond @admin 403 reverse_proxy s3:8080 } ``` Clients use `https://s3.example.com` with path-style URLs. The proxy must forward the `Host` header unchanged: it is part of the signature. ### Hardened container {#hardened-container} The server only writes to `S3_DATA_DIR`. It runs with a read-only root filesystem and no capabilities: ```yaml services: s3: image: ghcr.io/loch-sh/s3:1.4.1 restart: unless-stopped read_only: true cap_drop: [ALL] security_opt: ["no-new-privileges:true"] user: "100:101" mem_limit: 128m pids_limit: 64 environment: S3_USERS_FILE: /etc/s3/users.json S3_ENCRYPTION_KEY_FILE: /run/secrets/master.key S3_ENCRYPT_BY_DEFAULT: "true" S3_MAX_STORAGE_BYTES: "107374182400" # 100 GiB S3_INIT_BUCKETS: backups,media volumes: - /srv/s3/data:/data - /srv/s3/conf:/etc/s3:ro - /srv/s3/secrets:/run/secrets:ro ``` | Mount | Mode | Content | |---|---|---| | `/data` | read-write | Buckets, objects, metadata. One filesystem, case-sensitive (Linux) | | users file | read-only | Never written while `S3_ADMIN_API_KEY` is unset | | master key | read-only | Mode `0400`, owned by the container uid | With a read-only users file, the file must already exist: the server exits instead of bootstrapping it. Users then change only by editing the file and restarting. Keep the PID limit above `S3_MAX_BLOCKING_THREADS` (32) plus the number of CPUs plus a few. ### Quota {#quota} `S3_MAX_STORAGE_BYTES` caps the whole data directory: objects, versions, multipart parts in progress and metadata files. A write that would cross it fails with `403 QuotaExceeded`; deletes still work. Versioned buckets reserve twice the object size. The quota counts file sizes, not disk blocks or inodes. For a hard disk limit, also give the data directory its own volume or filesystem quota. ### Health and usage | Endpoint | Auth | Answer | |---|---|---| | `GET` or `HEAD /_loch/health` | none | `200 {"status":"ok"}`, plus `"quota":"ok"` or `"exceeded"` when a quota is set; `503` if the data directory is unreachable | | `GET /_loch/usage` | SigV4, root | `{"used_bytes":…,"reserved_bytes":…,"limit_bytes":…}` | ```bash curl --aws-sigv4 "aws:amz:us-east-1:s3" -u "$ROOT_KEY:$ROOT_SECRET" https://s3.example.com/_loch/usage ``` ### Logs The server logs to standard error (`docker logs`): startup settings, then one line per request and per response with the method, path and status. Headers and query strings are never logged. Health probes are not logged. There is no metrics endpoint. ### Shutdown On `SIGTERM`, the server stops accepting connections and gives running requests `S3_SHUTDOWN_GRACE` seconds (8 by default, below Docker's 10-second stop timeout). An interrupted upload leaves no object. ### Good to know - Slow uploads are cut: no data for 60 seconds, or less than `S3_MIN_UPLOAD_RATE` bytes/s (4096) over 60 seconds, ends with `400 RequestTimeout`. - At most `S3_MAX_CONNECTIONS` (1024) connections at once; extra ones are closed. - HTTP/2 connections that answer pings never time out: keep the proxy in front. - Every limit: [Limits](../reference/limits.md). Every variable: [Configuration reference](../reference/configuration.md). --- ## Security (Admin) ### Checklist 1. Enable authentication: set `S3_USERS_FILE` (or `S3_ACCESS_KEY_ID` + `S3_SECRET_ACCESS_KEY`). Without it, anyone can read and write. 2. Serve the API over HTTPS only, through a reverse proxy. 3. Block `/_loch/users` at the proxy, or leave `S3_ADMIN_API_KEY` unset. 4. Read the master key from a `0400` file (`S3_ENCRYPTION_KEY_FILE`), not from the environment. 5. Keep the data directory on a case-sensitive filesystem, used only by Loch S3. 6. Run the container read-only, without capabilities, with memory and PID limits. See [Production deployment](./production.md#hardened-container). 7. Give each application its own user and grant it only the buckets it needs. ### Built-in protections | Area | Protection | |---|---| | Signatures | SigV4 checked in constant time; clock skew over 15 minutes refused; `host` and every `x-amz-*` header must be signed | | Request bodies | Signed SHA-256 and `aws-chunked` chunk signatures verified; nothing is stored on mismatch | | Presigned URLs | 1 second to 7 days; a bad signature never falls back to anonymous access | | Paths | Bucket names and keys validated; `.`, `..`, empty segments and symlinks refused | | Policies | Explicit `Deny` wins; unsupported fields refused instead of ignored; a stored policy that cannot be read denies | | Encryption | Data encrypted before it is published; plaintext never lands under a bucket | | Logs | No headers, query strings or keys in the logs | | Resources | Header and body timeouts, minimum upload rate, connection cap, quota | ### Known limits - `UNSIGNED-PAYLOAD` and trailing checksums are accepted without verification: integrity then relies on TLS. - Encrypted objects keep their parameters in a metadata file that is not authenticated. - A bucket-level `READ` grant allows downloading every object of the bucket. Full audit history and fixes: [SECURITY.md](https://github.com/loch-sh/s3/blob/1.4.1/SECURITY.md). --- ## Upgrade to 1.4.1 1.4.1 is a security release. Some fixes change how the server behaves: check them before you upgrade. ### Upgrade 1. [Back up](./backup.md) the data directory, the users file and the master key. 2. Check the changes below. 3. Change the image tag to `ghcr.io/loch-sh/s3:1.4.1` and restart: ```bash docker compose pull s3 && docker compose up -d s3 ``` ### Behavior changes | Change | Effect | What to do | |---|---|---| | Policies with `Condition`, `NotAction`, `NotResource`, `NotPrincipal` or an invalid action are refused | A policy stored by an older version with these fields no longer applies: the bucket becomes private (root and owner only) | Before upgrading, read each policy with `get-bucket-policy` and rewrite it without these fields | | Explicit `Deny` wins over ACLs | A `public-read` ACL no longer opens objects that a policy denies | Nothing, unless you relied on it | | Slow uploads are cut | An upload under 4096 bytes/s over 60 seconds fails with `RequestTimeout` | For slow links, lower `S3_MIN_UPLOAD_RATE` or set it to `0` | | The quota counts metadata files and ACLs | Reported usage grows a little | Check the margin under `S3_MAX_STORAGE_BYTES` | | Non-canonical keys are refused | Keys with `.`, `..` or empty segments (`a//b`, `./x`) fail with `400` | Objects written earlier under such keys stay reachable through their normalized key (`a/b`, `x`) | | Stricter signatures | `host` and every `x-amz-*` header must be signed; the body must match the signed hash | Standard SDKs comply. Check hand-made clients | New variables: `S3_MIN_UPLOAD_RATE` and `S3_MAX_CONNECTIONS`. See [Configuration reference](../reference/configuration.md). ### Good to know - Upgrading from 1.2 or older: a crash could leave `{name}.{uuid}.tmp` files next to objects. They now show up as ordinary objects: delete them like any object. --- ## Users and admin API Multi-user mode starts when `S3_USERS_FILE` is set. Each user has a `user_id`, an access key and a secret key. Exactly one user is `root`. ### Users file If the file does not exist, the server creates it on first start with a `root` user and random keys (mode `0600`), and prints the access key. The secret is in the file. You can also write it yourself before the first start: ```json { "users": [ {"user_id": "root", "display_name": "Root", "access_key_id": "<20 random chars>", "secret_access_key": "<40 random chars>", "is_root": true}, {"user_id": "backup", "display_name": "Backup job", "access_key_id": "<20 random chars>", "secret_access_key": "<40 random chars>"} ] } ``` Rules: one `root`, unique `user_id` and `access_key_id`, no empty field. An invalid file stops the server. Without the admin API, edit the file and restart. ### Enable the admin API Set `S3_ADMIN_API_KEY` to a long random value: ```bash openssl rand -hex 32 ``` The API is served on `/_loch/users` and authenticated with `Authorization: Bearer `. It then writes the users file, so the file must be writable. :::warning The admin API is protected by this single key. Never expose `/_loch/users` to the Internet: block it at the reverse proxy, as in [Production deployment](./production.md#reverse-proxy). ::: ### loch-s3 CLI `loch-s3` ships in the Docker image and in the release archives. ```bash export LOCH_SERVER=http://localhost:8080 LOCH_ADMIN_KEY= loch-s3 users create "Backup job" ``` `create` derives the `user_id` from the name (`backup-job`), generates the keys and prints them once. | Command | Action | |---|---| | `loch-s3 users list` | List users | | `loch-s3 users get ` | Show one user | | `loch-s3 users create ` | Create a user with generated keys (replaces the keys if it exists) | | `loch-s3 users put --display-name --access-key --secret-key ` | Create or update with your own keys | | `loch-s3 users delete ` | Delete a user | | Flag | Variable | Default | |---|---|---| | `--server` | `LOCH_SERVER` | `http://localhost:8080` | | `--api-key` | `LOCH_ADMIN_KEY` | required | | `--access-key`, `--secret-key` (`put`) | `LOCH_ACCESS_KEY`, `LOCH_SECRET_KEY` | required | Prefer the variables to the flags: flags show up in the process list. From the container: ```bash docker compose exec s3 sh -c 'loch-s3 --api-key "$S3_ADMIN_API_KEY" users list' ``` ### Good to know - Secret keys are never returned by the API: save them when you create the user. - The API cannot create, change or delete `root`. Change root keys in the file. - A new user owns no bucket. Give access with a [bucket policy](../guides/access-control.md#bucket-policies) on `arn:loch:iam:::user/`, or let the user create buckets. - Deleting a user keeps the buckets they own. A new user with the same `user_id` owns them again. - Single-user mode (`S3_ACCESS_KEY_ID`) has no admin API. Endpoints: [Admin API reference](../reference/admin-api.md). --- ## Architecture (Developer) Loch S3 is written in Rust on raw `hyper` 1 and `tokio`, without a web framework. Requests are routed by hand in `src/router.rs`. ### Request flow 1. `router.rs` parses the path (`/{bucket}/{key}`) and the sub-resource (`?policy`, `?uploads`…), and rejects non-canonical keys. 2. `auth.rs` checks the SigV4 signature and attaches the body check (signed hash or chunk signatures). 3. The router authorizes: root, bucket owner, then bucket policy (`policy.rs`) and ACLs. 4. A handler in `src/handlers/` runs the operation through `src/storage/`. 5. CORS headers are added when the bucket has a matching rule (`cors.rs`). ### Source layout | Path | Role | |---|---| | `src/main.rs` | Environment variables, startup, cleanup tasks | | `src/lib.rs` | `ServerConfig`, connection loop, HTTP/1 and HTTP/2 settings | | `src/router.rs` | Routing, authorization, ACLs, CORS preflight | | `src/auth.rs` | SigV4 headers and presigned URLs, body checks | | `src/users.rs` | Users file and in-memory user store | | `src/policy.rs` | Bucket policy model and evaluation | | `src/encryption.rs` | SSE-S3 and SSE-C, AES-256-GCM in 1 MiB chunks, HKDF-SHA256 | | `src/quota/` | Quota counter, reservations, background recount | | `src/handlers/` | One file per area: bucket, object, multipart, versioning, policy, cors, encryption, users, health | | `src/storage/` | Filesystem layer for the same areas | | `src/bin/loch-s3.rs` | User management CLI | | `tests/integration.rs` | Integration tests against a real server | ### On-disk layout | Path in `S3_DATA_DIR` | Content | |---|---| | `{bucket}/` | One directory per bucket | | `{bucket}/{key}` | Object data, one file per object (ciphertext if encrypted) | | `{bucket}/.metadata` | Bucket creation date and owner | | `{bucket}/.meta/{key}.json` | Object metadata: headers, ETag, owner, encryption parameters | | `{bucket}/.versions/`, `.versioning.json` | Versions and versioning status | | `{bucket}/.acl/`, `.acl.xml` | Object and bucket ACLs | | `{bucket}/.policy.json`, `.cors.json`, `.encryption.json` | Bucket settings | | `{bucket}/.uploads/{upload_id}/` | Multipart parts in progress | | `.loch-tmp/` | Writes in progress, renamed into place when complete; emptied at startup | ### Rules - Writes go to a temporary file, are encrypted there if needed, then published with a rename: metadata first, then data. - Bodies are streamed to disk with an incremental MD5; nothing is buffered whole. - Every write reserves its size in the quota before writing. - Headers and query strings are never logged. --- ## Build and test :::note Requires a recent stable Rust toolchain (the crate uses edition 2024). Docker is needed for the image, [`cross`](https://github.com/cross-rs/cross) for Linux and Windows cross-builds. ::: ### Build ```bash git clone https://github.com/loch-sh/s3.git && cd s3 cargo build --release ./target/release/s3 ``` The build produces two binaries: `s3` (the server) and `loch-s3` (the user CLI). ### Test ```bash cargo test # unit and integration tests cargo test test_name # one test ``` Integration tests in `tests/integration.rs` start a real server on a random port and call it with `reqwest`. ### Cross-builds | Command | Target | |---|---| | `make linux-amd64` | `x86_64-unknown-linux-musl` | | `make linux-arm64` | `aarch64-unknown-linux-musl` | | `make windows-amd64` | `x86_64-pc-windows-gnu` | | `make macos-amd64` | `x86_64-apple-darwin` (on macOS) | | `make macos-arm64` | `aarch64-apple-darwin` (on macOS) | | `make all` | All of the above | The server binary lands in `dist//`. ### Docker image ```bash make docker # image for the local architecture make docker-multiplatform # amd64 + arm64, without push ``` ### Releases CI runs the tests on every branch and tag. A tag builds the release archives, publishes the GitHub release and pushes the multi-arch image to `ghcr.io/loch-sh/s3`. --- ## Access control ### Authentication modes The server picks one mode at startup, in this order: | Mode | Set | Behavior | |---|---|---| | Multi-user | `S3_USERS_FILE` | Users from a JSON file, one of them `root`. See [Users and admin API](../admin/users.md) | | Single user | `S3_ACCESS_KEY_ID` + `S3_SECRET_ACCESS_KEY` | One key pair, with root rights | | Open | nothing | Every request is accepted without a signature. Local tests only | ### Who can do what | Caller | Access | |---|---| | `root` | Everything | | Bucket owner | Everything on the buckets they created | | Other user | Create buckets. On other buckets: only what a bucket policy grants. `ListBuckets` shows the buckets they own or may list (`s3:ListBucket`) | | Anonymous | Only what a bucket policy or a public ACL grants | Buckets are private by default. Only root and the owner can change a bucket's policy, CORS, encryption, versioning or ACL, or delete it. ### Canned ACLs Make a bucket or an object public: ```bash aws s3api put-bucket-acl --bucket my-bucket --acl public-read aws s3api put-object-acl --bucket my-bucket --key logo.png --acl public-read ``` | ACL | Anonymous access | |---|---| | `private` | None | | `public-read` | Read | | `public-read-write` | Read and write | ### Bucket policies {#bucket-policies} A bucket policy is a JSON document of `Allow` and `Deny` statements. A user is named by `arn:loch:iam:::user/`, a resource by `arn:loch:s3:::/`. Give the user `reader` read-only access to `media`: ```bash aws s3api put-bucket-policy --bucket media --policy '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": "arn:loch:iam:::user/reader"}, "Action": ["s3:GetObject", "s3:ListBucket"], "Resource": ["arn:loch:s3:::media", "arn:loch:s3:::media/*"] }] }' ``` Make every object public: use `"Principal": "*"` and `"Action": "s3:GetObject"` on `arn:loch:s3:::media/*`. Read or remove it with `get-bucket-policy` and `delete-bucket-policy`. Syntax, actions and evaluation: [Bucket policy reference](../reference/bucket-policies.md). ### Good to know - An explicit `Deny` wins over any `Allow` and over ACLs. - `Condition`, `NotAction`, `NotResource` and `NotPrincipal` are rejected with `MalformedPolicy`. - A bucket-level `READ` grant (`public-read`) also allows anonymous downloads of every object in the bucket, not only the listing. - Policies match keys case-sensitively: keep the data directory on a case-sensitive filesystem. --- ## Buckets and objects The examples use the AWS CLI configured as in [Connect a client](./connect-a-client.md), with `--endpoint-url` omitted. ### Buckets ```bash aws s3 mb s3://my-bucket # create aws s3 ls # list your buckets aws s3 rb s3://my-bucket # delete (must be empty) ``` | Rule | Value | |---|---| | Name | 3 to 63 characters: lowercase letters, digits, `-` and `.`; starts and ends with a letter or digit | | Owner | The user who creates the bucket. The owner has full access to it | | Delete | Refused with `BucketNotEmpty` while objects, old versions or multipart uploads remain | ### Objects ```bash aws s3 cp photo.jpg s3://my-bucket/2026/photo.jpg # upload aws s3 cp s3://my-bucket/2026/photo.jpg ./photo.jpg # download aws s3 ls s3://my-bucket/2026/ # list a prefix aws s3 rm s3://my-bucket/2026/photo.jpg # delete ``` Stored headers: `Content-Type`, `Cache-Control`, `Content-Disposition`, `Content-Encoding`, `Content-Language`, `Expires` and custom `x-amz-meta-*` metadata (2 KiB in total). ```bash aws s3api put-object --bucket my-bucket --key notes.txt --body notes.txt \ --content-type "text/plain; charset=utf-8" --metadata author=alice ``` Keys are limited to 1,024 bytes and 64 `/`-separated levels. A key with a `.`, `..` or empty segment (`a/./b`, `a//b`) is rejected. `GET` supports `Range: bytes=a-b`, `bytes=a-` and `bytes=-n`. ### Presigned URLs A presigned URL gives temporary access to one object without credentials. Validity: 1 second to 7 days. ```bash aws s3 presign s3://my-bucket/report.pdf --expires-in 3600 ``` `aws s3 presign` only creates GET URLs. Create PUT URLs with an SDK (`generate_presigned_url("put_object", ...)` in boto3). ### Multipart upload The AWS CLI and SDKs switch to multipart upload for large files on their own. Parts are numbered 1 to 10,000. The server sets no minimum or maximum part size. Unfinished uploads are deleted after `S3_UPLOAD_TTL` (24 hours by default). To abort one now: ```bash aws s3api list-multipart-uploads --bucket my-bucket aws s3api abort-multipart-upload --bucket my-bucket --key big.iso --upload-id ``` ### Versioning Versioning keeps every version of an object. Only the bucket owner or root can change it. ```bash aws s3api put-bucket-versioning --bucket my-bucket --versioning-configuration Status=Enabled aws s3api list-object-versions --bucket my-bucket --prefix report.pdf aws s3api get-object --bucket my-bucket --key report.pdf --version-id old.pdf ``` | Action | Result | |---|---| | Upload | Creates a new version; the response carries `x-amz-version-id` | | Delete without `--version-id` | Adds a delete marker; older versions stay | | Delete with `--version-id` | Removes that version for good | | `Status=Suspended` | New uploads overwrite the `null` version; existing versions stay | ### Good to know - Multi-object delete (`aws s3api delete-objects`) is not supported. Tools that delete in batches fail; delete objects one by one. - In a versioned bucket, an object completed by a multipart upload does not create a version. - Each write in a versioned bucket counts twice against the [quota](../admin/production.md#quota). --- ## Connect a client Every S3 client needs three settings: | Setting | Value | |---|---| | Endpoint | URL of the server, for example `https://s3.example.com` | | Addressing style | Path-style (virtual-hosted-style is not supported) | | Region | Any value; use `us-east-1` | ### AWS CLI ```bash aws configure set aws_access_key_id aws configure set aws_secret_access_key aws configure set region us-east-1 aws configure set default.s3.addressing_style path ``` Pass the endpoint to every command, or set it once with `aws configure set endpoint_url https://s3.example.com`: ```bash aws --endpoint-url https://s3.example.com s3 ls ``` HTTP/2 is negotiated automatically. Enable it in the AWS CLI with `export AWS_USE_HTTP2=true`. ### SDKs Set the endpoint, the region and path-style addressing. Example with Python (boto3): ```python import boto3 from botocore.config import Config s3 = boto3.client( "s3", endpoint_url="https://s3.example.com", region_name="us-east-1", aws_access_key_id="", aws_secret_access_key="", config=Config(s3={"addressing_style": "path"}), ) ``` Other SDKs have the same options: `forcePathStyle: true` in JavaScript v3, `usePathStyle` / `forcePathStyle` in Java, `UsePathStyle` in Go v2. ### Cyberduck 1. Download [`LochS3.cyberduckprofile`](https://github.com/loch-sh/s3/blob/1.4.1/LochS3.cyberduckprofile) from the repository. 2. Double-click it to install it. 3. Open a new connection, choose the **Loch S3** protocol, then enter the server, port, access key and secret key. The profile sets SigV4, path-style URLs and plain HTTP on port `8080`. For a server behind HTTPS, set `Scheme` to `https` and `Default Port` to `443` in the file before you install it. ### Good to know - The server clock and the client clock must agree within 15 minutes, or requests fail with `RequestTimeTooSkewed`. - The `host` header and every `x-amz-*` header must be signed. Standard SDKs do this. - Recent SDKs send `aws-chunked` bodies with trailing checksums: they are accepted, the checksums are not verified. --- ## CORS A CORS configuration lets a web page on another origin read or upload objects of a bucket from the browser. Only the bucket owner or root can set it. ```bash aws s3api put-bucket-cors --bucket my-bucket --cors-configuration '{ "CORSRules": [{ "AllowedOrigins": ["https://app.example.com"], "AllowedMethods": ["GET", "PUT"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 3600 }] }' ``` | Field | Role | |---|---| | `AllowedOrigins` | Origins allowed. `*` for any, or one `*` inside a pattern: `https://*.example.com` | | `AllowedMethods` | `GET`, `PUT`, `POST`, `DELETE`, `HEAD` | | `AllowedHeaders` | Request headers allowed in the preflight. `*` for any | | `ExposeHeaders` | Response headers the browser may read, for example `ETag` | | `MaxAgeSeconds` | How long the browser caches the preflight answer | The first rule that matches the origin and the method applies. Read or remove the configuration with `get-bucket-cors` and `delete-bucket-cors`. ### Good to know - CORS does not grant access: the request still needs a signature, a presigned URL or a public policy or ACL. - Preflight `OPTIONS` requests are answered without authentication. - For browser uploads, give the page a presigned PUT URL created by your backend. --- ## Encryption Loch S3 encrypts objects with AES-256-GCM. Two modes: | Mode | Key | Server setup | |---|---|---| | SSE-S3 | A master key on the server; one key derived per object | `S3_ENCRYPTION_KEY_FILE` or `S3_ENCRYPTION_KEY` | | SSE-C | Sent by the client with every request, never stored | None | ### Set the master key The key is 32 random bytes, base64-encoded: ```bash openssl rand -base64 32 > master.key sudo chown 100:101 master.key # the container user chmod 0400 master.key ``` Mount the file and set `S3_ENCRYPTION_KEY_FILE` to its path. Prefer the file to `S3_ENCRYPTION_KEY`: environment variables show up in `docker inspect`. :::warning Without the master key, SSE-S3 objects cannot be read. Back it up apart from the data, see [Backup and restore](../admin/backup.md). ::: ### Choose what gets encrypted | Scope | How | |---|---| | One request | Header `x-amz-server-side-encryption: AES256` (`--sse AES256` in `aws s3 cp`) | | One bucket | Bucket default encryption (below) | | Every write | `S3_ENCRYPT_BY_DEFAULT=true` | Bucket default encryption (only the owner or root can set it): ```bash aws s3api put-bucket-encryption --bucket my-bucket --server-side-encryption-configuration \ '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}' ``` Downloads are decrypted transparently. ### SSE-C ```bash KEY=$(openssl rand -base64 32) KEY_MD5=$(echo -n "$KEY" | base64 -d | openssl dgst -md5 -binary | base64) SSE_C="--sse-customer-algorithm AES256 --sse-customer-key $KEY --sse-customer-key-md5 $KEY_MD5" aws s3api put-object --bucket my-bucket --key secret.txt --body secret.txt $SSE_C aws s3api get-object --bucket my-bucket --key secret.txt secret.txt $SSE_C ``` Keep the key: the server does not store it, and the object cannot be read without it. Without the key, a GET fails with `MissingSecurityHeader`; with a wrong key, with `AccessDenied`. ### Good to know - Objects written before encryption was turned on stay in clear text. Copy an object onto itself to encrypt it. - SSE-C headers win over default encryption. SSE-S3 and SSE-C headers cannot be combined. - Multipart parts stay in clear text in `.uploads/` until the upload is completed. - Without HTTPS, SSE-C keys travel in clear text: keep a TLS proxy in front. --- ## Install (S3) ### Docker The image `ghcr.io/loch-sh/s3:` runs on `linux/amd64` and `linux/arm64`. It listens on port `8080`, stores data in `/data`, runs as uid:gid `100:101` and has a built-in healthcheck. 1. Create `compose.yaml`: ```yaml services: s3: image: ghcr.io/loch-sh/s3:1.4.1 restart: unless-stopped ports: - "127.0.0.1:8080:8080" environment: S3_USERS_FILE: /data/.users.json volumes: - s3-data:/data volumes: s3-data: ``` 2. Start it: ```bash docker compose up -d ``` 3. Read the root credentials. On first start, the server creates the users file with a `root` user and random keys: ```bash docker compose exec s3 cat /data/.users.json ``` The port is published on the loopback only. To expose it, put a TLS reverse proxy in front: see [Production deployment](./admin/production.md). ### Binary Download the archive for your platform from [GitHub Releases](https://github.com/loch-sh/s3/releases): Linux musl (`amd64`, `arm64`), Windows `amd64` and macOS (`amd64`, `arm64`). Each archive holds the server `s3` and the user CLI `loch-s3`. ```bash curl -L -o s3.tar.gz https://github.com/loch-sh/s3/releases/download/1.4.1/s3-linux-amd64.tar.gz tar xzf s3.tar.gz S3_DATA_DIR=./data S3_USERS_FILE=./users.json ./s3 ``` Without `S3_USERS_FILE` or `S3_ACCESS_KEY_ID`, the server accepts every request without authentication. Use this only for local tests. ### First bucket Use the access key and secret of the `root` user: ```bash export AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 aws --endpoint-url http://localhost:8080 s3 mb s3://my-bucket aws --endpoint-url http://localhost:8080 s3 cp report.pdf s3://my-bucket/report.pdf aws --endpoint-url http://localhost:8080 s3 ls s3://my-bucket ``` Next: [connect a client](./guides/connect-a-client.md). Every variable: [Configuration reference](./reference/configuration.md). --- ## Loch S3 Loch S3 is an S3-compatible object storage server. It is a single Rust binary that stores every object as a file in a local directory. Use it for backups, application storage or file hosting on your own servers, with the AWS CLI or any S3 SDK. All configuration is done with environment variables: there is no configuration file. ### Features | Area | Supported | |---|---| | Authentication | AWS Signature V4 in headers and presigned URLs, `aws-chunked` uploads with chunk signatures | | Buckets | Create, list, head, delete | | Objects | Put, get, head, delete, copy, metadata, `Range` requests | | Listing | ListObjectsV2 with `prefix` and `delimiter` | | Large files | Multipart upload (create, upload part, complete, abort, list) | | Versioning | `Enabled` and `Suspended`, delete markers, version listing | | Access control | Users, bucket owners, canned ACLs, JSON bucket policies | | Encryption | SSE-S3 and SSE-C (AES-256-GCM), default encryption per bucket or for the whole server | | Browsers | CORS per bucket | | Operations | Global storage quota, `/_loch/health` probe, HTTP/1.1 and HTTP/2 | ### Not supported - Virtual-hosted-style URLs: use path-style (`https://s3.example.com/bucket/key`). - Multi-object delete (`DeleteObjects`). - Lifecycle rules, replication, object lock, tagging, regions, SSE-KMS, IAM policies. - `partNumber` on GET. Full list: [S3 API compatibility](./reference/s3-api.md). ### Next step [Install Loch S3](./install.md) and create your first bucket. --- ## Admin API reference ### Users API Available when `S3_USERS_FILE` and `S3_ADMIN_API_KEY` are both set. Every request carries `Authorization: Bearer `. | Method | Path | Body | Success | |---|---|---|---| | `GET` | `/_loch/users` | none | `200` `{"users":[…]}` | | `GET` | `/_loch/users/{user_id}` | none | `200` user object | | `PUT` | `/_loch/users/{user_id}` | `{"display_name","access_key_id","secret_access_key"}` | `201` created, `200` replaced. Empty body | | `DELETE` | `/_loch/users/{user_id}` | none | `204` | User object: ```json {"user_id": "backup", "display_name": "Backup job", "access_key_id": "", "is_root": false} ``` Example: ```bash curl -X PUT -H "Authorization: Bearer $LOCH_ADMIN_KEY" -H "Content-Type: application/json" \ -d '{"display_name":"Backup job","access_key_id":"","secret_access_key":""}' \ http://localhost:8080/_loch/users/backup ``` | Rule | Value | |---|---| | `user_id` | 1 to 128 characters: letters, digits, `-`, `_` | | `access_key_id` | Unique among users | | `secret_access_key` | Write-only: never returned | | `is_root` | Cannot be set by the API. `root` cannot be changed or deleted | | Body size | 64 KiB | Changes are saved to the users file at once (write to a temporary file, then rename) and apply without a restart. #### Errors Errors use the S3 XML format (`……`). | Status | Code | When | |---|---|---| | `400` | `InvalidArgument` | Invalid `user_id` or JSON, duplicate access key, unknown user, change to `root` | | `403` | `AccessDenied` | Missing or wrong key, `S3_ADMIN_API_KEY` unset, or no authentication configured | | `405` | `MethodNotAllowed` | Single-user mode (`S3_ACCESS_KEY_ID`) | ### Service endpoints | Method | Path | Auth | Answer | |---|---|---|---| | `GET`, `HEAD` | `/_loch/health` | none | `200 {"status":"ok"}`; with a quota, `"quota":"ok"` or `"exceeded"` is added. `503 {"status":"error"}` if the data directory is unreachable. Not logged | | `GET` | `/_loch/usage` | SigV4, root only (open without authentication) | `200 {"used_bytes":N,"reserved_bytes":N,"limit_bytes":N}`; `limit_bytes` is `null` without quota | --- ## Bucket policy reference A bucket policy is a JSON document set with `PutBucketPolicy` (`aws s3api put-bucket-policy`) by the bucket owner or root. Maximum size: 20 KiB. ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "ReadOnlyForReader", "Effect": "Allow", "Principal": {"AWS": ["arn:loch:iam:::user/reader"]}, "Action": ["s3:GetObject", "s3:ListBucket"], "Resource": ["arn:loch:s3:::media", "arn:loch:s3:::media/*"] } ] } ``` ### Fields | Field | Values | |---|---| | `Version` | `2012-10-17` or `2008-10-17` | | `Statement` | At least one statement | | `Sid` | Optional label | | `Effect` | `Allow` or `Deny` | | `Principal` | `"*"` (everyone, anonymous included), or `{"AWS": ""}` / `{"AWS": ["", …]}` with `arn:loch:iam:::user/` | | `Action` | One action or a list (below) | | `Resource` | One ARN or a list (below) | Any other field (`Condition`, `NotAction`, `NotResource`, `NotPrincipal`…) makes the policy invalid: `400 MalformedPolicy`. ### Actions | Action | Request | |---|---| | `s3:GetObject` | GET or HEAD of an object | | `s3:PutObject` | PUT or POST on an object: upload, copy destination, multipart | | `s3:DeleteObject` | DELETE of an object | | `s3:ListBucket` | List objects, multipart uploads | | `s3:GetBucketLocation` | HEAD of the bucket | | `s3:GetObjectVersion` | GET or HEAD with `versionId`; `s3:GetObject` also matches | | `s3:DeleteObjectVersion` | DELETE with `versionId`; `s3:DeleteObject` also matches | | `s3:ListBucketVersions` | `GET /{bucket}?versions`; `s3:ListBucket` also matches | Actions match like on AWS: case-insensitive, with `*` and `?` wildcards (`*`, `s3:*`, `s3:Get*`). An action that is not `*` or `s3:` makes the policy invalid. ### Resources | Resource | ARN | |---|---| | The bucket (listing) | `arn:loch:s3:::media` | | Every object | `arn:loch:s3:::media/*` | | Objects under a prefix | `arn:loch:s3:::media/public/*` | | Everything | `*` | `arn:aws:s3:::` is accepted in place of `arn:loch:s3:::`. Only a trailing `*` is a wildcard. Keys are compared case-sensitively. ### Evaluation 1. Root and the bucket owner are always allowed. Policies do not apply to them. 2. Only root and the owner can change the bucket's policy, CORS, encryption, versioning and ACL, or delete it. 3. For anyone else, a matching `Deny` refuses the request. 4. Otherwise, a matching `Allow` grants it. 5. Otherwise, anonymous requests may still be granted by a public ACL. Authenticated users are refused. A copy also needs `s3:GetObject` (or `s3:GetObjectVersion`) on the source. A stored policy that can no longer be read refuses every request covered by step 3. --- ## Configuration reference (Reference) Loch S3 reads its configuration from environment variables only. Most invalid values stop the server at startup with `Invalid configuration: …` and exit status 1. ### Server {#server} | Variable | Default | Description | |---|---|---| | `S3_PORT` | `8080` | Listen port, on all interfaces | | `S3_DATA_DIR` | `./data` (`/data` in the image) | Data directory. Created if missing | | `S3_USERS_FILE` | none | Users JSON file. Enables multi-user mode; created with a `root` user if missing | | `S3_ACCESS_KEY_ID` | none | Single-user mode access key. Set with `S3_SECRET_ACCESS_KEY`. Ignored when `S3_USERS_FILE` is set | | `S3_SECRET_ACCESS_KEY` | none | Single-user mode secret key | | `S3_ADMIN_API_KEY` | none | Bearer key of the `/_loch/users` admin API. Only with `S3_USERS_FILE`. Unset: the API is off and the users file is never written | | `S3_ENCRYPTION_KEY` | none | SSE-S3 master key: 32 bytes, base64-encoded (`openssl rand -base64 32`) | | `S3_ENCRYPTION_KEY_FILE` | none | File holding the base64 master key. Cannot be combined with `S3_ENCRYPTION_KEY` | | `S3_ENCRYPT_BY_DEFAULT` | `false` | `true`: encrypt with SSE-S3 every write that has no SSE header. Needs a master key. Accepts `true`/`false`, `1`/`0`, `yes`/`no` | | `S3_MAX_STORAGE_BYTES` | unlimited | Quota of the whole data directory, in bytes | | `S3_INIT_BUCKETS` | none | Comma-separated buckets created at startup if missing, owned by `root` | | `S3_UPLOAD_TTL` | `86400` | Seconds before an unfinished multipart upload is deleted. Checked every 5 minutes | | `S3_MIN_UPLOAD_RATE` | `4096` | Minimum upload speed in bytes/s over 60-second windows. `0` disables | | `S3_MAX_CONNECTIONS` | `1024` | Maximum simultaneous connections. Extra ones are closed. An invalid value falls back to the default | | `S3_SHUTDOWN_GRACE` | `8` | Seconds given to running requests after `SIGTERM` or `SIGINT` | | `S3_MAX_BLOCKING_THREADS` | `32` | Size cap of the file I/O thread pool. Keep it under the container PID limit | The server refuses to start when: - only one of `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` is set, or one is empty; - the users file is invalid, or cannot be created; - the master key is empty, unreadable, not base64, or not 32 bytes once decoded, or both key variables are set; - `S3_ENCRYPT_BY_DEFAULT=true` without a master key; - a name in `S3_INIT_BUCKETS` is not a valid bucket name; - a numeric variable is not a number. ### loch-s3 CLI {#cli} | Variable | Flag | Default | Description | |---|---|---|---| | `LOCH_SERVER` | `--server` | `http://localhost:8080` | Server URL | | `LOCH_ADMIN_KEY` | `--api-key` | required | Value of `S3_ADMIN_API_KEY` | | `LOCH_ACCESS_KEY` | `--access-key` | required by `users put` | Access key to set | | `LOCH_SECRET_KEY` | `--secret-key` | required by `users put` | Secret key to set | Flags win over variables. --- ## Error codes Errors are returned in the S3 XML format: `…………`. The message is a fixed text per code. | Code | Status | Usual cause | |---|---|---| | `AccessDenied` | 403 | No permission, unsigned `host` or `x-amz-*` header, wrong SSE-C key, presigned expiry out of range | | `SignatureDoesNotMatch` | 403 | Wrong secret key, or an `aws-chunked` chunk signature that does not verify | | `InvalidAccessKeyId` | 403 | Unknown access key | | `RequestTimeTooSkewed` | 403 | Client clock more than 15 minutes off | | `ExpiredToken` | 403 | Presigned URL expired | | `QuotaExceeded` | 403 | Write over `S3_MAX_STORAGE_BYTES`. Not retried by SDKs | | `NoSuchBucket` | 404 | Bucket does not exist | | `NoSuchKey` | 404 | Object does not exist | | `NoSuchVersion` | 404 | Unknown or malformed `versionId` | | `NoSuchUpload` | 404 | Unknown or malformed `uploadId` | | `NoSuchBucketPolicy` | 404 | No policy on the bucket | | `NoSuchCORSConfiguration` | 404 | No CORS configuration on the bucket | | `BucketAlreadyExists` | 409 | Name taken by another user | | `BucketAlreadyOwnedByYou` | 409 | You already own this bucket | | `BucketNotEmpty` | 409 | Objects, versions or multipart uploads remain | | `InvalidBucketName` | 400 | Name breaks the naming rules | | `InvalidArgument` | 400 | Invalid key (`.`, `..`, empty segment, over 1,024 bytes or 64 levels), malformed `aws-chunked` body, invalid copy source, SSE-C key MD5 mismatch, unsupported `x-amz-content-sha256` | | `InvalidPart` | 400 | Part missing or ETag mismatch on complete | | `InvalidPartOrder` | 400 | Parts not in ascending order on complete | | `MalformedPolicy` | 400 | Invalid policy JSON, field or action | | `MalformedXML` | 400 | Invalid XML body | | `XAmzContentSHA256Mismatch` | 400 | Body does not match the signed hash. Nothing is stored | | `RequestTimeout` | 400 | Upload stalled 60 seconds or slower than `S3_MIN_UPLOAD_RATE` | | `MissingSecurityHeader` | 400 | SSE-C object read without its key | | `InvalidEncryptionAlgorithmError` | 400 | Algorithm other than `AES256` | | `ServerSideEncryptionConfigurationNotFoundError` | 400 | SSE-S3 requested without a master key | | `MethodNotAllowed` | 405 | Unsupported method on this path. Often an empty body | | `EntityTooLarge` | 413 | Body or metadata over its [limit](./limits.md) | | `InvalidRange` | 416 | `Range` outside the object | | `InternalError` | 500 | Server-side failure: check the logs | --- ## Limits | Item | Limit | |---|---| | Bucket name | 3 to 63 characters: `a-z`, `0-9`, `-`, `.`; starts and ends with a letter or digit; no `..` | | Object key | 1,024 bytes, 64 `/`-separated levels; no `.`, `..` or empty segment (a single trailing `/` is allowed). A key cannot also be the "folder" of another key (`a` and `a/b`) | | Object size | No limit other than the quota and the disk: bodies are streamed | | User metadata (`x-amz-meta-*`) | 2 KiB in total | | Stored headers (`Content-Type`, `Cache-Control`, `Content-Disposition`, `Content-Encoding`, `Content-Language`, `Expires`) | 8 KiB in total | | Multipart | Part numbers 1 to 10,000; no part size limit; unfinished uploads deleted after `S3_UPLOAD_TTL` (24 h) | | Listing | 1,000 keys per page | | Request bodies | Bucket policy 20 KiB; CORS and encryption configuration 64 KiB; ACL 16 KiB; versioning configuration 4 KiB; `CompleteMultipartUpload` 10 MiB; admin API 64 KiB | | Presigned URL | 1 second to 7 days | | Clock skew | 15 minutes | | Request headers (HTTP/1) | Received within 30 seconds | | Idle new connection | Closed after 30 seconds without a byte | | Upload body | Aborted after 60 seconds without data, or under `S3_MIN_UPLOAD_RATE` (4096 bytes/s) over 60 seconds | | Connections | `S3_MAX_CONNECTIONS` (1024) | | HTTP/2 | Ping every 30 seconds, dropped after 20 seconds without answer; 128 streams per connection | | `aws-chunked` chunk header | 4,096 bytes | | Encryption chunk | 1 MiB; each chunk adds 16 bytes on disk | Over a size limit, the server answers `413 EntityTooLarge`. --- ## S3 API compatibility Loch S3 only accepts path-style URLs: `/{bucket}` and `/{bucket}/{key}`. Any region is accepted in the signature. ### Supported operations | Operation | Route | |---|---| | ListBuckets | `GET /` | | CreateBucket | `PUT /{bucket}` (a `LocationConstraint` body is ignored) | | HeadBucket | `HEAD /{bucket}` | | DeleteBucket | `DELETE /{bucket}` | | ListObjectsV2 | `GET /{bucket}` with `prefix`, `delimiter`, `max-keys` (up to 1000), `start-after`, `continuation-token` | | PutObject | `PUT /{bucket}/{key}` | | CopyObject | `PUT /{bucket}/{key}` with `x-amz-copy-source`; `x-amz-metadata-directive: COPY` or `REPLACE` | | GetObject | `GET /{bucket}/{key}`, optional `Range` and `versionId` | | HeadObject | `HEAD /{bucket}/{key}`, optional `versionId` | | DeleteObject | `DELETE /{bucket}/{key}`, optional `versionId` | | CreateMultipartUpload | `POST /{bucket}/{key}?uploads` | | UploadPart | `PUT /{bucket}/{key}?partNumber=N&uploadId=X` (N from 1 to 10,000) | | CompleteMultipartUpload | `POST /{bucket}/{key}?uploadId=X` | | AbortMultipartUpload | `DELETE /{bucket}/{key}?uploadId=X` | | ListParts | `GET /{bucket}/{key}?uploadId=X` | | ListMultipartUploads | `GET /{bucket}?uploads` | | Put/GetBucketVersioning | `PUT`, `GET /{bucket}?versioning` | | ListObjectVersions | `GET /{bucket}?versions` | | Put/Get/DeleteBucketPolicy | `PUT`, `GET`, `DELETE /{bucket}?policy` | | Put/Get/DeleteBucketCors | `PUT`, `GET`, `DELETE /{bucket}?cors` | | CORS preflight | `OPTIONS /{bucket}` or `/{bucket}/{key}` | | Put/GetBucketAcl | `PUT`, `GET /{bucket}?acl` (canned `x-amz-acl` or XML body) | | Put/GetObjectAcl | `PUT`, `GET /{bucket}/{key}?acl` | | Put/Get/DeleteBucketEncryption | `PUT`, `GET`, `DELETE /{bucket}?encryption` (`AES256` only) | Presigned URLs work for any of these operations. ### Authentication | Method | Detail | |---|---| | Header SigV4 | `Authorization: AWS4-HMAC-SHA256 …`. `host` and every `x-amz-*` header must be signed | | Presigned URL | `X-Amz-*` query parameters, `X-Amz-Expires` from 1 to 604,800 seconds | | `x-amz-content-sha256` | A SHA-256 hash (verified), `UNSIGNED-PAYLOAD`, `STREAMING-AWS4-HMAC-SHA256-PAYLOAD[-TRAILER]` (chunk signatures verified) or `STREAMING-UNSIGNED-PAYLOAD-TRAILER`. Other values are refused | ### Not supported | Feature | Note | |---|---| | Virtual-hosted-style URLs | Use path-style | | ListObjects v1 | `GET /{bucket}` always answers in the V2 format; `marker` is ignored | | DeleteObjects (multi-object delete) | `405` | | GetBucketLocation (`?location`) | Not implemented | | `partNumber` on GetObject | Ignored, the whole object is returned | | Multi-range `Range` headers | Ignored, the whole object is returned | | Tagging, lifecycle, replication, object lock, website, logging, notifications | Not implemented | | SSE-KMS, IAM, STS, regions | Not implemented | | `Content-MD5` and `x-amz-checksum-*` | Not verified | :::warning Requests for an unsupported sub-resource are not always refused: they can be handled as the plain operation on the same path. For example `PUT /{bucket}/{key}?tagging` replaces the object with the request body. Do not send tagging or lifecycle requests to Loch S3. ::: --- ## Architecture (3) Loch Lambda Runtime is a Go module (`github.com/loch-sh/lambda-runtime`) with no dependency outside the standard library. Source: [github.com/loch-sh/lambda-runtime](https://github.com/loch-sh/lambda-runtime). ### Layout | Path | Role | |---|---| | `cmd/loch-lambda-runtime` | `main`: makes the process non-dumpable first, then dispatches the subcommand | | `internal/app` | Wires the components into the subcommands (`run`, `health`), the connection limits and the credential reload | | `internal/config` | Parses and validates the command line, the `LOCH_RUNTIME_*` settings, the function variables and the bootstrap environment | | `internal/engine` | Runs the bootstrap and feeds it one invocation at a time: init, timeouts, outcomes, re-init, shutdown | | `internal/runtimeapi` | The AWS Lambda™ Runtime API (`2018-06-01`) served to the bootstrap | | `internal/invokeapi` | The Invoke API, health and metrics endpoints, browser filter, per-IP caps, error shapes | | `internal/auth` | Bearer and SigV4 authentication, credential files and their checks | | `internal/queue` | The bounded FIFO of waiting invocations | | `internal/process` | Starts the bootstrap as a child process, PID 1 duties | | `internal/cgroup` | Reads the container's own cgroup (memory limit, peak, OOM kills) | | `internal/logs` | Relays the bootstrap output, writes the platform lines, keeps log tails | | `internal/metrics` | Collects figures and writes the Prometheus format | | `internal/invokecli` | The `invoke` subcommand | | `internal/ids`, `internal/netutil` | Request and trace ids; networking helpers | | `internal/release` | Tests of the release tooling (the Makefile) | | `integration/` | Integration tests against the real AWS images, under Docker | ### Request flow 1. A caller sends `POST /2015-03-31/functions/{name}/invocations` to `invokeapi`. 2. `invokeapi` filters browsers, applies the caps, authenticates, reads the body and queues the invocation. 3. `engine` pops it when the bootstrap asks `runtimeapi` for the next invocation. 4. The bootstrap posts the response or the error; `engine` decides the outcome (error, timeout, OOM, exit) and answers the caller. 5. `logs` writes `START`, `END` and `REPORT`; `metrics` counts the outcome. --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Build and test (Developer) You need Go and make. The Makefile pins the toolchain (`GOTOOLCHAIN=go1.27.3`); the go command fetches it if needed. The integration tests and `make test-linux` need Docker. ### Build ```bash git clone https://github.com/loch-sh/lambda-runtime.git cd lambda-runtime make binaries # dist/loch-lambda-runtime-linux-amd64 and -arm64 ``` ### Make targets | Target | What it does | |---|---| | `make build` | Binary for the current platform in `dist/loch-lambda-runtime` | | `make binaries` | Static Linux binaries for amd64 and arm64 in `dist/` | | `make vet` | `go vet`, also for Linux and the integration tests | | `make test` | Unit tests with the race detector | | `make test-linux` | Unit tests in a Linux container (process groups, `/proc`) | | `make cover` | Coverage | | `make integration` | Integration tests against the real AWS images | | `make release VERSION=vX.Y.Z` | Binaries, SBOMs and `SHA256SUMS` | | `make reproducible` | Two builds in two directories; fails unless the checksums match | ### Integration tests `make integration` runs `go test -tags integration` against the AWS base images (Node.js, Python, `provided.al2023` with a Go bootstrap), under the production hardening: concurrent invocations, timeouts, errors, init errors, OOM, payload limits, SIGTERM, authentication with the AWS CLI and the AWS SDK for JavaScript. The CI runs them on amd64 and arm64. The first pulls from `mirror.gcr.io` may answer "not found" or 429: the tests retry. ### CI GitHub Actions (`.github/workflows/ci.yml`) runs gofmt, vet and unit tests, the reproducible build and the integration tests, on amd64 and arm64. Release tags also build, attest, sign and publish: see [Releases](./releases.md). --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Releases A release is two static binaries (`linux/amd64`, `linux/arm64`), one CycloneDX SBOM per binary, `SHA256SUMS` over these four files, and a cosign signature of `SHA256SUMS`. To verify one, see [Install](../install.md#download-and-verify). ### Reproducible build Two builds of the same tree and version give the same bytes. - Exact toolchain: `GOTOOLCHAIN=go1.27.3`. - `CGO_ENABLED=0`, `GOAMD64=v1`, `GOARM64=v8.0`, an empty `GOFLAGS`, `-trimpath`, `-buildvcs=false`, `-ldflags '-s -w -buildid= -X main.version='`: no path, build id, VCS stamp or date in the binary. - Module sums checked against `sum.golang.org`. Check it: ```bash make reproducible VERSION=v1.0.1 ``` ### SBOM `make sbom` runs syft (pinned by digest, without network) on each binary and writes `.cdx.json`. Its serial number and timestamp are removed, so the SBOMs are reproducible too. The module version reads `UNKNOWN` in the SBOM, because of `-buildvcs=false`: the release version is in the file name. ### Signing and provenance On a `vX.Y.Z` tag, GitHub Actions builds the release, then: - signs `SHA256SUMS` with cosign, keyless: a short-lived certificate from Fulcio for the workflow identity, logged in Rekor (`SHA256SUMS.cosign.bundle`); - attests the build provenance of every file (`actions/attest-build-provenance`), binding it to the workflow, the commit and the tag; - publishes the GitHub release after checking the checksums and the signature. To accept every release of a major version, verify with an anchored regexp instead of the exact identity: ```bash cosign verify-blob SHA256SUMS --bundle SHA256SUMS.cosign.bundle \ --certificate-identity-regexp '^https://github\.com/loch-sh/lambda-runtime/\.github/workflows/ci\.yml@refs/tags/v1\.[0-9]+\.[0-9]+$' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` ### Good to know - `make release` refuses a version that is not `vX.Y.Z[-suffix]`, uncommitted or untracked files, and a HEAD not tagged with the version. - A tag whose commit is not on `main` is refused by the CI. --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Authentication Network callers must authenticate. The runtime refuses to start without a credential, unless `LOCH_RUNTIME_AUTH=off` (local development only). Callers on the container's loopback (the function itself, `docker exec … invoke`, `health`) need none. ### Choose a credential | Credential | Variables | Callers send | Readable by the function | |---|---|---|---| | Bearer token, as its SHA-256 (recommended) | `LOCH_RUNTIME_TOKEN_SHA256` (64 hex) or `LOCH_RUNTIME_TOKEN_SHA256_FILE` | `Authorization: Bearer ` | No: the token is never in the container | | Plain bearer token | `LOCH_RUNTIME_TOKEN` or `LOCH_RUNTIME_TOKEN_FILE` (32 bytes at least, printable ASCII) | `Authorization: Bearer ` | Yes | | AWS SigV4 key pair (optional) | `LOCH_RUNTIME_ACCESS_KEY` or `LOCH_RUNTIME_ACCESS_KEY_FILE` (`:`) | A SigV4 signature (AWS CLI, AWS SDKs) | Yes | Set one bearer source at most, and one source for the key pair. You can set a bearer token and a key pair together. Create a token and its hash: ```bash TOKEN=$(openssl rand -hex 32) printf %s "$TOKEN" | openssl dgst -sha256 -r | cut -d' ' -f1 # value of LOCH_RUNTIME_TOKEN_SHA256 ``` ### Why the function can read some secrets The function's code runs under the runtime's uid, in the same container. It can read the environment of processes started with `docker exec`, so a plain token is for standalone use only. SigV4 needs the secret itself to check signatures, so **the function can read its own SigV4 secret**. That only lets it invoke itself, which it can already do through the loopback. Never present that secret as protected from the function's code. Generate the access key id at random and keep it private too. ### Use credential files Mount the files read-only (Docker secrets, or a `:ro` bind mount): ```bash docker run … \ -v "$PWD/secrets:/run/fn-secrets:ro" \ -e LOCH_RUNTIME_TOKEN_SHA256_FILE=/run/fn-secrets/token.sha256 \ … ``` The function can send SIGHUP to the runtime, so the runtime refuses a file it could change. The file and every directory above it must be on a read-only mount, or belong to another user without being writable by the runtime. A world-writable file is refused. As root, the whole path, rootfs included, must be read-only: run as a non-root user. ### Rotate The `*_FILE` variants are re-read on SIGHUP. 1. Write the complete new file next to the old one, then rename it over the old one. 2. Send SIGHUP: `docker kill --signal HUP fn`. A reload that fails keeps the credentials in force: writing an invalid credential revokes nothing. To revoke, restart with new credentials. Variables are read once at startup, then removed from the runtime's environment. ### Good to know - Never run with `--network host`: the loopback exemption assumes the container has its own network namespace. - SigV4 requests can be replayed within the ±5 min clock skew, and the Invoke API is plain HTTP: expose it on private networks only. - Requests from web browsers (`Origin`, `Sec-Fetch-Site`, `Sec-Fetch-Dest` or `Sec-Fetch-User` header) get 403, even with authentication off. `LOCH_RUNTIME_ALLOW_BROWSER=true` accepts them. - `/_loch/health` needs no credential; `/_loch/metrics` does, except from the loopback. - Threat model: [Security model](../reference/security-model.md). --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Custom runtimes and images The runtime starts whatever command runs the function. By default it is the AWS base images' entry script: `index.handler` means `/lambda-entrypoint.sh index.handler`. Pass any other command after `--`. ### A Go bootstrap on provided.al2023 On `provided.al2023`, the function is an executable named `bootstrap` in `/var/task`. With `aws-lambda-go`: 1. Build a static binary for the image's architecture, and make it readable and executable by everyone: ```bash CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags lambda.norpc -o fn/bootstrap . chmod 0755 fn/bootstrap ``` 2. Run it. The handler argument is required by `/lambda-entrypoint.sh` but ignored by Go: use `bootstrap`. ```bash docker run -d --name fn --read-only --user 10001:10001 --cap-drop ALL \ -v "$PWD/loch-lambda-runtime-linux-arm64:/loch/lambda-runtime:ro" \ -v "$PWD/fn:/var/task:ro" \ --env-file fn.env \ --entrypoint /loch/lambda-runtime \ public.ecr.aws/lambda/provided:al2023 bootstrap ``` `-- ./bootstrap` works too: the command is resolved from the working directory, `/var/task`. | Problem | Cause | |---|---| | `Runtime.InvalidEntrypoint`, `no bootstrap found at /var/task/bootstrap or /opt/bootstrap` | No `bootstrap` file | | `Runtime.InvalidEntrypoint`, `failed to exec /var/task/bootstrap` | Not executable or not readable by the container user, or wrong architecture (reason on stderr) | | `Runtime.ExitError` | The bootstrap exited during init | In each case `health` fails, every invocation answers with that error, and the init is retried. ### Any image: pass the command after `--` For an image that does not use `/lambda-entrypoint.sh`, put the image's ENTRYPOINT and CMD after `--`: ```bash docker image inspect --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}' my-function:latest docker run … --entrypoint /loch/lambda-runtime my-function:latest -- ``` Only the AWS base images are tested in CI. ### Build an image You can copy the binary into an image instead of mounting it. This variant is not tested in CI. `COPY --chmod` needs BuildKit (the default since Docker 23). ```dockerfile FROM public.ecr.aws/lambda/nodejs:22 COPY --chmod=0755 loch-lambda-runtime-linux-amd64 /loch/lambda-runtime COPY index.js /var/task/ ENTRYPOINT ["/loch/lambda-runtime"] CMD ["index.handler"] ``` Run it with the same options as in [Run in production](./production.md), without the `-v` of the binary and `--entrypoint`. ### Good to know - Docker Desktop on macOS does not enforce file owners on bind mounts: a `0700` bootstrap runs there and fails on Linux. Use `0755`. - A binary for the wrong architecture fails with `Exec format error`, unless Docker emulates that architecture. - `/var/task` is read-only: write to `/tmp` only. - Use `/loch/lambda-runtime`, outside `/opt`: `/opt` is the layers' directory (the `provided.al2023` bootstrap looks for `/opt/bootstrap` there). --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Invoke a function The runtime implements the AWS Lambda™ Invoke API on port 8080: `POST /2015-03-31/functions/{name}/invocations`. `{name}` is `function`, or the value of `AWS_LAMBDA_FUNCTION_NAME`. The examples use the container `fn` and the credentials of [Run in production](./production.md). ### curl, with a bearer token ```bash curl -s -H "Authorization: Bearer $TOKEN" \ localhost:8080/2015-03-31/functions/function/invocations -d '{"name":"curl"}' ``` A function error still answers 200, with the header `X-Amz-Function-Error: Unhandled` and the error as body. Add `-i` to see it. ### AWS CLI, with SigV4 From a container on the function's network: ```bash docker run --rm --network fn-net \ -e AWS_ACCESS_KEY_ID="$KEY_ID" -e AWS_SECRET_ACCESS_KEY="$KEY_SECRET" -e AWS_DEFAULT_REGION=us-east-1 \ amazon/aws-cli:2.39.5 lambda invoke --endpoint-url http://fn:8080 --function-name function \ --payload '{"name":"cli"}' --cli-binary-format raw-in-base64-out /dev/stdout ``` Any region works. Add `--log-type Tail` to get the last 4 KB of the invocation logs. ### AWS SDK Point the Lambda client at the runtime with the endpoint option and the SigV4 pair. AWS SDK v3 for JavaScript: ```js import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda'; const client = new LambdaClient({ endpoint: 'http://fn:8080', region: 'us-east-1' }); const out = await client.send(new InvokeCommand({ FunctionName: 'function', Payload: Buffer.from(JSON.stringify({ name: 'sdk' })), })); console.log(out.FunctionError ?? 'ok', Buffer.from(out.Payload).toString()); ``` The SDK reads `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` from the environment. It retries 429 answers by itself. ### From inside the container `invoke` reads the event on stdin and invokes through the loopback, without credential: ```bash echo '{"name":"exec"}' | docker exec -i fn /loch/lambda-runtime invoke --log-tail ``` It prints one JSON envelope: `statusCode`, `functionError`, `requestId`, `durationMs`, `payload` (base64) and `logTail` (base64). Exit codes: 0 delivered (whatever the function returned), 1 transport error or caller gone, 2 unusable event. Options: [Command line reference](../reference/command-line.md#invoke). ### Good to know - One invocation runs at a time. The others wait in the queue; a full queue answers 429 with `Retry-After`. - Asynchronous invocations (`--invocation-type Event`) get 400. `DryRun` gets 204. - The request id is always generated by the runtime: read it in `x-amzn-RequestId`. - Every status code and error: [Invoke API reference](../reference/invoke-api.md). --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Logs and metrics ### Logs The function's stdout and stderr go to the container output, unbuffered. Around each invocation, the runtime writes the platform lines AWS Lambda™ writes: ```text START RequestId: 3f2a… Version: $LATEST END RequestId: 3f2a… REPORT RequestId: 3f2a… Duration: 12.34 ms Billed Duration: 13 ms Memory Size: 256 MB Max Memory Used: 61 MB ``` | Field | Content | |---|---| | `Init Duration` | On the first `REPORT` after an init | | `Status: timeout` | The invocation timed out; a `Task timed out after N seconds` line comes before `END` | | `Status: error`, `Error Type` | The invocation failed | | `Max Memory Used` | The container's peak since it started, not the invocation's | With `AWS_LAMBDA_LOG_FORMAT=JSON`, these lines become JSON records (`platform.start`, `platform.runtimeDone`, `platform.report`). The runtimes of the AWS images also format the application logs from this variable. The runtime's own logs are JSON on stderr. Set their level with `LOCH_RUNTIME_LOG_LEVEL` (`debug`, `info`, `warn`, `error`). They never contain events or secrets. ```bash docker logs -f fn ``` ### Health `GET /_loch/health` needs no credential. It answers 200 `{"status":"ready","queueDepth":0}` when the function is ready, 503 with `initializing`, `error` or `stopping` otherwise. In the container, `/loch/lambda-runtime health` checks it and exits 0 or 1. ### Metrics `GET /_loch/metrics` returns Prometheus text. It needs the credential, except from the loopback. ```bash curl -s -H "Authorization: Bearer $TOKEN" localhost:8080/_loch/metrics ``` Main series: `loch_lambda_invocations_total{outcome}`, `loch_lambda_invocation_duration_seconds`, `loch_lambda_queue_depth`, `loch_lambda_state{state}`. Full list: [Metrics reference](../reference/metrics.md). ### Good to know - The function's code can print fake `START`, `END` or `REPORT` lines. Never derive billing or health from the logs: use the metrics, the `invoke` envelope or `/_loch/health`. - The runtime does not rate-limit logs: bound their volume with the Docker log driver options (`max-size`, `max-file`). - `X-Amz-Log-Type: Tail` (`--log-type Tail` in the AWS CLI) returns the last 4 KB of the invocation logs in `X-Amz-Log-Result`. --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Run in production A production container has a credential, runs read-only as a non-root user without capabilities, has memory, CPU and process limits, and a healthcheck. The binary and the function code are mounted read-only. ### 1. Create the credentials Only the SHA-256 of the bearer token enters the container. Give the token itself to the callers. ```bash TOKEN=$(openssl rand -hex 32) # for the callers, never for the container TOKEN_SHA256=$(printf %s "$TOKEN" | openssl dgst -sha256 -r | cut -d' ' -f1) KEY_ID=$(LC_ALL=C tr -dc A-Z0-9 fn.env chmod 600 fn.env ``` Keep both values secret. The SigV4 pair is only needed for the AWS CLI and SDKs. Details: [Authentication](./authentication.md). ### 2. Start the container `fn/` holds the function code and `$ARCH` matches the host, as in the [quick start](../install.md#quick-start). ```bash docker network create fn-net docker run -d --name fn --network fn-net \ --read-only --user 10001:10001 --cap-drop ALL --security-opt no-new-privileges:true \ --tmpfs /tmp:size=64m --memory 256m --memory-swap 256m --cpus 0.5 --pids-limit 128 \ -p 127.0.0.1:8080:8080 \ -v "$PWD/loch-lambda-runtime-linux-$ARCH:/loch/lambda-runtime:ro" \ -v "$PWD/fn:/var/task:ro" \ --env-file fn.env -e AWS_LAMBDA_FUNCTION_TIMEOUT=30 \ --entrypoint /loch/lambda-runtime \ public.ecr.aws/lambda/nodejs:22 index.handler until docker exec fn /loch/lambda-runtime health >/dev/null; do sleep 0.2; done ``` | Option | Why | |---|---| | `--read-only`, `--tmpfs /tmp` | The function writes to `/tmp` only, as on AWS Lambda™ | | `--user 10001:10001` | Any non-root uid. As root, credential files are harder to protect | | `--cap-drop ALL`, `no-new-privileges` | The runtime needs no capability | | `--memory` = `--memory-swap` | No swap. The runtime reads the limit and sets `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` (256) | | `--cpus`, `--pids-limit` | Bound what the function can use | | `-p 127.0.0.1:8080:8080` | Publish on the loopback only, or not at all: callers on `fn-net` use `http://fn:8080` | Invoke it: see [Invoke a function](./invoke.md). ### 3. Add a healthcheck `docker run --health-cmd` only has the shell form, which you must not use. Use compose and the exec form: ```yaml services: fn: image: public.ecr.aws/lambda/nodejs:22 entrypoint: ["/loch/lambda-runtime"] command: ["index.handler"] user: "10001:10001" read_only: true cap_drop: [ALL] security_opt: ["no-new-privileges:true"] tmpfs: ["/tmp:size=64m"] mem_limit: 256m memswap_limit: 256m cpus: 0.5 pids_limit: 128 env_file: fn.env environment: AWS_LAMBDA_FUNCTION_TIMEOUT: "30" volumes: - ./loch-lambda-runtime-linux-amd64:/loch/lambda-runtime:ro # or -linux-arm64 - ./fn:/var/task:ro healthcheck: test: ["CMD", "/loch/lambda-runtime", "health"] interval: 10s timeout: 5s ``` This compose file applies the options above; it is not tested in CI. `health` exits 0 when the function is ready, 1 otherwise (initializing, init error, stopping). ### Limits | Setting | Default | Effect | |---|---|---| | `AWS_LAMBDA_FUNCTION_TIMEOUT` | `300` (s) | Longer invocations are stopped and get `Sandbox.Timedout` | | `LOCH_RUNTIME_QUEUE_SIZE` | `16` | Waiting invocations; beyond, 429 | | `LOCH_RUNTIME_QUEUE_BYTES` | `min(32MiB, memory/8)` | Total size of the waiting invocations; beyond, 429 | | `LOCH_RUNTIME_QUEUE_TIMEOUT` | `30s` | Longest wait in the queue; beyond, 429 | | `LOCH_RUNTIME_MAX_PAYLOAD` | `6MiB` | Larger requests get 413 | | `LOCH_RUNTIME_SHUTDOWN_GRACE` | `8s` | Time left to the running invocation on `docker stop` | Full list: [Configuration reference](../reference/configuration.md). ### Good to know - Never use `--network host`: the loopback needs no credential, so every process of the host could invoke the function. - Keep `LOCH_RUNTIME_SHUTDOWN_GRACE` shorter than the container stop timeout (`stop_grace_period`, 10 s by default in Docker). On SIGTERM, queued invocations get 503. - A tmpfs counts against the container memory limit. - The Invoke API is plain HTTP: keep it on private networks. - Never bill or alert on the function's logs: its code can forge `START`/`END`/`REPORT` lines. Use [metrics](./logs-and-metrics.md). --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Troubleshooting | Symptom | Cause and fix | |---|---| | Startup fails: `authentication is on: set LOCH_RUNTIME_TOKEN_SHA256 …` | No credential. Set one ([Authentication](./authentication.md)). `LOCH_RUNTIME_AUTH=off` is for local development only | | Startup fails with a message naming a setting (exit code 2) | Invalid value. Every invalid setting is listed at once | | A handler named `health`, `invoke` or `version` runs the subcommand | Add `run`: `loch-lambda-runtime run health` | | 403 `AccessDeniedException` behind a reverse proxy | The proxy forwards the browser's `Origin`. Strip the header at the proxy, or set `LOCH_RUNTIME_ALLOW_BROWSER=true` | | A credential file is refused | The runtime's user could change it. Mount the whole path read-only (`:ro`, Docker secrets), or give it to another user. As root on a writable rootfs, it is always refused: run as non-root | | Healthcheck written with `CMD-SHELL` or `docker run --health-cmd` | Use the exec form `["CMD", "/loch/lambda-runtime", "health"]`, never `CMD-SHELL`: a shell would hold the container's environment in a process the function can read | | `docker exec … invoke --watch-stdin` exits 1, or waits for the event | Keep stdin open (`docker exec -i`) until the result arrives: its closing means the caller is gone. End the event with a newline: a number, `true` or `null` only ends at the next byte | | provided.al2023: `failed to exec /var/task/bootstrap` | Set mode `0755` and build for the image's architecture. Docker Desktop on macOS does not enforce owners on bind mounts: `0700` passes there, fails on Linux | | `docker pull` from `mirror.gcr.io` answers "not found" or 429 | First pulls of a tag can fail: retry | | Invocations get 429 | The queue is full (`LOCH_RUNTIME_QUEUE_SIZE`, `LOCH_RUNTIME_QUEUE_BYTES`) or an invocation waited longer than `LOCH_RUNTIME_QUEUE_TIMEOUT`. Clients should retry after `Retry-After` | | Invocations get 400 with `Event` | Asynchronous invocations are not supported | | `REPORT` shows a growing `Max Memory Used` | It is the container's peak since it started, not the invocation's | ### Good to know - Run with `LOCH_RUNTIME_LOG_LEVEL=debug` to see more of the runtime's own logs. - Check the state: `docker exec fn /loch/lambda-runtime health` prints the health document. --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Install (Lambda-runtime) Loch Lambda Runtime ships as a static Linux binary on [GitHub Releases](https://github.com/loch-sh/lambda-runtime/releases). There is no Docker image: you mount the binary into the base image of your function (AWS Lambda™ base images, for example). ### Release files | File | Content | |---|---| | `loch-lambda-runtime-linux-amd64` | Binary for x86-64 | | `loch-lambda-runtime-linux-arm64` | Binary for ARM64 | | `loch-lambda-runtime-linux-.cdx.json` | CycloneDX SBOM of each binary | | `SHA256SUMS` | Checksums of the four files above | | `SHA256SUMS.cosign.bundle` | Keyless cosign signature of `SHA256SUMS` | ### Download and verify You need [cosign](https://docs.sigstore.dev/cosign/system_config/installation/) and, for the provenance check, the [GitHub CLI](https://cli.github.com/). 1. Download the binary for your architecture, the checksums and the signature: ```bash VERSION=v1.0.1 ARCH=amd64 # or arm64 BASE=https://github.com/loch-sh/lambda-runtime/releases/download/$VERSION curl -fLO "$BASE/loch-lambda-runtime-linux-$ARCH" curl -fLO "$BASE/SHA256SUMS" curl -fLO "$BASE/SHA256SUMS.cosign.bundle" ``` 2. Verify the signature of `SHA256SUMS`. The identity binds it to the release workflow and to the tag: ```bash cosign verify-blob SHA256SUMS --bundle SHA256SUMS.cosign.bundle \ --certificate-identity "https://github.com/loch-sh/lambda-runtime/.github/workflows/ci.yml@refs/tags/$VERSION" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` 3. Verify the checksum of the binary (`--ignore-missing`: you did not download every file): ```bash sha256sum -c --ignore-missing SHA256SUMS # macOS: shasum -a 256 -c --ignore-missing SHA256SUMS ``` 4. Optional: verify the build provenance, which binds the binary to the commit and the tag that built it: ```bash gh attestation verify "loch-lambda-runtime-linux-$ARCH" --repo loch-sh/lambda-runtime ``` 5. Make it executable. It runs as a non-root user in the container, so it must be readable and executable by others: ```bash chmod 0755 "loch-lambda-runtime-linux-$ARCH" ``` ### Quick start Run a Node.js function with the base image AWS publishes, the binary mounted as its entrypoint. Docker picks the image for your machine; `ARCH` must match it. ```bash mkdir -p fn && echo 'exports.handler = async (event) => ({ hello: event.name ?? "world" });' > fn/index.js ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') docker run --rm -p 127.0.0.1:8080:8080 \ -v "$PWD/loch-lambda-runtime-linux-$ARCH:/loch/lambda-runtime:ro" \ -v "$PWD/fn:/var/task:ro" \ -e LOCH_RUNTIME_AUTH=off \ --entrypoint /loch/lambda-runtime \ public.ecr.aws/lambda/nodejs:22 index.handler ``` `index.handler` is the handler, as on AWS: the `handler` export of `index.js`. Invoke it from another terminal: ```bash curl localhost:8080/2015-03-31/functions/function/invocations -d '{"name":"you"}' # {"hello":"you"} ``` Or with the AWS CLI. With authentication off, the credentials are not checked, but the CLI needs some to sign: ```bash AWS_ACCESS_KEY_ID=x AWS_SECRET_ACCESS_KEY=x AWS_DEFAULT_REGION=us-east-1 \ aws lambda invoke --endpoint-url http://localhost:8080 --function-name function \ --payload '{"name":"cli"}' --cli-binary-format raw-in-base64-out /dev/stdout ``` :::warning `LOCH_RUNTIME_AUTH=off` is for this local try only. By default, the runtime refuses to start without a credential. See [Run in production](./guides/production.md). ::: ### Good to know - `function` is the default function name. Set `AWS_LAMBDA_FUNCTION_NAME` to use another one in the URL. - The binary does not depend on the image's libc or tools: it runs on any Linux base image. - To add the binary to an image instead of mounting it, see [Custom runtimes and images](./guides/custom-runtimes.md#build-an-image). --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Loch Lambda Runtime Loch Lambda Runtime runs handlers written for AWS Lambda™ in plain Docker containers, unchanged, with the base images AWS publishes. It is an open-source alternative to the Runtime Interface Emulator (RIE) bundled in those base images. It contains no RIE code and knows nothing about the platform around it. It is one static binary, `loch-lambda-runtime`, mounted read-only in the container at `/loch/lambda-runtime` and used as its entrypoint. 1. [Install the binary and run a first function](./install.md). 2. [Run it in production](./guides/production.md). 3. [Invoke the function](./guides/invoke.md) with curl, the AWS CLI or an AWS SDK. ### How it works The binary is the container's PID 1. It starts the image's bootstrap (`/lambda-entrypoint.sh `) as a child process and sits between the bootstrap and the callers. | Side | What it serves | Address | |---|---|---| | Bootstrap | The AWS Lambda Runtime API, version `2018-06-01` | `127.0.0.1:9001` (loopback only) | | Callers | The AWS Lambda Invoke API: `POST /2015-03-31/functions/{name}/invocations` | `0.0.0.0:8080` | | Probes | `/_loch/health`, `/_loch/metrics` (Prometheus) | port 8080, and `127.0.0.1:8081` for `health` | `aws lambda invoke --endpoint-url` and the AWS SDKs work against port 8080. ### Features | Feature | Behavior | |---|---| | Concurrency | One invocation at a time; the others wait in a bounded FIFO queue (16 invocations, 30 s at most). Full queue: 429 | | Timeout | Enforced (`AWS_LAMBDA_FUNCTION_TIMEOUT`, 300 s by default), then the bootstrap is re-initialized | | Errors | Reported as AWS Lambda reports them: `X-Amz-Function-Error: Unhandled` and the error body | | Out of memory | Detected and reported as `Runtime.OutOfMemory` | | Payloads | 6 MiB requests; a larger request gets 413. A response is never truncated | | Logs | `START`, `END` and `REPORT` lines, in text or JSON (`AWS_LAMBDA_LOG_FORMAT=JSON`) | | Authentication | Required by default: bearer token (stored as its SHA-256) or AWS SigV4 | | Health and metrics | `/_loch/health`, `/_loch/metrics`, and a `health` subcommand for Docker healthchecks | ### Tested runtimes The CI runs these images on amd64 and arm64: | Runtime | Images | |---|---| | Node.js 22, 24, 26 | `mirror.gcr.io/amazon/aws-lambda-nodejs`; `public.ecr.aws/lambda/nodejs:22` | | Python 3.12, 3.13, 3.14 | `mirror.gcr.io/amazon/aws-lambda-python`; `public.ecr.aws/lambda/python:3.13` | | Custom runtime (`provided.al2023`) | `public.ecr.aws/lambda/provided:al2023` with a Go bootstrap (`aws-lambda-go`) | Tested clients: curl with a bearer token, AWS CLI 2.39.5 (SigV4), AWS SDK v3 for JavaScript, and the `invoke` subcommand through `docker exec`. Java, .NET and Ruby images are not tested. ### Not supported - Extensions and Telemetry APIs: layer extensions in `/opt/extensions` are not started. - Response streaming to the client: streamed responses are buffered, then returned whole. - SnapStart, X-Ray export, the `Lambda-Runtime-Invocation-Id` header. - Asynchronous invocations (`X-Amz-Invocation-Type: Event`): answered with 400. - `Max Memory Used` in `REPORT` is the container's peak since it started, not the invocation's. ### Differences from AWS Lambda - One container runs one function, always warm: there is no scale-to-zero. - No scaling: one container serves one invocation at a time. - You run the container: Docker options, network and limits are yours to set. --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Command line reference ```text loch-lambda-runtime [run] [flags] -- [args...] loch-lambda-runtime [run] [flags] (= /lambda-entrypoint.sh ) loch-lambda-runtime health [flags] loch-lambda-runtime invoke [flags] [--timeout ] [--log-tail] [--watch-stdin] < event.json loch-lambda-runtime version ``` `-h` prints the usage and every flag. The `LOCH_RUNTIME_*` flags (`--queue-size`, …) are listed in the [Configuration reference](./configuration.md); `health` and `invoke` accept them too, to find the addresses and limits. ### run {#run} The default subcommand: it runs the function and serves the APIs. It is the container's entrypoint. | Form | Runs | |---|---| | `` | `/lambda-entrypoint.sh ` (or `LOCH_RUNTIME_BOOTSTRAP`). One argument only | | `-- [args...]` | That command, for example the image's ENTRYPOINT and CMD | ```bash loch-lambda-runtime --queue-size 32 index.handler ``` Write `run` explicitly when the handler is literally named `health`, `invoke` or `version`. Exit codes: 0 after a graceful stop (SIGTERM, SIGINT), 2 for an invalid command line or configuration. ### health {#health} Queries the health listener on the loopback. Prints the health document; exits 0 when the function is ready, 1 otherwise. No credential needed. ```yaml healthcheck: test: ["CMD", "/loch/lambda-runtime", "health"] ``` Use this exec form only, never `CMD-SHELL`. ### invoke {#invoke} Reads one event on stdin (6 MiB at most, within 10 s), invokes the function through the loopback without credential, and prints one JSON envelope on stdout. | Option | Description | |---|---| | `--timeout ` | Deadline of the whole call, queue wait included. Default: function timeout + `LOCH_RUNTIME_QUEUE_TIMEOUT` + 5 s | | `--log-tail` | Fill `logTail` with the last 4 KB of the invocation logs | | `--watch-stdin` | The event is the first JSON value on stdin, ended by a newline; stdin closing afterwards means the caller is gone | ```bash echo '{"hello":"world"}' | docker exec -i fn /loch/lambda-runtime invoke --log-tail ``` | Envelope field | Content | |---|---| | `statusCode` | HTTP status of the Invoke API | | `functionError` | `Unhandled` on a function error, empty otherwise | | `requestId` | Request id of the invocation | | `durationMs` | Duration of the call | | `payload` | Response body, base64 | | `logTail` | Logs, base64; empty when not requested | Exit codes: 0 delivered (whatever the function returned), 1 transport error, deadline or caller gone, 2 unusable event. `docker exec` cannot be killed by Docker and keeps the process's stdout open when the client leaves, but closes its stdin. With `--watch-stdin`, keep stdin open until the result: `invoke` ends within a second once it closes. ### version {#version} Prints the version of the binary. ```bash docker exec fn /loch/lambda-runtime version ``` --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Configuration reference (3) Every `LOCH_RUNTIME_*` setting is read from the environment or from its flag; the flag wins. The three variables that hold a credential value have no flag, so that it never shows in the process list. Every value is checked at startup: an invalid one stops the runtime (exit code 2) with a message naming the setting, all errors listed at once. Durations accept Go syntax (`1m30s`) or seconds (`2.5`), up to 24 h. Sizes accept bytes or a `KiB`, `MiB` or `GiB` suffix. Switches accept `on`/`true` and `off`/`false`. ### Runtime settings {#runtime} | Variable | Flag | Default | Description | |---|---|---|---| | `LOCH_RUNTIME_INVOKE_ADDR` | `--invoke-addr` | `0.0.0.0:8080` | Listen address of the Invoke API (IP and port) | | `LOCH_RUNTIME_API_ADDR` | `--runtime-api-addr` | `127.0.0.1:9001` | Listen address of the Runtime API. Loopback IPv4 only | | `LOCH_RUNTIME_HEALTH_ADDR` | `--health-addr` | `127.0.0.1:8081` | Health-only listener used by `health`. Loopback IPv4 only | | `LOCH_RUNTIME_BOOTSTRAP` | `--bootstrap` | `/lambda-entrypoint.sh` | Program run with the handler in the `` form. Absolute path | | `LOCH_RUNTIME_WORKDIR` | `--workdir` | current directory (the image `WORKDIR`) | Working directory of the bootstrap. Absolute path | | `LOCH_RUNTIME_INIT_TIMEOUT` | `--init-timeout` | `10s` | Init budget. A longer init keeps running, and its time is charged to the first invocation's timeout | | `LOCH_RUNTIME_SHUTDOWN_GRACE` | `--shutdown-grace` | `8s` | Time given to the running invocation on SIGTERM. `0` accepted | | `LOCH_RUNTIME_MAX_PAYLOAD` | `--max-payload` | `6MiB` | Request limit, 1 KiB to 1 GiB. Responses: this limit + 100 bytes, as on AWS Lambda™ | | `LOCH_RUNTIME_QUEUE_SIZE` | `--queue-size` | `16` | Waiting invocations (0 to 100000) | | `LOCH_RUNTIME_QUEUE_BYTES` | `--queue-bytes` | `min(32MiB, memory/8)`, at least `LOCH_RUNTIME_MAX_PAYLOAD` | Total size of the invocations in memory. A set value below `LOCH_RUNTIME_MAX_PAYLOAD` is refused | | `LOCH_RUNTIME_QUEUE_TIMEOUT` | `--queue-timeout` | `30s` | Longest wait in the queue; beyond, 429 | | `LOCH_RUNTIME_MAX_CONNS` | `--max-conns` | 8 × (queue size + 1), at most 768 | Open connections to the Invoke API. One source IP may hold an eighth of them, at least 4 | | `LOCH_RUNTIME_AUTH` | `--auth` | `on` | `off` disables authentication (development only) | | `LOCH_RUNTIME_ALLOW_BROWSER` | `--allow-browser` | `false` | `true` accepts requests from web browsers (`Origin`, `Sec-Fetch-Site`, `Sec-Fetch-Dest`, `Sec-Fetch-User` headers) | | `LOCH_RUNTIME_LOG_LEVEL` | `--log-level` | `info` | `debug`, `info`, `warn`, `error` | ### Credentials {#credentials} Set one bearer source at most, and one source for the SigV4 pair. With authentication on, at least one credential is required. See [Authentication](../guides/authentication.md). | Variable | Flag | Description | |---|---|---| | `LOCH_RUNTIME_TOKEN_SHA256` | none | Hex SHA-256 of the bearer token, 64 characters (recommended) | | `LOCH_RUNTIME_TOKEN_SHA256_FILE` | `--token-sha256-file` | File holding it | | `LOCH_RUNTIME_TOKEN` | none | Plain bearer token, 32 printable ASCII characters at least, no space (readable by the function) | | `LOCH_RUNTIME_TOKEN_FILE` | `--token-file` | File holding the plain bearer token | | `LOCH_RUNTIME_ACCESS_KEY` | none | `:` for SigV4 (readable by the function) | | `LOCH_RUNTIME_ACCESS_KEY_FILE` | `--access-key-file` | File holding `:` | Files: absolute paths, regular files of 4 KiB at most, re-read on SIGHUP, refused if the runtime's user could change them. ### Function variables {#function} The function is configured with the AWS variables. | Variable | Default | Description | |---|---|---| | `AWS_LAMBDA_FUNCTION_NAME` | `function` | Name accepted in the Invoke URL. 1 to 64 letters, digits, `-` or `_` | | `AWS_LAMBDA_FUNCTION_VERSION` | `$LATEST` | Version reported to the function | | `AWS_LAMBDA_FUNCTION_TIMEOUT` | `300` | Timeout in seconds, up to 86400. A convention of the RIE, not an AWS variable | | `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` | the cgroup memory limit, else `3008` | Memory in MB, reported in `REPORT` and to the function | | `AWS_REGION` | `AWS_DEFAULT_REGION`, else `us-east-1` | Region of the function ARN | | `AWS_LAMBDA_LOG_FORMAT` | `Text` | `JSON` writes the platform log lines as JSON | ### Environment of the bootstrap {#child-env} The bootstrap inherits the container's environment, with these changes: | Variables | Change | |---|---| | `AWS_LAMBDA_RUNTIME_API` | Always set to the Runtime API address | | `AWS_LAMBDA_MAX_CONCURRENCY` | Always removed, with a warning | | `LOCH_RUNTIME_*` | Always removed | | `AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, `AWS_LAMBDA_LOG_GROUP_NAME`, `AWS_LAMBDA_LOG_STREAM_NAME`, `AWS_REGION`, `AWS_DEFAULT_REGION`, `AWS_LAMBDA_INITIALIZATION_TYPE`, `AWS_XRAY_CONTEXT_MISSING`, `LAMBDA_TASK_ROOT`, `LAMBDA_RUNTIME_DIR` | Set when absent | No credential is ever invented: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` come from your environment only. --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Invoke API reference The runtime implements the AWS Lambda™ Invoke API on `LOCH_RUNTIME_INVOKE_ADDR` (`0.0.0.0:8080`), plain HTTP. ### Endpoint ```text POST /2015-03-31/functions/{name}/invocations ``` | Part | Accepted values | |---|---| | `{name}` | `function` or `AWS_LAMBDA_FUNCTION_NAME`; `:$LATEST`; the ARN `arn:aws:lambda::000000000000:function:`; the partial ARN `000000000000:function:` | | `?Qualifier=` | Absent or `$LATEST` | | Method | `POST` only; any other method gets 405 and never invokes | | Body | The raw event, up to `LOCH_RUNTIME_MAX_PAYLOAD` (6 MiB). An empty body is passed as is | ### Request headers | Header | Effect | |---|---| | `Authorization` | Bearer token or SigV4 signature, unless the caller is on the loopback | | `X-Amz-Invocation-Type` | `RequestResponse` (default); `DryRun` answers 204; `Event` answers 400 (not supported) | | `X-Amz-Log-Type: Tail` | Returns the last 4 KB of the invocation logs, base64, in `X-Amz-Log-Result` | | `X-Amz-Client-Context` | Base64 JSON object, 3,583 bytes at most; passed to the function | | `X-Amzn-Trace-Id` | Becomes the trace id when valid | With SigV4, every `x-amz-*` header and `Content-Type` must be signed, as on AWS. ### Function results The delivery answers 200, with `x-amzn-RequestId`, `X-Amz-Executed-Version: $LATEST` and `Content-Type`. | Result | Body | |---|---| | Success | The function's response, never truncated | | Function error | `X-Amz-Function-Error: Unhandled` and the error the function posted, verbatim | | Init error | `X-Amz-Function-Error: Unhandled` and the init error (`Runtime.ImportModuleError`, `Runtime.HandlerNotFound`, …) | | Response over 6 MiB + 100 bytes | `Function.ResponseSizeTooLarge`, then re-init | | Timeout | `Sandbox.Timedout`, `Task timed out after N seconds`, then re-init | | Out of memory | `Runtime.OutOfMemory`, then re-init | | Bootstrap exited | `Runtime.ExitError`, `Runtime exited with error: exit status N`, then re-init | Error bodies are `{"errorType":"…","errorMessage":"…"}`. Match on `errorType`, not on the message. ### Errors of the API Errors of the API itself (not of the function) have the header `x-amzn-ErrorType: `, `x-amzn-RequestId`, and the body `{"Type":"User","message":"…"}` (`"Type":"Service"` for 5xx). A 429 body also has `"Reason":"ReservedFunctionConcurrentInvocationLimitExceeded"`. | Status | Name | When | |---|---|---| | 400 | `InvalidParameterValueException` | `X-Amz-Invocation-Type: Event`, or another invalid parameter | | 400 | `InvalidRequestContentException` | Invalid `X-Amz-Client-Context` | | 401 | `MissingAuthenticationTokenException` | No credential | | 403 | `UnrecognizedClientException` | Wrong bearer token or unknown access key id | | 403 | `InvalidSignatureException`, `IncompleteSignatureException` | Wrong, expired or malformed SigV4 signature | | 403 | `AccessDeniedException` | Request from a web browser (`Origin`, `Sec-Fetch-Site`, `Sec-Fetch-Dest`, `Sec-Fetch-User`) | | 404 | `ResourceNotFoundException` | Unknown function name, qualifier or route | | 405 | `MethodNotAllowedException` | Method other than `POST` | | 408 | `RequestTimeoutException` | Body not received in time | | 413 | `RequestTooLargeException` | Request over `LOCH_RUNTIME_MAX_PAYLOAD` | | 429 | `TooManyRequestsException` | Queue full or wait too long, too many connections or requests from one IP. `Retry-After` is set; AWS SDKs retry by themselves | | 503 | `ServiceException` | The runtime is shutting down. `Retry-After: 1` | Authentication errors close the connection. ### Other routes | Route | Credential | Answer | |---|---|---| | `GET /_loch/health` | None | 200 `{"status":"ready","queueDepth":0}`, or 503 with `initializing`, `error` or `stopping` | | `GET /_loch/metrics` | Required, except from the loopback | Prometheus text: see [Metrics reference](./metrics.md) | --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Metrics reference `GET /_loch/metrics` serves the Prometheus text format on the Invoke API port. It needs the credential, except from the loopback. With SigV4, sign the `GET` over an empty body. Labels only carry fixed values, never event contents or secrets. | Metric | Type | Labels | Description | |---|---|---|---| | `loch_lambda_runtime_info` | gauge | `version` | Version of the runtime, always 1 | | `loch_lambda_invocations_total` | counter | `outcome`: `success`, `error`, `timeout`, `exit`, `oom`, `too_large`, `init_error`, `throttled` | Invocations by outcome. `throttled` counts the 429 answers | | `loch_lambda_invocation_duration_seconds` | histogram | | Duration of the invocations run by the bootstrap, buckets from 1 ms to 15 min | | `loch_lambda_auth_failures_total` | counter | | Requests refused for their credential | | `loch_lambda_inits_total` | counter | `result`: `success`, `failure` | Inits of the bootstrap | | `loch_lambda_init_duration_seconds` | histogram | | Duration of the inits | | `loch_lambda_queue_depth` | gauge | | Invocations waiting | | `loch_lambda_queue_depth_max` | gauge | | Highest number of invocations waiting since start | | `loch_lambda_memory_peak_bytes` | gauge | | Peak memory of the container (cgroup `memory.peak`); absent when not readable | | `loch_lambda_state` | gauge | `state`: `initializing`, `ready`, `error`, `stopping` | 1 for the current state, 0 for the others | Every `outcome` and `state` value is exposed from the start, at 0. Example alert: `increase(loch_lambda_invocations_total{outcome=~"timeout|oom"}[5m]) > 0`. --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Security model The function's code is untrusted. It runs under the runtime's uid, in the same container, and the uids cannot be separated without capabilities. Hence the rule: **no usable secret in the container**. ### What the function's code can do | Action | Consequence | Protection | |---|---|---| | Read the environment of `docker exec` processes (`/proc//environ`) | A plain token or a SigV4 secret is readable | Use the bearer token as its SHA-256 (`LOCH_RUNTIME_TOKEN_SHA256`): the token never enters the container. The runtime is non-dumpable and removes credential variables from its own environment | | Read its own SigV4 secret | It can invoke itself, which it already can through the loopback | Never present the SigV4 secret as protected from the function | | Send SIGHUP to PID 1 | Credential files are re-read | Files the runtime's user could change are refused | | Send SIGTERM, SIGQUIT, … to PID 1 | It stops its own container | Self-inflicted only | | Lower its `oom_score_adj` (started at 1000) | An OOM may kill PID 1 instead of the bootstrap | Self-inflicted only | | Forge `START`, `END`, `REPORT` lines | Logs cannot be trusted | Never bill or alert on logs: use `/_loch/metrics`, the `invoke` envelope or `/_loch/health` | | Invoke itself through the loopback | No credential needed | Bound by the global connection cap | A shell-form healthcheck (`CMD-SHELL`) leaves a shell holding the container's full environment: use the exec form. ### What a network caller can do without credential | Action | Note | |---|---| | Read `/_loch/health` | State and queue depth only | | Tell a valid access key id from a wrong one | As on AWS. Keep the access key id random and private | Everything else answers 401 or 403. A bearer token is checked before the body is read; a SigV4 request is checked as far as possible before it, and its body has its own bounded budget until verified. Each source IP has its own connection and request caps, so one caller cannot take every slot. ### What a web page can do Nothing: requests from a browser (`Origin`, `Sec-Fetch-Site`, `Sec-Fetch-Dest` or `Sec-Fetch-User` header) get 403, even with authentication off, which blocks CSRF and DNS rebinding. `LOCH_RUNTIME_ALLOW_BROWSER=true` lifts this. With authentication off, a page could still read `/_loch/metrics` through DNS rebinding. ### Limits of the model - The loopback exemption assumes the container has its own network namespace: never use `--network host`. - The Invoke API is plain HTTP, and a SigV4 request can be replayed within the ±5 min clock skew: expose it on private networks only. - A credential reload that fails keeps the old credentials: to revoke, restart with new ones. ### Hardening Run as a non-root uid, with a read-only rootfs, `--cap-drop ALL`, `no-new-privileges`, a tmpfs `/tmp`, a memory limit without swap, CPU and process limits. The binary writes nothing outside `/tmp`. See [Run in production](../guides/production.md). --- "AWS" and "AWS Lambda" are trademarks of Amazon.com, Inc. or its affiliates. This project is not affiliated with or endorsed by Amazon. It implements the publicly documented AWS Lambda Runtime and Invoke APIs and contains no code from the AWS Lambda Runtime Interface Emulator. --- ## Capacity and timeouts The plugin counts **visitors**, not requests: a browser that keeps the cookie counts once, whatever the number of requests it sends. ### Options | Option | Default | Role | |---|---|---| | `maxEntries` | `100` | Number of visitors allowed in at the same time | | `inactivityTimeoutSeconds` | `60` | An active visitor who sends no request for this long loses their slot | | `hardSessionLimitSeconds` | `0` (off) | Maximum time in the service, even for a busy visitor | | `cleanupIntervalSeconds` | `30` | How often the plugin frees slots and lets in the head of the queue | | `refreshIntervalSeconds` | `20` | How often the waiting page reloads | | `cookieMaxAgeSeconds` | `3600` | Lifetime of the cookie in the browser | Example: 300 visitors, 5 minutes of inactivity, 1 hour at most. ```yaml labels: - traefik.http.middlewares.queue.plugin.queuemanager.maxEntries=300 - traefik.http.middlewares.queue.plugin.queuemanager.inactivityTimeoutSeconds=300 - traefik.http.middlewares.queue.plugin.queuemanager.hardSessionLimitSeconds=3600 ``` ### Choose the values - **`maxEntries`**: start below what the service handles under load, then raise it. - **`inactivityTimeoutSeconds`**: every request through the router keeps the slot. Too short, a visitor reading a long page goes back to the queue. Too long, idle visitors hold slots. - **`hardSessionLimitSeconds`**: counts from entry, or from the exit of the queue. When it is reached, the visitor goes back to the queue unless a slot is free. - **`cleanupIntervalSeconds`**: the queue mostly moves at each cleanup. A wait can last this long after a slot frees up. - **`refreshIntervalSeconds`**: keep it well below `inactivityTimeoutSeconds`. A visitor let in must send a request before the inactivity timeout, otherwise they lose the slot before seeing it. - **`cookieMaxAgeSeconds`**: the cookie is not renewed. When it expires, the visitor becomes a new one, at the end of the queue. Make it longer than a wait plus a session. The wait time shown to visitors is an approximation computed from the position, not a measurement. Its floor is `minWaitTimeMinutes` (`1`). ### Turn the queue off Set `enabled=false` to let every request through without removing the middleware. The rate limit is off too. Full list: [Configuration reference](../reference/configuration.md). --- ## Limits and troubleshooting ### Limits - **State in memory.** The queue lives in the memory of each middleware instance. Several Traefik replicas do not share it, and a restart empties it. A dynamic configuration change may also recreate the middleware with an empty queue: change the configuration outside peak hours. - **Abandoned places.** A visitor who closes the waiting page keeps their place in the queue. When their turn comes, the slot stays reserved until `inactivityTimeoutSeconds` runs out. The queue moves slower than its size suggests when many visitors give up. - **Approximate wait time.** The wait shown is computed from the position, not measured from real departures. - **Cookie without `Secure` behind a TLS proxy.** The cookie gets the `Secure` flag only when the TLS connection ends at Traefik. - **Reload URL.** The default page reloads with `` and a script that reloads the current path without its query string. On a deep path such as `/shop/item`, the meta refresh goes to `/shop/`. Edit the template if visitors must come back to the exact URL. ### Troubleshooting | Symptom | Cause | Fix | |---|---|---| | Traefik refuses the middleware: `invalid QueueManager configuration` | A value fails the [checks](../reference/configuration.md) | Fix the value given in the message | | The built-in English page shows instead of yours | Template not found or invalid | Check `queuePageFile` and the volume; the log says `Could not load queue page template` | | Empty texts on the waiting page | Language missing from the translations file, or file not found | Add the language, check `queueTranslationsFile` | | Changes to the template or translations do not show | Files are read at startup | Restart Traefik | | JSON `{"error":"capacity overload"}` without `detail` | [Rate limit](./rate-limit.md) on requests without the cookie | Keep cookies, or raise `rateLimitTimes` | | Visitors go back to the queue while browsing | `inactivityTimeoutSeconds` too short, or cookie expired | Raise `inactivityTimeoutSeconds` or `cookieMaxAgeSeconds` | ### Logs The plugin writes to the Traefik standard error, or to `logFile`, with the prefix `[Loch-Queue]`. Set `debug=true` to see each decision (client, position, promotions) while troubleshooting, and turn it off afterwards: it is verbose. --- ## JSON clients and single-page apps A request with the header `Accept: application/json` gets the queue status as JSON instead of the waiting page. The value must be exactly `application/json`: a list such as `application/json, text/plain` gets the HTML page. ```json { "error": "capacity overload", "handler": "Loch.sh Queue Manager", "detail": { "position": 12, "queueSize": 40, "estimatedWaitTime": 5, "refreshInterval": 20, "progressPercentage": 70, "debugInfo": "" } } ``` The status is `httpResponseCode` (`429` by default) and the response has the header `X-Loch-sh: Loch Queue`. When the visitor gets in, the request reaches the service as usual. ### Handle the queue in the client 1. Send the requests with the cookies: the plugin identifies the visitor by its cookie (`loch-queue-id`). The cookie is `HttpOnly`: the browser sends it, scripts cannot read it. 2. On a response with the `X-Loch-sh` header, show the position and wait time from `detail`. 3. Retry after `detail.refreshInterval` seconds. ```js async function callApi(url) { const res = await fetch(url, { headers: {Accept: 'application/json'}, credentials: 'same-origin', // send the queue cookie }); if (res.headers.get('X-Loch-sh')) { const body = await res.json(); if (body.detail) { showQueue(body.detail.position, body.detail.estimatedWaitTime); await new Promise((r) => setTimeout(r, body.detail.refreshInterval * 1000)); } else { await new Promise((r) => setTimeout(r, 60_000)); // rate limited } return callApi(url); } return res; } ``` ### Good to know - A client that does not keep the cookie becomes a new visitor at each request, at the end of the queue, and hits the [rate limit](./rate-limit.md) after a few requests. - A response without `detail` comes from the rate limit, not from the queue. - `debugInfo` is empty unless the `debug` option is on. Field details: [JSON response reference](../reference/json-response.md). --- ## Rate limit and client IP The plugin limits the requests that arrive **without the queue cookie**, per IP address. It protects the queue from clients that create a new visitor at every request. ### How it works - Every request without the cookie is counted for its IP address, over a sliding window of one minute, even when the service has free slots. - Above `rateLimitTimes` requests (`5` by default), the request gets a JSON response with the `httpResponseCode` status (`429`): ```json {"error": "capacity overload", "handler": "Loch.sh Queue Manager"} ``` - The plugin checks that the cookie is present, not its value. A browser that keeps the cookie is counted only on its first request. ### Who may be limited | Client | Why | What to do | |---|---|---| | `curl`, uptime monitors, webhooks, bots | They do not keep cookies | Use a cookie jar (`curl -c jar -b jar`), or raise `rateLimitTimes` | | Many visitors behind one IP (company network, NAT) | Their first requests all come from the same address | Raise `rateLimitTimes` | | Every visitor, with `useCookies=false` | No cookie is set, so every request is counted | Keep `useCookies=true`, or raise `rateLimitTimes` well above the number of requests per minute of a page | There is no option to turn the limit off: set `rateLimitTimes` high to make it loose. ```yaml labels: - traefik.http.middlewares.queue.plugin.queuemanager.rateLimitTimes=60 ``` ### Client IP address The plugin takes the first header found, in this order: 1. `X-Forwarded-For`, first entry 2. `X-Real-IP` 3. `Fly-Client-IP` 4. `CF-Connecting-IP` 5. The address of the TCP connection The value is used as is: a client that can set these headers can choose its IP for the rate limit and, with `useCookies=false`, its identity. Behind a proxy or a CDN, accept forwarded headers only from it, with `forwardedHeaders.trustedIPs` on the Traefik entrypoint, and do not enable `forwardedHeaders.insecure`. ```yaml title="traefik.yml" entryPoints: websecure: address: ":443" forwardedHeaders: trustedIPs: - "10.0.0.0/8" # your proxy or load balancer ``` --- ## Waiting page and translations The waiting page is an HTML template read by Traefik, with texts taken from a JSON translations file. Start from the files of the repository: `public/queue-page.html` and `public/translations.json`. ### Customize the template 1. Copy `public/queue-page.html` and edit it (style, logo, texts). 2. Mount it in the Traefik container, at `/var/public/queue-page.html` or at the path set in `queuePageFile`. 3. Restart Traefik. The template uses Go `html/template` syntax with `[[ ]]` delimiters, so that `{{ }}` stays free for your own scripts: ```html

[[.Translations.YourPosition]]: [[.QueueData.Position]] / [[.QueueData.QueueSize]]

[[ printf .Translations.RefreshTime .QueueData.RefreshInterval | safeHtml ]]
``` Keep an automatic reload based on `[[.QueueData.RefreshInterval]]`: a visitor only gets in by sending a new request. | Variable | Content | |---|---| | `.QueueData.Position` | Position in the queue, from 1 | | `.QueueData.QueueSize` | Number of visitors in the queue | | `.QueueData.EstimatedWaitTime` | Approximate wait, in minutes | | `.QueueData.ProgressPercentage` | Progress, from 0 to 100 | Every variable: [Template reference](../reference/template.md). The template is read when the middleware starts. If the file is missing or invalid, visitors get a built-in English page; a missing file is read again on the next waiting page, so mounting it later is enough. ### Edit or add a language The translations file has one entry per two-letter language code. It ships `en`, `fr`, `es`, `de`, `pt` and `it`. ```json { "nl": { "pageTitle": "Wachtrij - even geduld", "title": "U staat in de wachtrij", "introduction": "Er is momenteel veel vraag naar onze diensten. U wordt automatisch doorgestuurd zodra u aan de beurt bent.", "yourPosition": "Uw positie", "eta": "Geschatte wachttijd", "refreshTime": "Deze pagina wordt over %d seconden vernieuwd", "mins": "min." } } ``` - The language comes from the first entry of the browser's `Accept-Language` header (`fr-FR` gives `fr`). Without the header, `en` is used. - A language missing from the file gives a page with empty texts: there is no fallback to `en`. Keep every language your visitors may use. - `refreshTime` contains `%d` (the number of seconds) and may contain HTML. The `countdown` element is the one the default page counts down. - The file is read when the middleware starts: restart Traefik after editing it. ### Response code and content type The waiting page is sent with the `httpResponseCode` status (`429` by default) and the `httpContentType` header (`text/html; charset=utf-8`). --- ## Install (Queue) The plugin runs inside Traefik v3 (tested with v3.6). There is nothing to compile: Traefik downloads the source and interprets it. ### 1. Declare the plugin Add the plugin from the Traefik plugin catalog to the static configuration, then restart Traefik. ```yaml title="traefik.yml" experimental: plugins: queuemanager: moduleName: github.com/loch-sh/traefik-queue version: v1.0.1 ``` Same thing as command-line flags: ```bash --experimental.plugins.queuemanager.moduleName=github.com/loch-sh/traefik-queue --experimental.plugins.queuemanager.version=v1.0.1 ``` `queuemanager` is the plugin name used in the middleware options below. Every example of this documentation uses it. #### Alternative: local plugin For development or a host without Internet access, load the source code from disk instead: 1. Copy the repository to `/plugins-local/src/github.com/loch-sh/traefik-queue` inside the Traefik container (Traefik reads local plugins from `plugins-local/` in its working directory, `/` in the official image). 2. Declare it: ```yaml title="traefik.yml" experimental: localPlugins: queuemanager: moduleName: github.com/loch-sh/traefik-queue ``` ### 2. Provide the waiting page files The plugin reads two files from the Traefik filesystem when the middleware starts: | File | Default path | Source | |---|---|---| | HTML template | `/var/public/queue-page.html` | `public/queue-page.html` in the repository | | Translations | `/var/public/translations.json` | `public/translations.json` in the repository | Copy them next to your compose file and mount the folder in Traefik: ```yaml volumes: - ./public:/var/public:ro ``` Without the template, visitors get a built-in English page. Without the translations file, the page texts are empty. Other paths: options `queuePageFile` and `queueTranslationsFile`. ### 3. Attach the middleware Declare a middleware with the plugin, then add it to the routers to protect. **Docker labels**: ```yaml labels: - traefik.http.middlewares.queue.plugin.queuemanager.maxEntries=200 - traefik.http.routers.my-app.middlewares=queue ``` **File provider**: ```yaml title="dynamic.yml" http: middlewares: queue: plugin: queuemanager: maxEntries: 200 routers: my-app: rule: Host(`app.example.com`) service: my-app middlewares: - queue ``` A router declared with Docker labels uses this middleware as `queue@file`. Every option: [Configuration reference](./reference/configuration.md). ### Try it locally This compose file lets one visitor at a time reach a test service. ```yaml title="compose.yaml" services: traefik: image: traefik:v3.6 command: - --providers.docker=true - --providers.docker.exposedbydefault=false - --entrypoints.web.address=:80 - --experimental.plugins.queuemanager.moduleName=github.com/loch-sh/traefik-queue - --experimental.plugins.queuemanager.version=v1.0.1 ports: - "80:80" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./public:/var/public:ro whoami: image: traefik/whoami labels: - traefik.enable=true - traefik.http.routers.whoami.rule=Host(`localhost`) - traefik.http.routers.whoami.entrypoints=web - traefik.http.routers.whoami.middlewares=queue - traefik.http.middlewares.queue.plugin.queuemanager.maxEntries=1 ``` 1. Put `queue-page.html` and `translations.json` in `./public`, then run `docker compose up -d`. 2. Open `http://localhost` in a browser: you reach the service. 3. Open it in a private window: you get the waiting page. Close the first window: the private window gets through within one to two minutes (60 seconds of inactivity, then the next cleanup and page refresh). The repository also has a `docker-compose.yml` for development, with the local plugin. Some of its labels (`sessionTime`, `purgeTime`, `refreshInterval`) are not plugin options: use the names of the [Configuration reference](./reference/configuration.md). --- ## Loch Queue Manager Loch Queue Manager is a Traefik v3 middleware plugin (tested with Traefik v3.6). It lets a set number of visitors use a service at the same time and shows the others a waiting page until a slot frees up. ![The waiting page: position 12 of 40, about 5 minutes to wait](/img/screenshots/queue/next/en/queue-page.png) ### How it works 1. Each visitor gets an identifier: a cookie (`loch-queue-id` by default) or a hash of the IP address and User-Agent. 2. While fewer than `maxEntries` visitors are active, a new visitor goes straight through and becomes active. 3. When the service is full, the visitor joins a FIFO queue and gets the waiting page with HTTP `429`. The page reloads itself every 20 seconds. 4. Every 30 seconds, the plugin drops active visitors idle for more than 60 seconds and lets in the head of the queue. All values are configurable: [Configuration reference](./reference/configuration.md). ### Features - Cap on concurrent visitors, with an inactivity timeout and an optional hard session limit. - Waiting page built from your own HTML template, with position, queue size, approximate wait time and a progress bar. - Page texts in English, French, Spanish, German, Portuguese and Italian, chosen from the browser's `Accept-Language`. - JSON response instead of HTML for clients that send `Accept: application/json`. - Per-IP rate limit for requests without the queue cookie. ### Next steps 1. [Install the plugin](./install.md) and attach the middleware to a router. 2. [Size the capacity and timeouts](./guides/capacity.md). 3. [Customize the waiting page](./guides/waiting-page.md). 4. Read the [limits](./guides/good-to-know.md) before going to production. ### About and license Loch Queue Manager is published under the Apache License 2.0. Source code: [github.com/loch-sh/traefik-queue](https://github.com/loch-sh/traefik-queue). From the `NOTICE` file: > Loch Queue Manager > Copyright 2025 Loch.sh > > This product is a derivative work of Traefik Queue Manager > (https://github.com/hhftechnology/traefik-queue-manager), > developed by HHF Technology and licensed under the Apache License, Version 2.0. > > The original source files have been modified by Loch.sh. Changes include, > among others: renaming of the Go package and module, split of the code into > multiple files, JSON API responses and rate limiting. --- ## Configuration reference (4) Options go under the plugin name `queuemanager` in the middleware configuration: - Docker labels: `traefik.http.middlewares..plugin.queuemanager.