Skip to main content

Deploy web applications to remote servers.

Project description

AppGarden

Deploy web applications to remote servers.

AppGarden manages a "garden" of web applications on remote servers. It handles deployment, routing, TLS certificates, and lifecycle management — all without requiring a persistent daemon on the server. State is encoded in the filesystem and configuration files on the remote, and all operations are performed over SSH using pyinfra.

Installation

pip install appgarden

Or with uv:

uv pip install appgarden

Quick Start

1. Add a server

appgarden server add myserver \
  --host 203.0.113.10 \
  --ssh-user root \
  --ssh-key ~/.ssh/id_rsa \
  --domain apps.example.com

Or with Hetzner Cloud:

appgarden server add myserver \
  --hcloud-name main \
  --hcloud-context my-project \
  --ssh-user root \
  --ssh-key ~/.ssh/hcloud \
  --domain apps.example.com

2. Initialize the server

appgarden server init myserver

This installs Docker, Caddy, configures UFW, SSH hardening, fail2ban, and sets up the AppGarden directory structure.

3. Deploy an app

# Static site
appgarden deploy mysite \
  --method static \
  --source ./dist/ \
  --url mysite.apps.example.com

# Docker app (auto-detects runtime)
appgarden deploy myapp \
  --method auto \
  --source ./my-project/ \
  --cmd "npm start" \
  --url myapp.apps.example.com

# Dockerfile
appgarden deploy myapp \
  --method dockerfile \
  --source ./my-project/ \
  --container-port 8080 \
  --url myapp.apps.example.com

# Bare command
appgarden deploy myapi \
  --method command \
  --cmd "python app.py" \
  --source ./api/ \
  --url myapi.apps.example.com

4. Manage apps

appgarden apps list
appgarden apps status myapp
appgarden apps logs myapp -n 100
appgarden apps restart myapp
appgarden apps redeploy myapp
appgarden apps remove myapp

DNS Setup

For subdomain-based routing (recommended), configure a wildcard DNS record:

*.apps.example.com.    A    <server-ip>
apps.example.com.      A    <server-ip>

Porkbun

  1. Go to Domain Management > DNS Records
  2. Add record: Type A, Host *.apps, Answer <server-ip>
  3. Add record: Type A, Host apps, Answer <server-ip>

Cloudflare

  1. Go to DNS > Records
  2. Add record: Type A, Name *.apps, IPv4 <server-ip>, Proxy status: DNS only
  3. Add record: Type A, Name apps, IPv4 <server-ip>

Important: Cloudflare's proxy (orange cloud) only supports wildcard DNS on Enterprise plans. Use "DNS only" (grey cloud) for the wildcard record.

Namecheap

  1. Go to Advanced DNS
  2. Add record: Type A, Host *.apps, Value <server-ip>
  3. Add record: Type A, Host apps, Value <server-ip>

Any new subdomain deployed by AppGarden will automatically resolve to your server, and Caddy will obtain TLS certificates on demand.

Deployment Methods

Method Use case How it works
static Static sites, SPAs Uploads files, Caddy serves them directly
command Any process Runs a command via systemd, Caddy reverse-proxies
dockerfile Docker apps Builds image, generates docker-compose, runs via systemd
docker-compose Multi-container apps Uses your docker-compose.yml, runs via systemd
auto Auto-detect runtime Detects Node.js/Python/Go/Ruby/Rust, generates Dockerfile

Static Sites

appgarden deploy docs --method static --source ./site/ --url docs.apps.example.com

Supports git sources too:

appgarden deploy docs --method static \
  --source https://github.com/user/site.git \
  --branch gh-pages \
  --url docs.apps.example.com

Docker Apps

From a Dockerfile:

appgarden deploy webapp --method dockerfile \
  --source ./app/ \
  --container-port 8080 \
  --url webapp.apps.example.com \
  --env SECRET_KEY=abc123

From an existing docker-compose.yml:

appgarden deploy stack --method docker-compose \
  --source ./project/ \
  --url stack.apps.example.com

Auto-Docker

Auto-detects runtime from project files (package.json, requirements.txt, go.mod, etc.), generates a Dockerfile, builds and deploys:

appgarden deploy myapp --method auto \
  --source ./project/ \
  --cmd "node server.js" \
  --url myapp.apps.example.com

Supported runtimes: Node.js, Python (pip), Python (pyproject.toml), Go, Ruby, Rust.

Command

Run any process directly via systemd (no Docker):

appgarden deploy api --method command \
  --cmd "python -m uvicorn main:app --host 0.0.0.0 --port \$PORT" \
  --source ./api/ \
  --url api.apps.example.com

The PORT environment variable is automatically set to the allocated port.

Environment Variables

Pass environment variables with --env (repeatable) or --env-file:

appgarden deploy myapp --method dockerfile \
  --source . \
  --url myapp.apps.example.com \
  --env DATABASE_URL=postgres://... \
  --env SECRET_KEY=abc123

# Or from a file
appgarden deploy myapp --method dockerfile \
  --source . \
  --url myapp.apps.example.com \
  --env-file .env.production

Environment files are stored on the server with 600 permissions (readable only by root).

Environments (appgarden.toml)

For projects with multiple deployment targets, create an appgarden.toml in your project root:

[app]
name = "mywebsite"
method = "dockerfile"
container_port = 3000
source = "."

[environments.production]
server = "myserver"
url = "mywebsite.apps.example.com"
branch = "main"
env = { NODE_ENV = "production" }

[environments.staging]
server = "myserver"
url = "mywebsite-staging.apps.example.com"
branch = "staging"
env = { NODE_ENV = "staging" }

Then deploy by environment name:

# Deploy production
appgarden deploy production

# Deploy staging
appgarden deploy staging

# Deploy all environments
appgarden deploy --all-envs

App names are derived automatically: mywebsite for production, mywebsite-staging for staging.

Subdirectory Routing

Apps can be deployed to subdirectories instead of subdomains:

appgarden deploy docs --method static \
  --source ./docs/ \
  --url apps.example.com/docs

appgarden deploy api --method command \
  --cmd "python app.py" \
  --source ./api/ \
  --url apps.example.com/api

Multiple subdirectory apps on the same domain share a single Caddy config file. The X-Forwarded-Prefix header is set so your app knows its base path.

Framework-specific BASE_URL settings:

Framework Setting
Next.js basePath: "/docs" in next.config.js
React Router <BrowserRouter basename="/docs">
Flask APPLICATION_ROOT = "/docs"
FastAPI app = FastAPI(root_path="/docs")
Django FORCE_SCRIPT_NAME = "/docs"

Localhost Tunneling

Expose a locally running app through your server with HTTPS:

# Start a tunnel (blocks until Ctrl+C)
appgarden tunnel open 3000 --url myapp.apps.example.com

# List active tunnels
appgarden tunnel list

# Close a specific tunnel
appgarden tunnel close tunnel-abc12345

# Clean up stale tunnels
appgarden tunnel cleanup

This opens an SSH reverse tunnel and configures Caddy as a reverse proxy with automatic HTTPS. Useful for demos, webhook testing, or sharing work-in-progress.

CLI Reference

Global Flags

--verbose, -v    Show detailed output
--quiet, -q      Suppress non-essential output
--version        Show version and exit

Server Management

appgarden server add <name> --host <ip> --domain <domain> [--ssh-user root] [--ssh-key ~/.ssh/id_rsa]
appgarden server add <name> --hcloud-name <name> --hcloud-context <ctx> --domain <domain>
appgarden server list
appgarden server remove <name>
appgarden server default <name>
appgarden server init [name]
appgarden server ping [name]

Deployment

appgarden deploy <name> --method <method> --url <url> [options]
appgarden deploy <env-name>           # From appgarden.toml
appgarden deploy --all-envs           # All environments

App Lifecycle

appgarden apps list [-s server]
appgarden apps status <name> [-s server]
appgarden apps start <name>
appgarden apps stop <name>
appgarden apps restart <name>
appgarden apps logs <name> [-n 50]
appgarden apps remove <name> [--keep-data] [--yes]
appgarden apps redeploy <name>

Tunnels

appgarden tunnel open <local-port> --url <url> [-s server]
appgarden tunnel list [-s server]
appgarden tunnel close <tunnel-id> [-s server]
appgarden tunnel cleanup [-s server]

Configuration

appgarden config show
appgarden version

Architecture

  • Agentless: No daemon on the server. All operations run locally via pyinfra over SSH.
  • Remote state: App registry stored on server at /srv/appgarden/garden.json.
  • Caddy: Each app gets a .caddy config file; Caddy obtains TLS certificates automatically.
  • Port allocation: Starting from port 10000, managed via /srv/appgarden/ports.json.
  • Systemd: Non-static apps run as systemd services for automatic restarts and log management.

Non-Root User Setup

By default, the quick start examples use root as the SSH user. For production servers, it's recommended to create a dedicated deploy user with restricted privileges. AppGarden includes a privileged wrapper script that limits sudo access to only the operations needed for deployment.

1. Create a deploy user on the server

SSH into your server as root and create a user:

useradd -m -s /bin/bash appgarden-deploy

2. Set up SSH key authentication

Copy your SSH public key to the new user:

# From your local machine
ssh-copy-id -i ~/.ssh/id_rsa appgarden-deploy@<server-ip>

Or manually:

# On the server as root
mkdir -p /home/appgarden-deploy/.ssh
cp ~/.ssh/authorized_keys /home/appgarden-deploy/.ssh/authorized_keys
chown -R appgarden-deploy:appgarden-deploy /home/appgarden-deploy/.ssh
chmod 700 /home/appgarden-deploy/.ssh
chmod 600 /home/appgarden-deploy/.ssh/authorized_keys

3. Add the server with the appgarden-deploy user

appgarden server add myserver \
  --host <server-ip> \
  --ssh-user appgarden-deploy \
  --ssh-key ~/.ssh/id_rsa \
  --domain apps.example.com

4. Initialize the server (as root)

Server init requires full sudo access, so run it once as root (or a user with NOPASSWD: ALL sudoers access):

# Temporarily add the server with root access for init
appgarden server add myserver-init \
  --host <server-ip> \
  --ssh-user root \
  --ssh-key ~/.ssh/id_rsa \
  --domain apps.example.com

appgarden server init myserver-init --include group
appgarden server remove myserver-init

This installs Docker, Caddy, creates the appgarden group, installs the privileged wrapper script at /usr/local/bin/appgarden-privileged, and configures a sudoers entry that grants the appgarden group passwordless sudo for only that wrapper.

5. Add the appgarden-deploy user to the appgarden group

On the server as root:

usermod -aG appgarden,docker appgarden-deploy

The user needs to log out and back in for the group membership to take effect. The docker group is needed for Docker-based deployment methods.

6. Deploy as the non-root user

All subsequent deploys use the restricted appgarden-deploy user:

appgarden deploy myapp \
  --method dockerfile \
  --source ./app/ \
  --url myapp.apps.example.com

How it works

The privileged wrapper (appgarden-privileged) only allows:

  • systemctl operations on appgarden-*.service units (start, stop, restart, enable, disable, is-active, daemon-reload)
  • systemctl reload caddy
  • Installing/removing systemd unit files matching appgarden-*.service
  • journalctl for appgarden-*.service units

All inputs are validated against strict patterns — no shell interpretation, no path traversal, no access to non-appgarden services. Root users bypass the wrapper entirely and execute commands directly.

Adding more deploy users

# On the server as root
useradd -m -s /bin/bash newuser
usermod -aG appgarden newuser
usermod -aG docker newuser  # if deploying Docker apps

Security

  • SSH key-only authentication, hardened sshd config
  • UFW firewall: default deny, allow SSH/HTTP/HTTPS
  • Fail2ban for SSH brute-force protection
  • Automatic security updates via unattended-upgrades
  • Privileged wrapper restricts non-root users to appgarden-scoped operations only
  • Environment files stored with 600 permissions
  • Docker isolation for container-based apps
  • TLS via Caddy's automatic HTTPS (HTTP-01 challenge)

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

appgarden-0.1.3.tar.gz (207.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

appgarden-0.1.3-py3-none-any.whl (43.0 kB view details)

Uploaded Python 3

File details

Details for the file appgarden-0.1.3.tar.gz.

File metadata

  • Download URL: appgarden-0.1.3.tar.gz
  • Upload date:
  • Size: 207.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for appgarden-0.1.3.tar.gz
Algorithm Hash digest
SHA256 dca92cc5bbc0f787052301a48b49042b8be270b1323325906de450e17bdc5a1a
MD5 df23e6ab38aeea15733e8fd47ea5b5c2
BLAKE2b-256 090c4e91b87d944ddb876a2c0f2e51886b04dbe75864294fea34c0e5faf7d89a

See more details on using hashes here.

Provenance

The following attestation bundles were made for appgarden-0.1.3.tar.gz:

Publisher: release.yml on lukastk/appgarden

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file appgarden-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: appgarden-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 43.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for appgarden-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 e1abb8bc38f36479a0a08a630183cfe84bc22d88fef672c55c3ff2f04f895c5a
MD5 b09c328c6117e753d350fc185f1b13d4
BLAKE2b-256 5cd3560ae5e14f3f8cabcc0eb224cbe0a81ebe900b4a4968168f823204ba8d0e

See more details on using hashes here.

Provenance

The following attestation bundles were made for appgarden-0.1.3-py3-none-any.whl:

Publisher: release.yml on lukastk/appgarden

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page