An open-source and self-hostable alternative to Vercel, Render, Netlify and the likes. It allows you to build and deploy any app (Python, Node.js, PHP, ...) with zero-downtime updates, real-time logs, team management, customizable environments and domains, etc.
- Git-based deployments: Push to deploy from GitHub with zero-downtime rollouts and instant rollback.
 - Multi-language support: Python, Node.js, PHP... basically anything that can run on Docker.
 - Environment management: Multiple environments with branch mapping and encrypted environment variables.
 - Real-time monitoring: Live and searchable build and runtime logs.
 - Team collaboration: Role-based access control with team invitations and permissions.
 - Custom domains: Support for custom domain and automatic Let's Encrypt SSL certificates.
 - Self-hosted and open source: Run on your own servers, MIT licensed.
 
- User documentation: devpu.sh/docs
 - Technical documentation: ARCHITECTURE
 
⚠️ Supported on Ubuntu/Debian. Other distros may work but aren't officially supported (yet).
Log in your server, run the following command and follow instructions:
curl -fsSL https://raw.githubusercontent.com/hunvreus/devpush/main/scripts/prod/install.sh | sudo bashYou user must have sudo privileges.
You will need a fresh Ubuntu/Debian server you can SSH into with sudo privileges. We recommend a CPX31 from Hetzner.
You can use the provisioning script to get a server up and running:
- Sign in or sign up for a Hetzner account: Hetzner Cloud Console
 - Generate an API token: Creating an API token
 - Provision a server (requires 
--token; optional:--user,--name,--region,--type):Tip: runcurl -fsSL https://raw.githubusercontent.com/hunvreus/devpush/main/scripts/prod/provision-hetzner.sh | bash -s -- --token <hetzner_api_key> [--user <login_user>] [--name <hostname>] [--region <fsn1|nbg1|hel1|ash|hil|sin>] [--type <cpx11|cpx21|cpx31|cpx41|cpx51>]
curl -fsSL https://raw.githubusercontent.com/hunvreus/devpush/main/scripts/prod/provision-hetzner.sh | bash -s -- --helpto list regions and types (with specs). Defaults: regionhil, typecpx31. - Configure DNS Records: Go to your DNS provider and create two A records pointing at the server IP for 
APP_HOSTNAME(e.g.app.devpu.sh) and a wildcard on subdomains ofDEPLOY_DOMAIN(e.g.*.devpush.app). If you're using Cloudflare, set SSL/TLS to "Full (strict)" and keep the records proxied. - SSH into your new server: The provision script will have created a user for you.
ssh <login_user>@<server_ip>
 - Run hardening for system and SSH:
 
curl -fsSL https://raw.githubusercontent.com/hunvreus/devpush/main/scripts/prod/harden.sh | sudo bash -s -- --sshEven if you already have a server, we recommend you harden security (ufw, fail2ban, disabled root SSH, etc). You can do that using scripts/prod/harden.sh.
- SSH into the server:
ssh <login_user>@<server_ip>
 - Install /dev/push:
curl -fsSL https://raw.githubusercontent.com/hunvreus/devpush/main/scripts/prod/install.sh | sudo bash - Switch to 
devpushuser: 
sudo -iu devpush- Edit 
.env: 
cd devpush && vi .envTip: you will need to fill in at least the following: LE_EMAIL, APP_HOSTNAME, DEPLOY_DOMAIN, EMAIL_SENDER_ADDRESS, RESEND_API_KEY and your GitHub app settings (see [environment-variables] for details). SERVER_IP, SECRET_KEY, ENCRYPTION_KEY, POSTGRES_PASSWORD should be pre-filled. You can ignore all commented out environment variables.
5. Start services:
scripts/prod/start.sh --migrate- Visit your URL: 
https://<APP_HOSTNAME> 
The follwing commands must be run as devpush user (su - devpush).
In most cases, you can run an update with:
scripts/prod/update.sh --allAlternatively, you can force a full upgrade (with downtime) using:
scripts/prod/update.sh --full -yYou can update specific components:
scripts/prod/update.sh --components <component_name>
⚠️ Development scripts target macOS for now.
- Install Colima and the Loki Docker plugin:
scripts/dev/install.sh
 - Set up environment variables:
cp .env.dev.example .env
 - Start the stack (streams logs):
scripts/dev/start.sh
- Add 
--pruneto prune dangling images before build - Add 
--cacheto use the build cache (default is no cache) 
 - Add 
 - Initialize your database once containers are up:
scripts/dev/db-migrate.sh
 
See the scripts section for more dev utilities.
- The app is mounted inside containers, so code changes reflect immediately. Some SSE endpoints may require closing browser tabs to trigger a reload.
 - The workers require a restart:
docker-compose restart worker-arq
 - To apply migrations:
scripts/dev/db-migrate.sh
 
| Area | Script | What it does | 
|---|---|---|
| Dev | scripts/dev/install.sh | 
Setup Colima and install Loki Docker plugin | 
| Dev | scripts/dev/start.sh | 
Start stack with logs (foreground); supports --prune, --cache | 
| Dev | scripts/dev/build-runners.sh | 
Build runner images (default no cache; --cache to enable) | 
| Dev | scripts/dev/db-generate.sh | 
Generate Alembic migration (prompts for message) | 
| Dev | scripts/dev/db-migrate.sh | 
Apply Alembic migrations | 
| Dev | scripts/dev/db-reset.sh | 
Drop and recreate public schema in DB | 
| Dev | scripts/dev/clean.sh | 
Stop stack and clean dev data (--hard for global) | 
| Prod | scripts/prod/provision-hetzner.sh | 
Provision a Hetzner server (API token, regions from API, fixed sizes) | 
| Prod | scripts/prod/install.sh | 
Server setup: Docker, Loki plugin, user, clone repo, create .env | 
| Prod | scripts/prod/harden.sh | 
System hardening (UFW, fail2ban, unattended-upgrades); add --ssh to harden SSH | 
| Prod | scripts/prod/start.sh | 
Start services; optional --migrate | 
| Prod | scripts/prod/stop.sh | 
Stop services (--down for hard stop) | 
| Prod | scripts/prod/restart.sh | 
Restart services; optional --migrate | 
| Prod | scripts/prod/update.sh | 
Update by tag; --all (app+workers), --full (downtime), or --components | 
| Prod | scripts/prod/db-migrate.sh | 
Apply DB migrations in production | 
| Prod | scripts/prod/check-env.sh | 
Validate required keys exist in .env | 
| Prod | scripts/prod/update/app.sh | 
Blue‑green update for app | 
| Prod | scripts/prod/update/worker-arq.sh | 
Drain‑aware blue‑green update for worker-arq | 
| Prod | scripts/prod/update/worker-monitor.sh | 
Blue‑green update for worker-monitor | 
| Variable | Comments | Default | 
|---|---|---|
APP_NAME | 
App name. | /dev/push | 
APP_DESCRIPTION | 
App description. | Deploy your Python app without touching a server. | 
URL_SCHEME | 
http (development) or https (production). | 
https | 
LE_EMAIL | 
Email used to register the Let's Encrypt (ACME) account in Traefik; receives certificate issuance/renewal/expiry notifications. | "" | 
APP_HOSTNAME | 
Domain for the app (e.g. app.devpu.sh). | 
"" | 
DEPLOY_DOMAIN | 
Domain used for deployments (e.g. devpush.app if you want your deployments available at *.devpush.app). | 
APP_HOSTNAME | 
SERVER_IP | 
Public IP of the server | "" | 
SECRET_KEY | 
App secret for sessions/CSRF. Generate: openssl rand -hex 32 | 
"" | 
ENCRYPTION_KEY | 
Fernet key (urlsafe base64, 32 bytes). Generate: `openssl rand -base64 32 | tr '+/' '-_' | 
EMAIL_LOGO | 
URL for email logo image. Only helpful for testing, as the app will use app/logo-email.png if left empty. | 
"" | 
EMAIL_SENDER_NAME | 
Name displayed as email sender for invites/login. | "" | 
EMAIL_SENDER_ADDRESS | 
Email sender used for invites/login. | "" | 
RESEND_API_KEY | 
API key for Resend. | "" | 
GITHUB_APP_ID | 
GitHub App ID. | "" | 
GITHUB_APP_NAME | 
GitHub App name. | "" | 
GITHUB_APP_PRIVATE_KEY | 
GitHub App private key (PEM format). | "" | 
GITHUB_APP_WEBHOOK_SECRET | 
GitHub webhook secret for verifying webhook payloads. | "" | 
GITHUB_APP_CLIENT_ID | 
GitHub OAuth app client ID. | "" | 
GITHUB_APP_CLIENT_SECRET | 
GitHub OAuth app client secret. | "" | 
GOOGLE_CLIENT_ID | 
Google OAuth client ID. | "" | 
GOOGLE_CLIENT_SECRET | 
Google OAuth client secret. | "" | 
POSTGRES_DB | 
PostgreSQL database name. | devpush | 
POSTGRES_USER | 
PostgreSQL username. | devpush-app | 
POSTGRES_PASSWORD | 
PostgreSQL password. Generate: `openssl rand -base64 24 | tr -d '\n'` | 
REDIS_URL | 
Redis connection URL. | redis://redis:6379 | 
DOCKER_HOST | 
Docker daemon host address. | tcp://docker-proxy:2375 | 
UPLOAD_DIR | 
Directory for file uploads. | /app/upload | 
TRAEFIK_CONFIG_DIR | 
Traefik configuration directory. | /data/traefik | 
DEFAULT_CPU_QUOTA | 
Default CPU quota for containers (microseconds). | 100000 | 
DEFAULT_MEMORY_MB | 
Default memory limit for containers (MB). | 4096 | 
JOB_TIMEOUT | 
Job timeout in seconds. | 320 | 
JOB_COMPLETION_WAIT | 
Job completion wait time in seconds. | 300 | 
DEPLOYMENT_TIMEOUT | 
Deployment timeout in seconds. | 300 | 
LOG_LEVEL | 
Logging level. | WARNING | 
DB_ECHO | 
Enable SQL query logging. | false | 
ENV | 
Environment (development/production). | development | 
ACCESS_DENIED_MESSAGE | 
Message shown to users who are denied access based on sign-in access control. | Sign-in not allowed for this email. | 
ACCESS_DENIED_WEBHOOK | 
Optional webhook to receive denied events (read more about Sign-in access control). | "" | 
LOGIN_HEADER | 
HTML snippet displayed above the login form. | "" | 
TOASTER_HEADER | 
HTML snippet displayed at the top of the toaster (useful to display a permanent toast on all pages). | "" | 
You will need to configure a GitHub App with the following settings:
- Identifying and authorizing users:
- Callback URL: add two callback URLs with your domain:
 - Expire user authorization tokens: No
 
 - Post installation:
- Setup URL: https://example.com/api/github/install/callback
 - Redirect on update: Yes
 
 - Webhook:
- Active: Yes
 - Webhook URL: https://example.com/api/github/webhook
 
 - Permissions:
- Repository permissions
- Administration: Read and write
 - Checks: Read and write
 - Commit statuses: Read and write
 - Contents: Read and write
 - Deployments: Read and write
 - Issues: Read and write
 - Metadata: Read-only
 - Pull requests: Read and write
 - Webhook: Read and write
 
 - Account permissions:
- Email addresses: Read-only
 
 
 - Repository permissions
 - Subscribe to events:
- Installation target
 - Push
 - Repository
 
 
Provide an access rules file to restrict who can sign up/sign in.
- Development: edit 
./access.json. If missing, runningscripts/dev/start.shwill sed an allow‑all file. - Production: edit 
/srv/devpush/access.jsonon the server. 
Rules format (any/all may be used):
{
  "emails": ["alice@example.com"],
  "domains": ["example.com"],
  "globs": ["*@corp.local", "*.dept.example.com"],
  "regex": ["^[^@]+@(eng|research)\\.example\\.com$"]
}Globs use shell-style wildcards; regex are Python patterns. If the file is missing or empty, all valid emails are allowed.
Additionally, if you set the ACCESS_DENIED_WEBHOOK environment variable, denied sign-in attempts will be posted to the provided URL with the following payload:
{
  "email": "user@example.com",
  "provider": "google",
  "ip": "203.0.113.10",
  "user_agent": "Mozilla/5.0"
}