/dev/push is a lightweight, self-hostable alternative to Vercel/Render/Netlify. It focuses on the essentials—push-to-deploy, zero-downtime rollouts, instant rollbacks, team roles, encrypted env vars, custom domains with automatic TLS, and live logs—while staying language-agnostic by running apps in Docker.
Key features
- Git-based deployments from GitHub (push to deploy, rollbacks).
- Multi-language via Docker runners (Python, Node.js now; PHP & RoR on the roadmap).
- Environments & branch mapping with encrypted environment variables.
- Real-time logs (build + runtime), searchable.
- RBAC & teams (invites, permissions).
- Custom domains + auto Let’s Encrypt certificates.
- Self-hosted, MIT-licensed.
Stack highlights
- FastAPI app + workers (arq), PostgreSQL, Redis, Traefik (reverse proxy / TLS), Loki (logs), Docker & Compose.
- Blue/green style updates for app/workers; zero-downtime where possible.
Links
- GitHub: https://github.com/hunvreus/devpush
- Docs: https://devpu.sh/docs
- Website: https://devpu.sh
- Hosted preview (maintainer’s): https://app.devpu.sh
Quick deploy on Linux (Ubuntu/Debian) with Docker
You’ve got two good options:
Option A — One-liner production installer (fastest path)
This is the project’s supported route. It installs Docker, sets up the system, checks prerequisites, pulls containers, wires Traefik, and guides you through .env
.
- SSH into a fresh Ubuntu/Debian server (user with
sudo
). - (Optional) Use the hardening script (UFW, fail2ban, SSH, etc.):
curl -fsSL https://raw.githubusercontent.com/hunvreus/devpush/main/scripts/prod/harden.sh | sudo bash -s -- --ssh
Code language: JavaScript (javascript)
- Install /dev/push:
curl -fsSL https://raw.githubusercontent.com/hunvreus/devpush/main/scripts/prod/install.sh | sudo bash
Code language: JavaScript (javascript)
- Switch to the service user and fill
.env
:
sudo -iu devpush
cd devpush && vi .env
Code language: CSS (css)
At minimum set:
LE_EMAIL
– email for Let’s Encrypt notices.APP_HOSTNAME
– domain for the control plane (e.g.,app.example.com
).DEPLOY_DOMAIN
– base domain your apps will be deployed under (e.g.,example.app
so apps live at*.example.app
).EMAIL_SENDER_ADDRESS
&RESEND_API_KEY
– for invites/login emails.- GitHub App settings (see “GitHub App config” below).
Create two DNS A records pointing to your server’s IP:
APP_HOSTNAME
(e.g.,app.example.com
)*.DEPLOY_DOMAIN
(wildcard, e.g.,*.example.app
)
If you use Cloudflare, set SSL/TLS Full (strict) and keep records proxied.
- Start services (with DB migrations):
scripts/prod/start.sh --migrate
- Visit https://APP_HOSTNAME and finish setup.
Updates
# As user `devpush`
scripts/prod/update.sh --all
# Or full (with downtime):
scripts/prod/update.sh --full -y
Code language: PHP (php)
Option B — Manual Docker Compose (transparent & tweakable)
If you prefer to see how things are wired, here’s a minimal Compose setup for a single-node server using Traefik + Let’s Encrypt. (This mirrors the project’s architecture but keeps configs in one place.)
1) Install Docker Engine + Compose
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# log out/in to apply group membership
docker --version && docker compose version
Code language: PHP (php)
2) Prepare DNS
APP_HOSTNAME
→ your server IP (A record), e.g.,app.example.com
*.DEPLOY_DOMAIN
→ the same IP (wildcard A record), e.g.,*.example.app
3) Create a working dir and .env
mkdir -p ~/devpush && cd ~/devpush
cat > .env << 'EOF'
# --- Core ---
APP_NAME=/dev/push
URL_SCHEME=https
[email protected]
APP_HOSTNAME=app.example.com
DEPLOY_DOMAIN=example.app
# --- Secrets (generate fresh values!) ---
SECRET_KEY=$(openssl rand -hex 32)
ENCRYPTION_KEY=$(openssl rand -base64 32 | tr '+/' '-_')
POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '\n')
# --- Email (Resend) ---
EMAIL_SENDER_NAME=/dev/push
[email protected]
RESEND_API_KEY=YOUR_RESEND_API_KEY
# --- GitHub App (fill from your GitHub app) ---
GITHUB_APP_ID=
GITHUB_APP_NAME=
GITHUB_APP_PRIVATE_KEY=
GITHUB_APP_WEBHOOK_SECRET=
GITHUB_APP_CLIENT_ID=
GITHUB_APP_CLIENT_SECRET=
EOF
Code language: PHP (php)
4) docker-compose.yml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: devpush
POSTGRES_USER: devpush-app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devpush-app -d devpush"]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
traefik:
image: traefik:v3.1
command:
- --api.dashboard=false
- --providers.docker=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
- --certificatesresolvers.le.acme.email=${LE_EMAIL}
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
ports:
- "80:80"
- "443:443"
volumes:
- traefik_data:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
loki:
image: grafana/loki:2.9.8
command: ["-config.file=/etc/loki/local-config.yaml"]
volumes:
- loki_data:/loki
# You can mount a config if you want; minimal setup works for /dev/push defaults.
app:
image: ghcr.io/hunvreus/devpush:latest
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
traefik:
condition: service_started
environment:
URL_SCHEME: ${URL_SCHEME}
APP_HOSTNAME: ${APP_HOSTNAME}
DEPLOY_DOMAIN: ${DEPLOY_DOMAIN}
SECRET_KEY: ${SECRET_KEY}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
POSTGRES_DB: devpush
POSTGRES_USER: devpush-app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
REDIS_URL: redis://redis:6379
# Email
EMAIL_SENDER_NAME: ${EMAIL_SENDER_NAME}
EMAIL_SENDER_ADDRESS: ${EMAIL_SENDER_ADDRESS}
RESEND_API_KEY: ${RESEND_API_KEY}
# GitHub app
GITHUB_APP_ID: ${GITHUB_APP_ID}
GITHUB_APP_NAME: ${GITHUB_APP_NAME}
GITHUB_APP_PRIVATE_KEY: ${GITHUB_APP_PRIVATE_KEY}
GITHUB_APP_WEBHOOK_SECRET: ${GITHUB_APP_WEBHOOK_SECRET}
GITHUB_APP_CLIENT_ID: ${GITHUB_APP_CLIENT_ID}
GITHUB_APP_CLIENT_SECRET: ${GITHUB_APP_CLIENT_SECRET}
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`${APP_HOSTNAME}`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=le"
- "traefik.http.middlewares.app-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.app-redirect.rule=Host(`${APP_HOSTNAME}`)"
- "traefik.http.routers.app-redirect.entrypoints=web"
- "traefik.http.routers.app-redirect.middlewares=app-https-redirect"
# Wildcard for deployed apps:
- "traefik.http.routers.deploy.rule=HostRegexp(`{subdomain:.+}.${DEPLOY_DOMAIN}`)"
- "traefik.http.routers.deploy.entrypoints=websecure"
- "traefik.http.routers.deploy.tls.certresolver=le"
volumes:
- app_data:/srv/devpush
restart: unless-stopped
volumes:
pg_data:
loki_data:
traefik_data:
app_data:
Code language: PHP (php)
This keeps Traefik terminating TLS for both the control plane (
APP_HOSTNAME
) and all deployed apps (*.DEPLOY_DOMAIN
). /dev/push will attach Traefik labels to runner containers dynamically during deployments.
5) Bring it up
docker compose up -d
docker compose ps
Visit https://APP_HOSTNAME. On first launch, /dev/push will guide you through basic configuration.
Configure the GitHub App (required for push-to-deploy)
Set these URLs using your APP_HOSTNAME
(replace example.com
):
- OAuth / callback URLs
https://app.example.com/api/github/authorize/callback
https://app.example.com/auth/github/callback
- Post-installation
- Setup URL:
https://app.example.com/api/github/install/callback
- Redirect on update: Yes
- Setup URL:
- Webhook
- URL:
https://app.example.com/api/github/webhook
- Secret: set and copy into your
.env
- URL:
- Permissions (Repository)
- Administration, Checks, Commit statuses, Contents, Deployments, Issues, Pull requests → Read & write
- Metadata → Read-only
- Webhook → Read & write
- Permissions (Account): Email addresses → Read-only
- Events: Installation target, Push, Repository
Then paste App ID, Client ID/Secret, Webhook secret and private key (PEM) into .env
.
Access control (who can sign in)
Create /srv/devpush/access.json
(prod) to restrict sign-in:
{
"emails": ["[email protected]"],
"domains": ["example.com"],
"globs": ["*@corp.local", "*.dept.example.com"],
"regex": ["^[^@]+@(eng|research)\\.example\\.com$"]
}
Code language: JSON / JSON with Comments (json)
If the file is missing/empty, all valid emails can sign in.
Day-2 ops
Update
- Installer path:
scripts/prod/update.sh --all
(asdevpush
user). - Manual path:
docker compose pull && docker compose up -d
.
Backups
- PostgreSQL: dump regularly (
pg_dump
) and keep off-box. - Save
.env
and any Traefik ACME state (volume).
Hardening
- Only expose 80/443; keep Docker socket root-only.
- Use strong
SECRET_KEY
/ENCRYPTION_KEY
and rotate if needed. - Set Cloudflare to Full (strict) if proxied; ensure DNS and TLS are consistent.
Monitoring
- Logs stream from Loki; view in the web UI and wire it to your observability stack if needed.
What you can deploy
Anything that can be containerized. /dev/push uses language-specific runner images (Python, Node.js today; PHP/RoR coming), but you can also point to custom Dockerfiles once supported in your version. Branch → environment mapping lets you keep main → production and develop → staging on autopilot.