#Installation
Install Bay on a Linux host in five steps: the binary, a systemd unit, a check, and the step people miss, granting a human or a CI job the right to deploy.
Bay is a single static binary plus a data directory. The only remote surface it
has is SSH: alepha deploy drives your own ssh binary and talks to Bay's
control socket on the other side. There is no admin panel and no inbound
control port.
#Requirements
- Linux on
amd64orarm64. - Docker Engine 25 or newer with the compose plugin, on cgroup v2: Bay runs every app in a container. The installer sets Docker up when it is missing.
- Ports 80 and 443 free. Port 80 answers the ACME HTTP-01 challenge even if you only serve HTTPS.
- A DNS record pointing each app's domain (or a wildcard under a base domain) at the machine.
#1. Install the binary
curl -sSL https://alepha.dev/bay/install.sh | sudo sh
The script installs Docker through get.docker.com when it is missing, fetches
the newest release for the host's architecture from the
GitHub releases, verifies it
against SHA256SUMS, and lays out:
| Path | What it is |
|---|---|
/opt/bay/bin/bay |
the binary |
/usr/local/bin/bay |
a symlink, so bay is on the PATH of a non-interactive SSH shell |
/opt/bay/data |
the data root (--root): releases, state, each app's .env and SQLite files |
/run/bay/control.sock |
the control socket, created by bay serve at startup (with the unit of step 2) |
It stops there on purpose: the systemd unit, the base domain, the ACME email
and the deploy group need decisions a curl | sh cannot make for you.
| Variable | Default | Effect |
|---|---|---|
BAY_VERSION |
latest |
a version such as 1.0.0, or latest |
BAY_PREFIX |
/opt/bay |
install root |
BAY_BIN_LINK |
/usr/local/bin/bay |
symlink put on the PATH |
BAY_INSTALL_DOCKER |
yes |
no stops instead of installing Docker |
Re-running the script upgrades in place: the binary is replaced atomically and
a running bay service is restarted.
Check the host before the first start, as root:
sudo bay doctor
It prints one line per check (Docker reachable and recent, cgroup v2, compose,
ports 80 and 443, free disk under --root, /opt/bay/data by default) and
exits with status 1 when one fails. Once Bay runs it holds ports 80 and 443,
and the doctor accepts that only when a Bay answers on the socket it is given,
so on a live host:
sudo bay doctor --control-socket /run/bay/control.sock
#Building it yourself
Bay is CGO_ENABLED=0, so it cross-compiles from any machine, a Mac included.
From the bay/ folder of the repository:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bay ./cmd/bay
scp bay root@HOST:/opt/bay/bin/bay
uname -m on the host tells you the architecture: x86_64 is amd64,
aarch64 is arm64.
#2. Run it under systemd
# /etc/systemd/system/bay.service
[Unit]
Description=Bay - Alepha application server
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
# Bay is the reverse proxy: if it dies, every hosted app goes dark.
Restart=always
RestartSec=2
WorkingDirectory=/opt/bay
RuntimeDirectory=bay
RuntimeDirectoryMode=0755
ExecStart=/opt/bay/bin/bay serve \
--root /opt/bay/data \
--base-domain bay.example.com \
--addr :80 \
--tls --tls-addr :443 \
--acme-email you@example.com \
--control-socket /run/bay/control.sock
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now bay
RuntimeDirectory=bayhas systemd create/run/bayon start and remove it on stop. Bay creates the directory itself when it is missing.--base-domainis what an app without a domain of its own is served under:myapp.bay.example.com, ormyapp-staging.bay.example.comfor a non-production environment.--addrand--tls-addrdefault to:8080and:8443, which suits a test install. A public host sets them to:80and:443.
⚠️ Do not add PrivateTmp=yes. A deploy carrying secrets stages them in
/tmp as the deploying user, and Bay reads them as root: a private /tmp
makes those two different directories, and every such deploy fails with
no secrets file at /tmp/....
⚠️ Behind a Cloudflare proxy, use SSL mode Full (strict), never Flexible. Flexible talks plain HTTP to the origin, Bay redirects it to HTTPS, and the browser reports "too many redirects" with nothing in Bay's logs to say why.
#3. Check it came up
sudo bay status --control-socket /run/bay/control.sock
As root, it prints the hosted apps: none yet on a fresh install.
bay looks for the socket at ./bay-data/control.sock, which is not where this
unit puts it, so every client command has to be told. In a shell on the host,
one line does it for the session, and the guides that follow assume it:
export BAY_SOCKET=/run/bay/control.sock
Use the flag under sudo, which resets the environment. Otherwise prefer
BAY_SOCKET, which saves repeating it.
#4. Grant a user the right to deploy
Everything above leaves a Bay that only root can drive. A deploy over SSH runs as an ordinary user, who needs two things root never did.
#Membership of bay-control
Bay creates a system group (bay-control, or --control-group) and gives the
socket mode 0660 in that group. Membership is the deploy permission:
there is no token to issue, store or revoke. Bay creates the group; adding
members is up to you.
sudo usermod -aG bay-control deploy
Group membership is read at login, so an existing SSH session keeps its old
groups: open a new one. With ControlMaster/ControlPersist in your
~/.ssh/config, close the shared connection first with ssh -O exit HOST.
Without it, the error blames the wrong thing:
error: control api unreachable (is `bay serve` running?):
dial unix /run/bay/control.sock: connect: permission denied
#bay on the non-interactive PATH
alepha deploy runs ssh HOST bay ..., a non-interactive shell that reads
neither ~/.profile nor the interactive part of ~/.bashrc. The installer's
/usr/local/bin/bay symlink covers it; a hand install needs it too:
sudo ln -s /opt/bay/bin/bay /usr/local/bin/bay
#Verify both at once
ssh deploy@HOST 'groups; command -v bay; bay status --control-socket /run/bay/control.sock'
You want bay-control among the groups, a path for bay, and app output rather
than a permission error. From the project, alepha infra login --env production
checks the same things and changes nothing.
#5. Point a project at it
1// alepha.config.ts 2import { defineConfig } from "alepha/cli/config"; 3import { bay, infra } from "alepha/cli/infra"; 4 5export default defineConfig({ 6 plugins: [ 7 infra({ 8 environments: { 9 production: bay({10 host: "bay-prod",11 socket: "/run/bay/control.sock",12 domain: "myapp.com",13 }),14 },15 }),16 ],17});
alepha deploy --env production
hostis handed tosshverbatim, so an alias from~/.ssh/configis the best value: it carries the port, identity file and jump host.BAY_HOSToverrides it.socketis required whenever the root is not./bay-dataunder the deploying user's home, which is every install following this page.- Never point
hostat a domain proxied by a CDN: the CDN answers HTTP, not SSH.
The adapter's own options are documented in Bay Deployment.
#Upgrading Bay
Run the installer again, or replace the binary and restart:
sudo install -m 0755 bay /opt/bay/bin/bay && sudo systemctl restart bay
/opt/bay/data is the only directory that must survive an upgrade, and it is
untouched. Hosted apps keep running until the restart cycles them; an app that
was stopped on purpose stays stopped.
Keep the host's Bay at least as new as the alepha CLI deploying to it.
The CLI names the version gate it hit (bay too old to read the artifact from
stdin, or not knowing --secrets-file), and in both cases nothing was
deployed: the release that was serving still is.
bay --version prints dev for a binary you built yourself, so the file's
date (ls -l /opt/bay/bin/bay) is the practical answer there.