Server setup
You need Bun 1.3+ on the server, and Docker only if you plan to deploy container apps. Then:
$ git clone https://github.com/chussum/arcturus.git && cd arcturus $ bun run server:up
That’s it — it installs dependencies, builds the dashboard and starts everything under pm2, all idempotent. Open http://<host>:7777.
On the first run, if there's no encrypted .env.secrets yet, server:up offers three ways to set up the production secrets — auto-generate them, enter your own ARCTURUS_ENV_KEY/ARCTURUS_JWT_SECRET, or just print a guide for injecting them via environment variables — then prompts you to set the admin password.
Want to skip the prompts? Set the keys (or ARCTURUS_ADMIN_PASSWORD) beforehand. With no terminal (CI/pm2) it prints the guide and exits instead of booting half-configured.
Installing Docker (no Docker Desktop needed)
Arcturus talks to the Docker API socket directly, so Docker Desktop is never required — which matters because Desktop needs a paid subscription at companies with 250+ employees or $10M+ annual revenue. Docker Engine and Colima are free open source for any commercial use.
# Linux server — Docker Engine (Apache 2.0): $ curl -fsSL https://get.docker.com | sh $ sudo usermod -aG docker $USER # re-login afterwards
# macOS — Colima (MIT) + the docker CLI: $ brew install colima docker $ colima start
On macOS, the docker CLI finds Colima's socket automatically — but Arcturus reads DOCKER_HOST instead of docker contexts.
Add this to ~/.zshrc (not apps/api/.env), then re-open your terminal or run source ~/.zshrc:
export DOCKER_HOST=unix://$HOME/.colima/default/docker.sock
Operating the server
| Command | What it does |
|---|---|
| bun run server:up | Install → build → start (safe to run multiple times) |
| bun run server:up -- --build | Same, but forces a dashboard rebuild — use after git pull to pick up any code changes |
| bun run server:down | Stop everything |
| bun run server:status | Process status |
| bun run server:logs | Tail logs |
Hardening secrets (macOS)
Out of the box, the two generated keys (session signing, env encryption) live as plaintext files under data/ — fine for trying things out, not for a long-lived server. One idempotent command moves them into a dotenvx-encrypted .env.secrets at the repo root, with the only decryption key stored in the macOS Keychain:
$ bun run secrets:init
Existing values are migrated as-is, so encrypted app env vars and live sessions survive — and when neither a key nor a file exists yet, fresh ones are generated. server:up runs this for you on the first production boot; add --input to type or paste your own values instead of generating them. To add or change a secret later, edit .env.secrets with plain KEY=value lines, then re-encrypt and restart:
$ bun run secrets:update $ bun run server:restart
Before moving to another machine, back up the Keychain key:
security find-generic-password -s arcturus-dotenvx -w
Linux has no Keychain — export ARCTURUS_JWT_SECRET / ARCTURUS_ENV_KEY yourself or stay on the file fallback. Details in Security notes.
Install the CLI
The gateway serves the installer and the platform binaries itself. Build them once on the server, then anyone on the team installs with one command:
# on the server, once: $ cd apps/cli && bun run build:all
# on any machine: $ curl -fsSL http://<host>:7777/install.sh | sh
Create an API token in the dashboard (Dashboard → API Tokens) and log in:
$ arcturus login --server http://<host>:7777 --token <arc_...>
First deploy
From the project directory, one command. What happens next depends on what’s inside:
- No Dockerfile → deployed as a static site, live the moment the upload finishes. A single
.htmlfile becomes the index automatically. - Dockerfile present → the image is built and run as a container, routed automatically with a dedicated port.
$ arcturus deploy ✦ blog — packing 42 files (1.2 MB) ✦ uploading … done, release ready in 0.4s ● live at http://<host>:7778/alice/blog
By default the app is named after the directory. Pin a name per project with an arcturus.json next to your code:
{ "name": "my-app" }
Automatic .env (container apps)
When you deploy a directory with a Dockerfile, Arcturus reads the .env next to it, stores the variables as the app's env encrypted at rest, and applies them to the container — exactly like setting them by hand. This happens only on first creation of the app; redeploys leave an existing app's env untouched (change it later with arcturus env or the dashboard). Every .env* file (.env, .env.local, .env.production …) is always excluded from the build image, so secrets never land in an image layer. Values are cleaned as they're read: surrounding whitespace is trimmed and a trailing inline comment (a # preceded by a space, as in TOKEN=abc # note) is dropped — a # with no leading space (#fff, ab#cd, URL fragments) stays put, and wrapping a value in quotes keeps a literal # or edge spaces. The same cleanup applies to arcturus env --set.
arcturus deploy options
--name <appName>— app name (defaults toarcturus.json'sname, else the directory name)--name <owner>/<appName>— deploy to another account's app you have manage access to (the app must already exist)--dir <path>— project directory to deploy (default:.)--env-file <path>— env file to seed a new container app (default:.envin the dir)--no-env-file— don't read any.envfile
Prefer no CLI at all? The dashboard accepts a zip upload — same pipeline, same result.
Day-2 operations
Environment variables
Saved values recreate the container instantly — no rebuild. They’re encrypted at rest (AES-256-GCM) in SQLite.
$ arcturus env my-app --set DATABASE_URL=postgres://…
History & rollback
The last 5 releases are kept per app (ARCTURUS_KEEP_RELEASES). Static releases are versioned directories; container releases are per-deployment image tags — rollback is instant either way.
$ arcturus deployments my-app # history, live / rollbackable markers $ arcturus rollback my-app <id> # restore a previous release
Logs & teardown
$ arcturus logs my-app # follow container logs $ arcturus destroy my-app # delete the app
Direct docker access from the terminal
For container apps you can reach the running container with the docker CLI — handy for tailing logs or poking around inside. Each container is named deterministically as arcturus--<username>--<app>, so there’s no ID to look up.
On the host — SSH into the server, then:
$ docker ps --filter label=arcturus.managed=true # list every Arcturus app container $ docker logs -f arcturus--alice--my-app # follow one app’s logs $ docker exec arcturus--alice--my-app ls -al # run a one-off command $ docker exec -it arcturus--alice--my-app sh # open a shell inside
From your laptop, without logging in — point your local docker at the server over SSH and run the same commands. Replace <ssh-user> with your SSH login on the server (the OS account, not your Arcturus username):
$ export DOCKER_HOST=ssh://<ssh-user>@<server> # once per shell $ docker logs -f arcturus--alice--my-app # …now runs against the server $ ssh <ssh-user>@<server> docker logs -f arcturus--alice--my-app # or a one-off, no setup
Needs the docker CLI on your laptop and SSH access that can run docker on the server. The shell must exist in the image — sh works for most bases, but distroless/scratch images have none. Containers run as the image’s USER (often root).
Set ARCTURUS_SSH_USER on the server and the dashboard’s terminal-access hint pre-fills your login in place of <ssh-user>.
Team accounts
The admin issues invite links from the dashboard; members sign up through them and manage their own apps. Roles are simple: admin and member.
App sharing
Any app owner (or admin) can share an app from its detail page — with specific teammates or with the whole team. Two access tiers:
- Can view — sees the app card, status, URL, and deployment history. Env vars and live logs are hidden.
- Can manage — everything view can see, plus deploying new builds, env editing, memory/routing settings, stop, restart, and rollback. Deleting and changing sharing settings remain owner/admin-only.
A manage collaborator can deploy to the shared app — arcturus deploy --name <owner>/<app>, or the dashboard's redeploy button.
Deleting and sharing config stay owner/admin-only.
Routing modes
Apps are served at /<username>/<app-name> behind the gateway. Path-prefix proxying breaks apps that reference assets by absolute path, so Arcturus layers three remedies:
- Prefix strip +
X-Forwarded-Prefix— apps with a configurable base path work as-is. - Referer fallback — unmatched requests (
/static/app.js) are re-routed to the app the browser is viewing. - Dedicated port — every container app keeps a fixed port (
30000+) with zero base-path issues.
New container apps default to the “redirect” routing mode — visitors are bounced to the dedicated port, so the base-path problem never appears.
Prefer path-based serving? Switch the app to “proxy” in its detail page.
The dedicated port is auto-assigned, but Port settings on the app page lets you set a specific one (e.g. 8080): it checks the port is free, recreates the container to bind it, and can reset back to auto-assign.
Configuration reference
Copy the examples and adjust — every value is optional, sensible defaults apply:
$ cp apps/api/.env.example apps/api/.env $ cp apps/web/.env.example apps/web/.env
| Env var | Default | Description |
|---|---|---|
| ARCTURUS_PORT | 7777 | Control-plane port (API + dashboard) |
| ARCTURUS_APPS_PORT | 7778 | Separate origin for deployed apps (isolates app code from the control-plane cookie) |
| ARCTURUS_DATA_DIR | ./data | SQLite, static releases, build workspace |
| ARCTURUS_PORT_POOL_START/END | 30000 / 30999 | Dedicated port pool for container apps |
| ARCTURUS_MAX_UPLOAD_MB | 256 | Upload size limit |
| ARCTURUS_KEEP_RELEASES | 5 | Releases kept per app for rollback |
| ARCTURUS_CLI_DIST | apps/cli/dist/cli | Cross-compiled CLI binaries to serve |
| ARCTURUS_DEV_ORIGINS | — | Extra allowed origins for next dev |
| ARCTURUS_SSH_USER | — | SSH login pre-filled in the dashboard’s terminal-access hint (the host OS account, not an Arcturus username) |
The secrets aren’t in this table on purpose. ARCTURUS_JWT_SECRET and ARCTURUS_ENV_KEY are managed in the encrypted .env.secrets via Hardening secrets, and ARCTURUS_ADMIN_PASSWORD is set via env or prompted in the terminal on first run.
All three are generated for local dev and required in production — see Security notes.
Security notes
Secrets & keys
- App env vars are encrypted at rest (AES-256-GCM, in SQLite). The key comes from
ARCTURUS_ENV_KEY, or is generated once intodata/env-key. Rows saved as plaintext by older versions are encrypted automatically on the next boot. - File permissions are enforced on every boot —
data/is0700; the DB and key files are0600. - The generated plaintext key files are a zero-config convenience. For a long-lived server, move them into the encrypted file + Keychain with
bun run secrets:init— see Hardening secrets.
Losing or changing ARCTURUS_ENV_KEY makes existing env values undecryptable. Back the key up alongside your data directory.
After secrets:init the key lives in the Keychain — export it with:
security find-generic-password -s arcturus-dotenvx -w
Accounts & tokens
- Login, signup and API-token creation are limited to 10 requests/minute per IP to blunt password guessing. Deploys get 20/minute.
- Logging out revokes the session server-side, immediately. Even a leaked cookie value stops working after logout.
- Password change and admin reset both revoke all sessions. Members change their password from Dashboard → Account (current password required); all other sessions are invalidated immediately. Admins can generate a one-time reset link for any member from the Team page — valid for one hour, single-use, revokes all sessions on completion.
- API tokens can expire. Pick 30/90/365 days or no expiry at creation. Expired tokens are rejected at authentication, and manual revocation always works.
Container isolation
- Deployed containers run with minimal privileges. All Linux capabilities dropped,
no-new-privileges, CPU/PID limits, and a per-app memory cap (set in the dashboard, ceilingARCTURUS_MAX_MEMORY_MB). Image builds get the same memory cap. - Tenants can’t reach each other. Every app sits on a dedicated bridge network with inter-container traffic disabled. Container→host traffic is not blocked, though — treat deployed code as something your team trusts.
- Containers honor the image’s
USERby default (often root). SetARCTURUS_CONTAINER_USER(e.g.1000:1000) to force a non-root user. - Container apps are also reachable on their dedicated host port, not only through the gateway path — intended, since the “jump to dedicated port” access mode relies on it. Keep the port pool firewalled to your team’s network.
Supply chain
- Dependency installs skip npm versions younger than 7 days (
minimumReleaseAgeinbunfig.toml). That buys time for a compromised release to be discovered and unpublished before it reaches this repo. Exempt an urgent patch per package viaminimumReleaseAgeExcludes.
Troubleshooting
An app’s assets 404 behind the path prefix
That’s the absolute-path problem — see Routing modes. It only appears in “proxy” mode; container apps default to “redirect”, so switch the app back to “redirect” and it’s served from its dedicated port.
Port already in use after a restart
If a process lingers and you hit EADDRINUSE, clear it by port rather than by name:
$ lsof -ti :7777 | xargs kill -9
Lost the admin password
You set it on the very first boot — interactively in the terminal, or beforehand via ARCTURUS_ADMIN_PASSWORD. If another admin exists, they can send you a one-time reset link from the Team page.
Deploy fails after moving the server
If you migrated the data/ directory but not the env key, container apps with saved env vars can’t decrypt them — restore data/env-key (or your ARCTURUS_ENV_KEY) from backup. If the old machine used secrets:init, copy .env.secrets too and move the Keychain key: read it on the old machine with security find-generic-password -s arcturus-dotenvx -w, then store it on the new one with security add-generic-password -s arcturus-dotenvx -a "$USER" -w "<key>".