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:
You 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):
curl -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>]Tip: run curl -fsSL https://raw.githubusercontent.com/hunvreus/devpush/main/scripts/prod/provision-hetzner.sh | bash -s -- --help to list regions and types (with specs). Defaults: region hil, type cpx31.
- 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 of DEPLOY_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:
Even 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 devpush user:
- Edit .env:
Tip: 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:
- 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:
Alternatively, you can force a full upgrade (with downtime) using:
You can update specific components:
⚠️ Development scripts target macOS for now.
- Install Colima and the Loki Docker plugin:
- Set up environment variables:
- Start the stack (streams logs):
- Add --prune to prune dangling images before build
- Add --cache to use the build cache (default is no cache)
- 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
| 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 |
| 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, running scripts/dev/start.sh will sed an allow‑all file.
- Production: edit /srv/devpush/access.json on the server.
Rules format (any/all may be used):
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:
.png)


