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.

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
  • 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.1.tar.gz (202.3 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.1-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: appgarden-0.1.1.tar.gz
  • Upload date:
  • Size: 202.3 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.1.tar.gz
Algorithm Hash digest
SHA256 2ac44166f7a7a3eafab9d2956709f4c2785212e61b6bebd9597bbdc5996426fc
MD5 fd51c8e333b262ac3111e1cb6d95bce7
BLAKE2b-256 56f0323783754c0dd6ca658eb1ef344e441497dfa38a9a64f2985df0a6d62332

See more details on using hashes here.

Provenance

The following attestation bundles were made for appgarden-0.1.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: appgarden-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 36.8 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c039e074763f1fd0b8d568b5fea659ae8f0264fd3ea692d452cc528251929b61
MD5 328382b1e312a30e664e41a731d68486
BLAKE2b-256 afce5c3af80f7ac76f4dbef84c5ebcf884dafa505e1b989c952ee80efedb0e82

See more details on using hashes here.

Provenance

The following attestation bundles were made for appgarden-0.1.1-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