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
- Go to Domain Management > DNS Records
- Add record: Type
A, Host*.apps, Answer<server-ip> - Add record: Type
A, Hostapps, Answer<server-ip>
Cloudflare
- Go to DNS > Records
- Add record: Type
A, Name*.apps, IPv4<server-ip>, Proxy status: DNS only - Add record: Type
A, Nameapps, 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
- Go to Advanced DNS
- Add record: Type
A, Host*.apps, Value<server-ip> - Add record: Type
A, Hostapps, 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
.caddyconfig 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file appgarden-0.1.2.tar.gz.
File metadata
- Download URL: appgarden-0.1.2.tar.gz
- Upload date:
- Size: 208.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc419722b7aff02670fba9d3e34fa52846c537c1009eab01fd1a94a4cb5b1888
|
|
| MD5 |
a8d938104356ec19035c1b727cd18001
|
|
| BLAKE2b-256 |
3b1d9752b267fd4548d964f413550bd67d2bc2b449d3c18d9140348b6551dbe8
|
Provenance
The following attestation bundles were made for appgarden-0.1.2.tar.gz:
Publisher:
release.yml on lukastk/appgarden
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
appgarden-0.1.2.tar.gz -
Subject digest:
bc419722b7aff02670fba9d3e34fa52846c537c1009eab01fd1a94a4cb5b1888 - Sigstore transparency entry: 954781520
- Sigstore integration time:
-
Permalink:
lukastk/appgarden@808a75e7c012c3a5a8a6210d708b4ae27083e4a5 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/lukastk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@808a75e7c012c3a5a8a6210d708b4ae27083e4a5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file appgarden-0.1.2-py3-none-any.whl.
File metadata
- Download URL: appgarden-0.1.2-py3-none-any.whl
- Upload date:
- Size: 38.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
054243111a389aa934824fcd5876bbf95dabf322d16d04bf82b49aad23e79039
|
|
| MD5 |
67728b29b88aceed57245739fab082c7
|
|
| BLAKE2b-256 |
97e3c7804e4bc9987130169191c94ea11a6bdf683e4ade1206998744627f4286
|
Provenance
The following attestation bundles were made for appgarden-0.1.2-py3-none-any.whl:
Publisher:
release.yml on lukastk/appgarden
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
appgarden-0.1.2-py3-none-any.whl -
Subject digest:
054243111a389aa934824fcd5876bbf95dabf322d16d04bf82b49aad23e79039 - Sigstore transparency entry: 954781593
- Sigstore integration time:
-
Permalink:
lukastk/appgarden@808a75e7c012c3a5a8a6210d708b4ae27083e4a5 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/lukastk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@808a75e7c012c3a5a8a6210d708b4ae27083e4a5 -
Trigger Event:
push
-
Statement type: