Devinity User Guide

1. Table of Contents

2. Server Initialization

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.

2.1. Initialize

   $ 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.

2.2. Options

-i, --identity <path>
Path to the SSH private key. Use this if the key is not the default ~/.ssh/id_ed25519.
-n, --name <name>
Local name for this server configuration. Defaults to the hostname or IP of the remote host.
--hostname <hostname>
Sets the system hostname on the remote server. This hostname is also used as the base for app URLs.
--tcp-ports <ports>
Additional TCP ports to open in the firewall. Ports 80 and 443 are always opened for HTTP/HTTPS.
--udp-ports <ports>
Additional UDP ports to open. Port 51820 is always opened for WireGuard.
--gatekeeper <username>
Username for the SSH gateway user. Defaults to gatekeeper.
-l, --language <lang>
Languages to install on the server. Repeatable (e.g., -l java -l node). Defaults to all supported languages.

2.3. What Init Does

3. Connecting

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).

3.1. Bring Up the VPN

   $ 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.

3.2. List VPN Configurations

Run vpn without a subcommand to list all configured VPN enrollments and their connection status:

   $ devinity vpn

3.3. VPN Status

Check the current VPN connection status:

   $ devinity vpn status

3.4. Disconnect

   $ devinity vpn disconnect

3.5. SSH to the Server

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

4. Working with Git

4.1. Create a Repository

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.

4.2. Clone an Existing Repository

   $ 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).

4.3. Configure an Existing Repo

If you already have a local Git repository and want to add the Devinity remote and signing configuration:

   $ devinity git configure myapp

4.4. Connect from a Repository

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.

4.5. Push and Deploy

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.

5. Build and Start Detection

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.

5.1. Supported Build Types

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

5.2. Auto-Detected Start Commands

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.

6. The .devinity File

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).

6.1. Minimal Example

   [start]
   java -jar target/app.jar

6.2. Full Example

   [setup]
   ./mvnw clean package

   [start]
   java -jar target/app.jar

   [auto-deploy]
   true

   [devinity-port-name-mapping]
   APP_PORT=8080

   [domains]
   myapp.example.com

6.3. Sections

[start] (optional)
The command to launch the app. If omitted, the server attempts to auto-detect it from the project type. Examples: java -jar target/app.jar, npm start, ./target/release/myapp, python main.py.
[setup] (optional)
Custom build commands to run before the auto-detected build. Use this when the default build command is not enough. Examples: ./mvnw clean package -DskipTests, npm ci && npm run build, pip install -r requirements.txt.
[auto-deploy] (optional, default: true)
Set to false to build without deploying automatically. Use devinity deploy <app> <build> to deploy manually.
[devinity-port-name-mapping] (optional)
Maps the allocated port to an app-specific environment variable. Format: VAR_NAME=<port>. The base variable DEVINITY_HTTP_PORT is always set regardless.
[domains] (optional)
Declares custom domains for public exposure, one per line:
   [domains]
   myapp.example.com
   api.example.com
Custom domains are served alongside the auto-generated subdomain when the app is exposed. Each domain must resolve to the server's public IP.

7. Managing Apps

7.1. List Apps

   $ devinity apps

Shows all apps on the server with their current status.

7.2. View Builds

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

7.3. Deploy a Build

List deployments for an app:

   $ devinity deploy myapp

Deploy a specific build number:

   $ devinity deploy myapp 3

7.4. Start, Stop, and Restart

   $ devinity stop myapp
   $ devinity start myapp
   $ devinity restart myapp

7.5. View Logs

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

8. Environment Variables

Environment variables let you configure your app without changing code. Use them for database URLs, API keys, feature flags, and other runtime settings.

8.1. View Current Variables

   $ devinity env myapp

8.2. Set or Remove Variables

Set one or more variables (KEY=VALUE), or remove with -KEY:

   $ devinity env myapp DATABASE_URL=postgres://... PORT=8080
   $ devinity env myapp -OLD_KEY

8.3. Reset Overrides (Keep Added)

Reset build defaults for overridden keys while keeping any user-added keys:

   $ devinity env myapp --reset-keep-added

8.4. Reset to Defaults

   $ devinity env myapp --reset

9. App URLs

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.

9.1. Internal URLs

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)

9.2. Public URLs

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)

9.3. Branch Detection Rules

The branch is included in URLs only when needed to avoid ambiguity:

9.4. IP-Based URLs

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>/

10. Exposing Apps Publicly

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.

10.1. Prerequisites

10.2. DNS Configuration

Public 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.

10.3. Enable Public Access

   $ 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)

10.4. Disable Public Access

   $ devinity expose myapp off

10.5. View Exposure Status

Run without arguments to list the current exposure status for all apps:

   $ devinity expose

10.6. Deployment Metadata

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"}

11. Git Web Interface

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.

12. Server Configuration

12.1. View and Modify Settings

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

12.2. Maintenance Mode

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

13. Admin: Managing Users

Adding a new developer to a Devinity server is a two-step process between the developer and the administrator.

13.1. Developer: Enroll

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.

13.2. Admin: Register

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.

13.3. Resolving VPN IP Collisions

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.

13.4. Updating Server Hostname

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

13.5. Persistent App Data

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.