Single-Binary PaaS CLI — deploy an app from a Git repo to your own server.
SimpleDeploy is a zero-dependency PaaS tool written in Go. Give it a Git repo URL and it handles the rest: Docker setup, image build, database provisioning, reverse proxy (Traefik or Caddy), SSL certificates, and push-to-deploy via webhooks.
Status: beta. The full path — deploy → proxy → SSL → webhook → rollback — works end to end as of 0.1.1, including the opt-in Docker integration suite. It has not yet been run at scale. Back up
~/.simpledeploy/state.json; it holds your encrypted tokens and database passwords.
#NOFORKANYMORE — do what Coolify/Dokploy do, with clean minimal Go. Generate a docker-compose.yml, that's it.
- Single binary — no runtime dependencies beyond Docker and git.
- Interactive wizards —
initanddeploywalk you through everything. - Auto-detection — Node.js, Go, PHP, Python, Ruby, static sites, or your own Dockerfile.
- Database provisioning — MySQL 8, PostgreSQL 16, MariaDB 11, MongoDB 7, Redis 7, with health checks and startup ordering.
- Reverse proxy — Traefik (container auto-discovery) or Caddy.
- Let's Encrypt — automatic SSL, HTTP→HTTPS redirect.
- Push-to-deploy — GitHub/GitLab/Gitea push events trigger a rebuild, with signature verification and automatic rollback if the new image fails to start.
- Encrypted secrets — AES-256-GCM for git tokens and database passwords.
- Security headers — applied to every app by default.
- No public port surface — app containers are reachable only through the proxy.
# 1. Install (auto-detects OS/arch)
curl -fsSL https://raw.githubusercontent.com/ersinkoc/SimpleDeploy/main/install.sh | sh
sudo mv simpledeploy /usr/local/bin/
# 2. First-time setup — picks a proxy, domain, SSL email, webhook secret
simpledeploy init
# 3. Deploy
simpledeploy deploy
# 4. Turn on push-to-deploy
simpledeploy webhook startOr download a binary from Releases (Linux, macOS, Windows × amd64, arm64).
init asks about wildcard DNS. You need both records:
| Record | Points to | Used for |
|---|---|---|
*.apps.example.com |
server IP | your deployed apps |
apps.example.com |
server IP | the webhook endpoint (/_qd/*) |
The second one is easy to forget and is why a webhook can return a TLS error while the apps themselves work fine.
simpledeploy init # First-time setup (interactive wizard)
simpledeploy status # Proxy + application status
simpledeploy deploy # Deploy a new application (interactive)
simpledeploy list # List deployed applications
simpledeploy redeploy <app> # Rebuild and restart an application
simpledeploy restart <app> # Restart an application's container
simpledeploy stop <app> # Stop an application
simpledeploy remove <app> # Remove an application and its files
simpledeploy logs <app> [-f] # Show logs (-f to follow)
simpledeploy exec <app> <cmd> # Run a command inside the app container
simpledeploy webhook start # Run the push-to-deploy listener
simpledeploy service install # Generate compose to run the listener as a container
simpledeploy service start|stop # Start/stop that container
simpledeploy version # Show versioninit— checks Docker and the Compose plugin, starts Traefik or Caddy on thesimpledeploynetwork, requests Let's Encrypt certs, and publishes the webhook endpoint athttps://<base-domain>/_qd/.deploy— clones the repo, detects the framework, generates a Dockerfile +.dockerignoreif the repo has none, builds an image, writes adocker-compose.yml, starts the container behind the proxy, and polls until it reportsrunning.- Webhook — verifies the push signature, pulls the latest commit, rebuilds, restarts, and rolls back to the previous image if the new container fails to come up — including a container that crash-loops under its
restart: unless-stoppedpolicy rather than exiting outright.
A failed or cancelled deploy removes the app directory it created, so nothing is left holding generated database credentials.
init publishes the route; the listener has to be running for it to answer.
simpledeploy webhook start # foreground — run it under systemd for real use
# or
simpledeploy service install
simpledeploy service start # runs the listener as a containerThen add a webhook to your repository:
| Field | Value |
|---|---|
| Payload URL | https://<base-domain>/_qd/webhook/<app-name> |
| Content type | application/json |
| Secret | the webhook secret from init |
| Events | push only |
simpledeploy deploy prints all of this, filled in, after a successful deploy.
Only pushes to the app's configured branch trigger a deploy. Requests without a valid signature are rejected, and the server refuses to deploy at all when no secret is configured — it will not fall back to trusting anonymous requests.
Supported providers: GitHub and Gitea (HMAC-SHA256), GitLab (constant-time token compare).
simpledeploy (single binary)
├── main.go
└── internal/
├── cli/ → command handlers
├── wizard/ → interactive prompts & ANSI colors
├── git/ → clone (shallow) / fetch + hard reset
├── docker/ → install check, build, compose, image pruning
├── compose/ → docker-compose.yml generator (deterministic output)
├── proxy/ → Traefik / Caddy setup + webhook routing
├── webhook/ → HTTP listener (GitHub/GitLab/Gitea)
├── db/ → database provisioning
├── state/ → JSON state + AES-256-GCM + input validation
├── buildpack/ → framework detection & Dockerfile generation
├── config/ → path resolution
└── runner/ → self-containerization
Dockerfile templates are Go constants in internal/buildpack/detect.go. There is no external template directory to keep in sync.
~/.simpledeploy/
└── state.json → apps + global config (AES-encrypted secrets)
/opt/simpledeploy/
├── proxy/
│ ├── docker-compose.yml → Traefik/Caddy
│ ├── Caddyfile → (Caddy only) per-app routes + /_qd/*
│ └── dynamic/webhook.yml → (Traefik only) /_qd/* route
├── apps/
│ └── <app-name>/
│ ├── source/ → git clone
│ ├── docker-compose.yml → generated compose (mode 0600)
│ ├── .env → environment variables (mode 0600)
│ └── deploy.log → deploy history
└── service/
└── docker-compose.yml → SimpleDeploy's own compose
| Variable | Default | Purpose |
|---|---|---|
SIMPLEDEPLOY_DIR |
/opt/simpledeploy |
Root for proxy, app, and service data |
SIMPLEDEPLOY_STATE_DIR |
~/.simpledeploy |
Where state.json lives |
They are separate because the containerised service runs with $HOME=/root; the generated compose bind-mounts both host paths and sets both variables.
Detection runs top to bottom; a committed Dockerfile always wins.
| Type | Detection | Default Port |
|---|---|---|
| Docker | Dockerfile |
3000 |
| Node.js | package.json |
3000 |
| Go | go.mod |
8080 |
| Python | requirements.txt / pyproject.toml |
8000 |
| PHP | composer.json / *.php |
80 |
| Ruby | Gemfile |
3000 |
| Static | *.html |
80 |
Generated Dockerfiles are conservative starting points: they install dependencies, run a build script only if one exists, and fall back to conventional entrypoints. If your app needs anything specific, commit your own Dockerfile — it is always preferred over the template.
| Type | Image | Injected env |
|---|---|---|
| MySQL | mysql:8 |
DATABASE_URL, MYSQL_URL |
| PostgreSQL | postgres:16 |
DATABASE_URL, POSTGRESQL_URL |
| MariaDB | mariadb:11 |
DATABASE_URL, MARIADB_URL |
| MongoDB | mongo:7 |
MONGODB_URI |
| Redis | redis:7-alpine |
REDIS_URL |
DATABASE_URL points at the first SQL database you select. Passwords are generated per app, stored encrypted in state, and written into the app's compose file (mode 0600).
deploy can import an existing .env. Its contents are preserved verbatim and the variables SimpleDeploy generates are appended after it, so a stale DATABASE_URL in your file cannot shadow the connection string for the database just provisioned.
Webhook returns 404 — the proxy route is missing. Re-run simpledeploy init (it rewrites the proxy config) and check that apps.example.com has an A record, not just the wildcard.
Webhook returns 502 — the route exists but nothing is listening. Start the listener: simpledeploy webhook start.
Webhook returns 401 — signature mismatch. Confirm the secret in your repository settings matches the one in ~/.simpledeploy/state.json, and that the payload content type is application/json.
simpledeploy list shows error — the container is not running. simpledeploy logs <app> shows why. After a failed redeploy the previous image has already been restored automatically.
Build fails on a generated Dockerfile — the template did not fit your project. Commit your own Dockerfile; it takes precedence and the template is not used again.
docker: 'compose' is not a docker command — install the Compose plugin (apt-get install docker-compose-plugin). init checks for this up front as of 0.1.0.
git clone https://github.com/ersinkoc/SimpleDeploy.git
cd SimpleDeploy
make build # or: CGO_ENABLED=0 go build -o simpledeploy .
make test # unit tests
make lint # go vet ./...
make release # cross-compile all platforms into dist/Tests that drive a real Docker daemon — building images, starting containers,
and (for the proxy) binding ports 80/443 — are opt-in, because they cannot run
on an unprivileged CI runner. make test skips them. To run everything:
make test-integration # SIMPLEDEPLOY_INTEGRATION=1 go test -p=1 -count=1 ./...The full opt-in suite passed for v0.1.1 and also runs weekly and on demand via the Integration Tests workflow.
Version is stamped at link time from the Makefile, so simpledeploy version always matches the build.
docker build -t simpledeploy:latest .- Linux server (Ubuntu/Debian/CentOS/Fedora)
- Docker Engine and the Docker Compose plugin —
initverifies both; the engine can be auto-installed - Go 1.23+ to build from source
- Git tokens and DB passwords encrypted with AES-256-GCM; any crypto failure aborts the operation rather than falling back to plaintext.
- Encryption key derived from the machine ID, mounted read-only into the service container so both modes agree.
- Webhook verification: HMAC-SHA256 (GitHub, Gitea), constant-time token compare (GitLab). Deploys are refused entirely when no secret is configured.
- Per-IP fixed-window rate limiting on the webhook endpoint, IPv6-safe.
- Git credentials passed via
GIT_ASKPASS+ environment, never on the command line; redacted from all error output. - App containers use
expose, notports— not reachable except through the proxy. - Input validation at every boundary that reaches a config file: domains, emails, header names and values, env keys, image tags, volume names, container paths.
.envand generated compose files are mode 0600; generated.dockerignorekeeps.gitout of built images.- Proxy config is written in place so the container's single-file bind mount stays valid; Caddy validates the config on reload and keeps the previous one if the new file is malformed.
See CHANGELOG.md. Latest release is v0.1.1; 0.1.0 remains the main audit release and is worth reading before upgrading from 0.0.x, as several defaults changed.
simpledeploy init # rewrites the proxy config: file provider, extra_hosts, webhook routeExisting apps keep running. Redeploy each app (simpledeploy redeploy <app>) to regenerate its compose file with expose instead of published host ports.
MIT — see LICENSE.
