/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


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.

  1. SSH into a fresh Ubuntu/Debian server (user with sudo).
  2. (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)
  1. Install /dev/push:
curl -fsSL https://raw.githubusercontent.com/hunvreus/devpush/main/scripts/prod/install.sh | sudo bash
Code language: JavaScript (javascript)
  1. 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.
  1. Start services (with DB migrations):
scripts/prod/start.sh --migrate
  1. 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
  • Webhook
    • URL: https://app.example.com/api/github/webhook
    • Secret: set and copy into your .env
  • 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 (as devpush 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.

Scroll to Top