Before anything else, the administrator must initialize a remote server. This requires root SSH access to an AlmaLinux 10 machine with a public IP and UDP port 51820 open.
$ devinity server init root@example.com $ devinity server init root@example.com -i ~/.ssh/id_ed25519 $ devinity server init root@example.com -n myserver --hostname cloud.example.com
The command copies the Devinity binary to the remote server, installs all dependencies (WireGuard, Caddy, Git, language runtimes, etc.), hardens SSH, configures the firewall, and sets up the WireGuard VPN tunnel. The administrator is automatically enrolled and can immediately connect.
~/.ssh/id_ed25519.gatekeeper.-l java -l node). Defaults to all supported
languages.devinity and gatekeeper
system users with appropriate permissions..internal domains resolve to the VPN
interface).Check which version of the CLI you have installed:
$ devinity --version
Before you can interact with a Devinity server, you need an active VPN connection. All Git operations, app management, and SSH sessions travel through the WireGuard tunnel.
The administrator is automatically enrolled after running
devinity server init. Other users must complete the
enrollment process first (see section 13).
$ devinity vpn connect example.com
Replace example.com with the enrollment name you chose
during setup. If you only have one enrollment, the name can be
omitted:
$ devinity vpn connect
The tunnel stays up until you explicitly disconnect or reboot.
Run vpn without a subcommand to list all configured
VPN enrollments and their connection status:
$ devinity vpn
Check the current VPN connection status:
$ devinity vpn status
$ devinity vpn disconnect
Administrators can open an interactive SSH session through the tunnel:
$ devinity server ssh example.com
Run a single command on the server without an interactive session:
$ devinity server ssh example.com -c "uptime"
If you only have one admin server, the name can be omitted:
$ devinity server ssh
Create a new repository on the server:
$ devinity git init myapp
This creates a bare Git repository on the server and clones it
locally into a myapp directory with the Devinity
remote already configured.
$ devinity git clone myapp $ devinity git clone myapp ~/projects/myapp
This clones the repository from the server via SSH over the VPN and configures the Devinity remote. An optional second argument specifies the target directory (defaults to the app name).
If you already have a local Git repository and want to add the Devinity remote and signing configuration:
$ devinity git configure myapp
If you are inside a Devinity-configured repository, you can bring up the VPN automatically without specifying the enrollment name:
$ devinity git connect
The command reads the enrollment name from the repository's Devinity remote URL and connects the VPN tunnel. If you are already connected to a different enrollment, it disconnects first and switches to the correct one.
A standard git push triggers the full build and
deploy pipeline:
$ git add . $ git commit -m "add feature" $ git push
The server auto-detects the project type from files in the repository and runs the appropriate build. It also detects how to start the app automatically. No configuration file is needed in most cases. Build output is streamed back to your terminal during the push. See section 5 for the full list of supported project types.
When you git push, the server auto-detects the
project type by looking for specific files in the repository.
It runs the appropriate build command and, in most cases, also
detects how to start the app. No configuration file is needed
for common project setups.
The following project types are detected and built automatically. Detection is checked in priority order (first match wins):
| Project Type | Detected By | Build Command |
|---|---|---|
| Java (Maven) | pom.xml | mvn clean package |
| Java (Gradle) | build.gradle(.kts) | ./gradlew build or gradle build |
| Deno | deno.json / deno.jsonc | deno install |
| Bun | package.json + bun.lockb | bun install |
| pnpm | package.json + pnpm-lock.yaml | pnpm install |
| Yarn | package.json + yarn.lock | yarn install |
| npm | package.json | npm install |
| Go | go.mod | go build |
| Rust | Cargo.toml | cargo build --release |
| Ruby | Gemfile | bundle install |
| PHP | composer.json | composer install |
| .NET | .sln / .csproj | dotnet publish |
| Elixir | mix.exs | mix deps.get && mix compile |
| Scala | build.sbt | sbt compile |
| Swift | Package.swift | swift build |
| Zig | build.zig | zig build |
| Python (modern) | pyproject.toml | pip install . |
| Python (legacy) | requirements.txt | pip install -r requirements.txt |
After building, the server tries to detect how to start the app.
If the .devinity file has a [start]
section, that takes priority. Otherwise, the following rules
apply:
| Framework / Type | Detected By | Start Command |
|---|---|---|
| Node.js (npm) | package.json with scripts.start | npm start |
| Node.js (Bun) | bun lockfile present | bun run start |
| Node.js (pnpm) | pnpm-lock.yaml present | pnpm start |
| Node.js (Yarn) | yarn.lock present | yarn start |
| Java (Maven) | pom.xml | java -jar target/*.jar |
| Java (Gradle) | build.gradle(.kts) | java -jar build/libs/*.jar |
| Go | go.mod (parses module name) | ./<binary-name> |
| Deno | deno.json with tasks.start | deno task start |
| Deno (fallback) | main.ts exists | deno run --allow-net main.ts |
| Rust | Cargo.toml (parses package name) | ./target/release/<name> |
| Laravel (PHP) | artisan file | php artisan serve |
| Rails (Ruby) | bin/rails | bundle exec rails server |
| Django (Python) | manage.py | python manage.py runserver |
| Python (generic) | main.py | python main.py |
If neither the .devinity file nor auto-detection
provides a start command, the build fails with an error message
asking you to create a .devinity file with a
[start] section.
The .devinity file is an optional INI-style
configuration file placed in the repository root. It gives you
explicit control over how the app is built, started, and deployed.
When omitted, the server relies on auto-detection
(see section 5).
[start] java -jar target/app.jar
[setup] ./mvnw clean package [start] java -jar target/app.jar [auto-deploy] true [devinity-port-name-mapping] APP_PORT=8080 [domains] myapp.example.com
java -jar target/app.jar,
npm start,
./target/release/myapp,
python main.py../mvnw clean package -DskipTests,
npm ci && npm run build,
pip install -r requirements.txt.false to build without deploying
automatically. Use devinity deploy <app>
<build> to deploy manually.VAR_NAME=<port>.
The base variable DEVINITY_HTTP_PORT is
always set regardless.[domains] myapp.example.com api.example.comCustom domains are served alongside the auto-generated subdomain when the app is exposed. Each domain must resolve to the server's public IP.
$ devinity apps
Shows all apps on the server with their current status.
List builds for an app:
$ devinity builds myapp
View the log for a specific build:
$ devinity builds myapp 3
Follow build output in real time:
$ devinity builds myapp 3 -f
List deployments for an app:
$ devinity deploy myapp
Deploy a specific build number:
$ devinity deploy myapp 3
$ devinity stop myapp $ devinity start myapp $ devinity restart myapp
View runtime logs for a running app:
$ devinity logs myapp
Follow logs in real time:
$ devinity logs myapp -f
Show the last N lines:
$ devinity logs myapp -n 100
Environment variables let you configure your app without changing code. Use them for database URLs, API keys, feature flags, and other runtime settings.
$ devinity env myapp
Set one or more variables (KEY=VALUE), or remove with -KEY:
$ devinity env myapp DATABASE_URL=postgres://... PORT=8080 $ devinity env myapp -OLD_KEY
Reset build defaults for overridden keys while keeping any user-added keys:
$ devinity env myapp --reset-keep-added
$ devinity env myapp --reset
Every deployed app gets an internal VPN URL. Optionally, it can also get public URLs when exposed. Both internal and public URLs follow the same naming strategy.
Internal URLs are always available inside the VPN with self-signed TLS. The format depends on how many branches are deployed:
Single branch deployed:
https://<app>.<server>.internal
Multiple branches deployed:
https://<app>.<branch>.<server>.internal
Examples (server hostname: cloud.devinity.dev):
myapp (only main deployed):
https://myapp.cloud.devinity.dev.internal
myapp (main + staging deployed):
https://myapp.cloud.devinity.dev.internal (main)
https://myapp.staging.cloud.devinity.dev.internal (staging)
When exposed via devinity expose, public URLs follow
the same rules. The branch is included or omitted using the same
logic as internal URLs:
Single branch deployed:
https://<app>.<server>
Multiple branches deployed:
https://<app>.<server> (main/master)
https://<app>-<branch>.<server> (other branches)
The branch is included in URLs only when needed to avoid ambiguity:
When exposed with --mode ip or --mode both,
apps are also reachable via the server's public IP with a path prefix:
Single branch: https://<ip>/<app>/ Multiple: https://<ip>/<app>/<branch>/
By default, deployed apps are only reachable inside the VPN. The
expose command makes them publicly accessible through
a Caddy reverse proxy with automatic HTTPS.
devinity server config --domain example.comPublic routing requires two DNS A records. One for the base subdomain and one wildcard record for app subdomains.
Required DNS records (example for devinity.dev):
TYPE NAME VALUE A cloud 116.202.19.183 A *.cloud 116.202.19.183
The first record handles requests to cloud.devinity.dev.
The wildcard record routes all app subdomains like
myapp.cloud.devinity.dev to the server.
$ devinity expose myapp on
You can specify the exposure mode:
$ devinity expose myapp on --mode domain (default, HTTPS via domain) $ devinity expose myapp on --mode ip (direct IP access) $ devinity expose myapp on --mode both (domain + IP)
$ devinity expose myapp off
Run without arguments to list the current exposure status for all apps:
$ devinity expose
Every deployed app exposes metadata through HTTP response headers and a JSON endpoint on its internal VPN address (see section 9).
Response headers added to every request:
X-Deploy-App: myapp X-Deploy-Branch: main X-Deploy-Build: 42 X-Deploy-Commit: abc123def456... X-Deploy-Time: 2026-01-15T10:30:45Z
JSON endpoint at
/_d/info.json returns the same data:
$ curl https://myapp.cloud.devinity.dev.internal/_d/info.json
{"app":"myapp","branch":"main","build":42,
"commit":"abc123...","deployedAt":"2026-01-15T10:30:45Z"}
Every Devinity server includes a web-based Git repository browser powered by cgit. It is accessible inside the VPN at:
https://git.internal
The interface lets you browse repositories, view commits, diffs, and file trees from any browser. It is read-only and accessible only through the VPN -- cloning and pushing still happen via SSH.
View all configuration values:
$ devinity server config
Set the server domain (also updates system hostname):
$ devinity server config --domain example.com
Set the server public IP address:
$ devinity server config --ip 203.0.113.10
Maintenance mode makes all publicly exposed apps return a maintenance page instead of their normal content. Apps remain accessible inside the VPN.
$ devinity maintenance on $ devinity maintenance on --message "Back in 30 minutes" $ devinity maintenance off
View current maintenance status:
$ devinity maintenance
Adding a new developer to a Devinity server is a two-step process between the developer and the administrator.
The developer runs the enroll command to generate credentials:
$ devinity vpn enroll example.com user@example.com jdoe \
203.0.113.10 <server-wg-pubkey>
The command outputs an SSH public key and a WireGuard public key. The developer sends both to the administrator through a secure channel.
The administrator registers the user on the server:
$ devinity server users add user@example.com jdoe \
<ssh-pubkey> <vpn-pubkey>
The server assigns a VPN IP and hot-reloads WireGuard. The developer can now connect.
During registration the server derives a VPN IP from the user's WireGuard public key. If that address is already taken, the server assigns a different one and the administrator is notified. The developer must then sync the new address locally:
$ devinity vpn sync-client-ip example.com 10.0.1.42
Replace example.com with the enrollment identity and
10.0.1.42 with the IP the administrator provides.
After syncing, connect normally with
devinity vpn connect.
If the server's hostname or IP changes, each user updates their local enrollment:
$ devinity vpn update-hostname example.com new.example.com $ devinity vpn update-hostname example.com 198.51.100.20
Each deployment has a persistent data directory that survives
redeploys. Use it for uploaded files, SQLite databases, or any
data that should not be wiped on each push. The path is available
via the DEVINITY_DATA_DIR environment variable and
follows the pattern:
/home/devinity/.devinity/apps/<app>/data/<branch>/
This directory is created per-branch, so main and
staging have separate data stores.