Documentation

Deploy, configure and scale on DeployCloud

Everything you need to take an app from a Git repository to a live, health-checked HTTPS URL on infrastructure you own.

On this page

Introduction

DeployCloud is a self-hosted platform-as-a-service — your own Heroku or Vercel, running on servers you control instead of someone else’s cloud. You point it at a Git repository and it clones the code, builds an image (from your Dockerfile, or automatically when there isn’t one), and releases it behind a Traefik reverse proxy.

A new release only takes traffic once it passes a health check, so every deploy is zero-downtime and a broken build can never take your site down. Each app is served on its own subdomain with automatic HTTPS, and the entire platform — dashboard, deploy worker, database and proxy — runs on a single Linux box with Docker.

Getting started

Create an app. In the dashboard choose New App, give it a name and paste the URL of the Git repository you want to deploy, then pick the branch to track (defaults to main). That’s the whole setup — there’s nothing to install in your repo.

Deploy it. Trigger the first build from the dashboard’s Deploy button, from the CLI, or by POSTing to the app’s deploy hook — handy from CI:

curl -X POST https://deploycloud.example.com/api/hooks/deploy/<deploy-token>

DeployCloud clones the branch, builds the image, runs any release step, and rolls the new version in behind the proxy once it’s healthy.

It goes live. Your app is served at <your-app>.<your-domain> with a Let’s Encrypt certificate issued automatically — for example myapp.deploycloud.example.com. Attach custom domains any time, and scale it out with one click.

How builds work

DeployCloud builds your repository one of two ways, and it decides for you based on what’s in the build root:

  • Dockerfile present. If your repo has a Dockerfile, it’s built exactly as written — you have full control over the image.
  • No Dockerfile. Otherwise the language is auto-detected and an image is produced for you with Nixpacks — no configuration required. Node, Python, Go, Ruby, PHP, Rust, Java and more are supported out of the box.
Your web process must listen on the port given in the $PORT environment variable, not a hardcoded one. DeployCloud injects PORT and health-checks exactly that port — binding elsewhere means the health check never passes and the deploy rolls back.

Procfile & processes

Most real apps are more than one process: a web server, a background worker, and a migration step that has to run before the new code goes live. A Procfile at your build root declares all three, one process per line, in the same format across every language:

web: node server.js
release: npx prisma migrate deploy
worker: node worker.js

DeployCloud recognises exactly three behaviours, keyed off the process name:

  • web — the one routed, health-checked process that gets a URL. It’s what your replica count scales, and it must listen on $PORT. A repo with no Procfile still gets a single web process from the image’s CMD.
  • release — a one-shot command run to completion before traffic shifts, against the freshly built image with real env vars. This is where database migrations go. A non-zero exit aborts the deploy — the previous release keeps serving, so a broken migration can’t take you down. Its output streams into the deploy logs.
  • Any other name (worker, clock, scheduler…) — a long-running background worker. It shares the image and env of web but is not routed and not health-checked, and is restarted if it crashes.

The format is identical everywhere; only the commands change:

Python / Django

web: gunicorn app.wsgi
release: python manage.py migrate
worker: celery -A app worker

Ruby / Rails

web: bundle exec puma
release: bundle exec rake db:migrate
worker: bundle exec sidekiq
Background workers run a single instance and are single-node only — in multi-node mode only web is scheduled across the fleet. For a migration you want to run by hand, use a one-off command: deploycloud run <app> "npx prisma migrate deploy".

Environment variables

Set environment variables per app from Settings, or from the CLI and REST API. They’re encrypted at rest with AES-256-GCM and injected into your containers at deploy time — the platform stores only ciphertext, and listing shows keys, never values.

Changes apply on the next deploy. The environment is baked in when containers start, so after adding or changing a variable, redeploy to pick it up. Add-on credentials (below) are injected the same way, so DATABASE_URL and friends are always present.

$ deploycloud env myapp LOG_LEVEL=debug
Set LOG_LEVEL on myapp — redeploy to apply.

Add-ons

Provision a backing service in one click and DeployCloud generates its credentials and injects the connection details straight into your app’s environment. Three add-ons are built in:

  • Postgres — a dedicated database, exposed as DATABASE_URL.
  • Redis — an in-memory cache / queue, exposed as REDIS_URL.
  • MinIO — S3-compatible object storage, exposed as S3_ENDPOINT, S3_ACCESS_KEY and S3_SECRET_KEY.

Each add-on runs on the platform’s private network and is reachable directly from your app. Like any other configuration change, provisioning an add-on takes effect on the next deploy — so redeploy to apply.

Persistent volumes

By default an app’s filesystem is disposable: every deploy starts a fresh container from a fresh image, so anything written to disk at runtime is gone. A persistent volume is a named disk mounted into your containers at a path you choose that survives deploys — reach for one when your app owns files that must outlive a release:

  • a SQLite database file,
  • user uploads (images, attachments) written to the local filesystem,
  • an on-disk cache or index you don’t want to rebuild every release.

Add one under Settings → Persistent volumes. Give it a name (a short lowercase identifier, e.g. data) and an absolute mount path (e.g. /data); core system directories are rejected. Then point your app at it — for a SQLite app, mount data at /data and set DATABASE_URL=file:/data/app.db. The same volume is mounted into both your web and worker containers, and changes apply on the next deploy.

Volumes are single-node only and shared by every web replica on the host — fine for read-mostly data, but for write-heavy state (like SQLite under concurrent writes) run a single replica or use the Postgres add-on instead. Deleting a volume destroys its data with no undo.

Preview deployments

Turn on branch previews in an app’s settings and every branch you push gets its own live environment — built with the same pipeline as production, on its own subdomain:

<app>-pv-<branch>.<your-domain>

A preview runs in isolated containers with a single replica, so it can never touch production — your main deployment keeps serving, untouched. Share the URL on the pull request, click around the real app, then merge with confidence. When you delete the branch, its preview is torn down automatically. Previews are driven by the same GitHub webhook you set up for auto-deploys (below).

Scaling

Scale the web process horizontally by setting its replica count — from the dashboard, deploycloud scale <app> <n>, or the API. Scaling reuses the current release image (no rebuild) and rolls the change out with health checks. Traefik load-balances requests across all healthy replicas, so more replicas means more throughput with no changes to your app.

Outgrowing one box? In multi-node mode the platform becomes a small Docker Swarm fleet: apps run as replicated services scheduled across many worker nodes (added by hand or provisioned on demand), all behind a single Traefik ingress. Your app compute scales out while the dashboard, database and proxy stay on the manager.

Deploy from GitHub

Wire up a GitHub webhook and a push to your tracked branch builds and releases automatically. The webhook is signed (HMAC-SHA256), so the signature is the authentication — no token ever appears in the URL. In your repository, open Settings → Webhooks → Add webhook and fill in:

  • Payload URL https://<your-platform-host>/api/hooks/github/<appId>, where <appId> is the numeric id in the app’s dashboard URL.
  • Content typeapplication/json (form-encoded payloads sign differently).
  • Secret — the app’s deploy token, shown on its Settings page.
  • Events — “Just the push event”.

Save, and GitHub sends a ping — a green check with {"ok":"pong"} means the URL and secret are correct. Only pushes to the branch the app tracks enqueue a deploy; pushes to other branches, tags and branch deletions are acknowledged and ignored so the hook stays healthy. Enable branch previews (see above) and those other-branch pushes instead build a preview environment.

CLI

The command-line tool is a single zero-dependency Node script (Node 20+) that talks to the REST API with a bearer token. It covers the daily loop — deploy, watch, tail logs, manage env, scale, and run one-off commands — without leaving your terminal.

The CLI currently ships as the shipdeck binary. Until it’s renamed, run shipdeck wherever these docs write deploycloud — e.g. shipdeck deploy myapp --watch.

Log in by pointing the CLI at your platform and pasting an API token (created in the dashboard under Tokens, shown once). The token is verified before it’s saved:

$ deploycloud login https://deploycloud.example.com
API token (dashboard → Tokens): ••••••••
ok logged in — 3 apps visible

Then the everyday commands:

# list apps with status, scale and URL
$ deploycloud apps

# deploy and stream build logs; exits on success/failure
$ deploycloud deploy myapp --watch

# tail the runtime logs
$ deploycloud logs myapp --tail 100

# manage environment variables
$ deploycloud env myapp
$ deploycloud env myapp LOG_LEVEL=debug
$ deploycloud env myapp --unset LOG_LEVEL

# scale the web process
$ deploycloud scale myapp 3

# run a one-off command in the live release env
$ deploycloud run myapp npx prisma migrate deploy

deploy --watch polls until the deployment reaches running (exit 0) or failed (exit 1), which makes it drop-in for CI. run is synchronous and exits with the wrapped command’s own exit code, so it composes in scripts too.

REST API

Everything the CLI does is a plain REST API you can script directly. Every request is authenticated with a bearer token — create one in the dashboard under Tokens (shown once) and send it in the Authorization header:

Authorization: Bearer <token>

Apps are addressed by slug. The full surface:

GET/api/v1/apps
List every app with its latest deployment, scale and live URL.
GET/api/v1/apps/<slug>
Fetch one app, including its custom domains and environment variable keys.
POST/api/v1/apps/<slug>/deploy
Queue a build + release of the tracked branch. Idempotent while one is already queued.
GET/api/v1/apps/<slug>/deployments
The 20 most recent deployments for the app.
GET/api/v1/apps/<slug>/deployments/<id>
A single deployment with its full build and release log.
GET/api/v1/apps/<slug>/logs
Runtime container logs of the live release. Add ?tail=N to limit lines.
GET/api/v1/apps/<slug>/env
List environment variable keys. Values never leave the platform.
PUT/api/v1/apps/<slug>/env
Set a variable, encrypted at rest. Body: { "key": "...", "value": "..." }.
DELETE/api/v1/apps/<slug>/env
Remove a variable. Body: { "key": "..." }.
POST/api/v1/apps/<slug>/scale
Set the web replica count. Body: { "scale": 3 }.
POST/api/v1/apps/<slug>/run
Run a one-off command in the release image. Returns exit code + output.

For example, to trigger a deploy and then set an environment variable:

# queue a deploy of the tracked branch  ->  { "deploymentId": 42, "queued": "ok" }
curl -X POST https://deploycloud.example.com/api/v1/apps/myapp/deploy \
  -H "Authorization: Bearer $DEPLOYCLOUD_TOKEN"

# set an environment variable (encrypted at rest)
curl -X PUT https://deploycloud.example.com/api/v1/apps/myapp/env \
  -H "Authorization: Bearer $DEPLOYCLOUD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key":"LOG_LEVEL","value":"debug"}'

Security model

DeployCloud is a single-trusted-team tool, not a multi-tenant SaaS. The most important thing to understand before you run it: anyone who can deploy an app effectively has root on the host. The worker builds and runs containers through the host’s Docker daemon, and a deploy is arbitrary code — there’s no sandbox between “can push a deploy” and “owns the box”. Treat dashboard access exactly like SSH access, and give it only to people you’d hand a root shell.

Practical consequences:

  • Keep signups closed. In production, registration is off by default — only the very first signup succeeds, and it claims the instance. Register the owner account immediately after first boot so no one else can.
  • Keep the dashboard off the public internet. It’s an internal operations console — put it behind a VPN or an IP allowlist rather than exposing it openly. (Deployed apps are meant to be public; the dashboard is not.)
  • Secrets are encrypted at rest. App env vars and add-on credentials are encrypted with AES-256-GCM under a secret key that only you hold. Generate it with real entropy and back it up separately from the database — losing it means losing every encrypted value, with no recovery.
  • One team, one instance. There are no roles or per-app boundaries — everyone with access shares full control of every app. Run separate instances for teams that shouldn’t share root.
A malicious or compromised deployer is out of scope by design: DeployCloud does not sandbox deploys and cannot contain a hostile insider. Its isolation is your team’s trust plus keeping the dashboard off the open internet.

© 2026 DeployCloud · Self-hosted · open source

← Back to home