Self-hosted Docker app deployment platform. Deploy Docker Compose stacks to your own VMs with auto HTTPS, Cloudflare DNS, and Git-based CI/CD — all from a single binary.
Inspired by Coolify. Built for simplicity.
- Single binary — Go + SQLite, no external dependencies
- Caddy reverse proxy — auto Let's Encrypt, no config needed
- Cloudflare DNS — auto-create DNS records on deploy
- Git webhook — push to main, auto deploy
- VM pool — add VMs, deploy to least-loaded VM
- SSH console — interactive terminal via browser (WebSocket + xterm.js)
- Dashboard — live resource monitoring and deployment status at a glance
- Encrypted backups — export/import config with AES-GCM passphrase encryption
- Web UI — manage everything from the browser, dark/light mode
Controller VM (Dockify binary)
├── Web UI + REST API (:8080)
├── SQLite (state)
├── SSH manager → workers
├── Caddy Admin API → inject routes
├── Cloudflare API → DNS
└── Git webhook → auto deploy
Worker VM
├── Caddy (Docker, :80/:443 + Admin API :2019)
├── App containers (Docker Compose)
└── Caddy routes to containers via Docker network
Recommended OS: Debian 12 (minimal, no GUI). All specs are total VM size (OS + Dockify).
| Mode | Min vCPU | Min RAM | Min Disk | RAM used | HTTPS | Docker? |
|---|---|---|---|---|---|---|
| 1: Docker Compose | 1 | 1 GB | 10 GB | ~100 MB | Auto (Caddy container) | ✅ |
| 2: Binary only | 1 | 512 MB | 10 GB | ~30 MB | Manual | ❌ |
| 3: Binary + Caddy | 1 | 512 MB | 10 GB | ~40 MB | Auto (native Caddy) | ❌ |
Option A: Binary + native Caddy (lightweight + auto HTTPS) — recommended
curl -fsSL https://raw.githubusercontent.com/coderbuzz/dockify/main/scripts/install.sh | bash
# Select mode 3 (Binary + Caddy)
sudo systemctl start dockify-caddyDockify + Caddy run as native binaries (no Docker). Caddy auto-obtains Let's Encrypt certificates and proxies to Dockify on 127.0.0.1:8080.
Option B: Binary only (lightest)
curl -fsSL https://raw.githubusercontent.com/coderbuzz/dockify/main/scripts/install.sh | bash
# Select mode 2 (Binary only)
sudo systemctl start dockifyNo Caddy, no Docker. Access at http://<ip>:8080. Add a reverse proxy manually if needed.
Option C: Docker Compose (bundled Caddy)
curl -fsSL https://raw.githubusercontent.com/coderbuzz/dockify/main/scripts/install.sh | bash
# Select mode 1 (Docker Compose) - default
cd /opt/dockify && docker compose up -dOption D: Build from source (development)
git clone https://github.com/coderbuzz/dockify.git
cd dockify
go build -o dockify ./cmd/dockify
./dockify serveAll installation methods share the same architecture for worker VMs:
- Dockify connects to workers via SSH
- Installs Docker + creates the
dockifyDocker network on each worker - Deploys Caddy on each worker (port 80/443 + Admin API on localhost:2019)
- Apps communicate with Caddy through the
dockifynetwork
The choice between binary and Docker only affects how you host the Dockify controller itself. Worker VMs are identical either way.
Open https://<your-domain> or http://<controller-ip>:8080.
Create a .env file in the project root or set these environment variables:
| Variable | Default | Required | Description |
|---|---|---|---|
DOCKIFY_HOST |
0.0.0.0 |
No | Network interface to bind |
DOCKIFY_PORT |
8080 |
No | HTTP port |
DOCKIFY_DATA_DIR |
/var/lib/dockify |
No | SQLite DB + data storage directory |
DOCKIFY_SSH_KEY_DIR |
/var/lib/dockify/keys |
No | Per-server SSH private key storage |
DOMAIN |
— | Mode 1 | Domain for Caddy reverse proxy (auto HTTPS) |
DOCKIFY_ADMIN_USER |
admin |
No | Web UI login username |
DOCKIFY_ADMIN_PASSWORD |
— | No | Web UI password. If not set, the web UI has no authentication |
DOCKIFY_DEV_MOCK |
false |
No | Enable mock SSH for local development (no real VMs needed) |
CLOUDFLARE_API_TOKEN |
— | No | Cloudflare API token for automatic DNS A records |
CLOUDFLARE_ZONE_ID |
— | No | Cloudflare zone ID |
DOCKIFY_BASE_PATH |
— | No | URL prefix when behind a reverse proxy (e.g. /proxy/9898) |
To enable web UI authentication, set DOCKIFY_ADMIN_PASSWORD:
# In .env
DOCKIFY_ADMIN_USER=admin
DOCKIFY_ADMIN_PASSWORD=secret123
# Or as env vars
DOCKIFY_ADMIN_PASSWORD=secret123 docker compose up -d- Default username:
admin(configurable viaDOCKIFY_ADMIN_USER) - No password set: Web UI has no authentication (open to anyone)
- Password set: All routes (except
/healthand/api/webhook/*) require login - Session: Cookie-based, expires after 24 hours
- Logout: Visit
/logout
The login page is at /login. Webhook endpoints (/api/webhook/github, /api/webhook/gitlab) and health check (/health) do not require authentication.
Fresh Ubuntu/Debian VM. Zero dependencies needed — Dockify installs everything.
Option A: One-liner script (run on the worker VM)
curl -fsSL https://raw.githubusercontent.com/coderbuzz/dockify/main/scripts/setup-worker.sh | bashThe script will:
- Generate an SSH key pair at
/root/.ssh/dockify - Add the public key to
/root/.ssh/authorized_keys - Install Docker + Docker Compose plugin (if not present)
- Output the private key content — copy this
Then paste the private key into the Dockify Add Server form. No ssh-copy-id needed.
Option B: Manual (generate elsewhere)
Generate an SSH key pair anywhere (laptop, controller VM, etc.):
ssh-keygen -t ed25519 -f ~/.ssh/dockify -N ""Copy the public key to the worker VM:
ssh-copy-id -i ~/.ssh/dockify.pub root@<worker-ip>The private key content (
cat ~/.ssh/dockify) is then pasted into the Dockify web UI form — not a file path, but the key material itself.
- Go to Servers → Add Server
- Fill in:
- Name:
worker-01 - Host:
<worker-ip> - User:
root - SSH Private Key: Paste the content of
~/.ssh/dockify(the private key file,cat ~/.ssh/dockify)
- Name:
- Click Add Server → redirects to server detail. Dockify auto-tests the connection and starts initialization in the background.
- Click Initialize Worker (if not already auto-initialized)
What "Initialize Worker" does automatically:
- SSH connect + verify
- Install Docker via
get.docker.com(if not present) - Install Docker Compose plugin (if not present)
- Create
dockifyDocker network - Deploy Caddy container (port 80/443 + Admin API on localhost:2019)
- Collect CPU, RAM, Disk info
- Status → online. Ready to deploy apps.
Initialization is idempotent — re-running it on an already-initialized server skips existing components.
- Go to Apps → Deploy App
- Choose Simple Mode (just an image name + env vars + volumes) or Advanced Mode (full
docker-compose.yml) - Set domain, port, and select a server (or Auto-select for least-loaded)
- Optional: fill Basic Auth username/password to protect the app behind HTTP basic auth
- Click Deploy App
What happens on deploy:
- SSH → write compose file to
/opt/dockify/apps/<name>/ - SSH →
docker compose up -d - Inject Caddy route via Admin API (domain → container:port)
- If basic auth is set, Caddy requires username/password before proxying
- Create Cloudflare DNS A record (if configured, skips duplicates)
- Record deployment + save compose snapshot for rollback
- Status → running
The home page (/) provides a live overview of your infrastructure:
- Stats cards — total servers, online servers, total apps, running apps
- Server summary — all servers with live CPU/RAM usage and status badges
- App summary — all apps with status badges and domain links
- Empty state call-to-action buttons when no servers or apps exist yet
The Servers page lists all worker VMs with connection status and live resource usage. Click any server for its detail page.
Servers can be added via the web form (name, host, port, user, SSH private key) and edited after creation — update host, port, user, or SSH key without deleting and re-adding.
Each server can optionally store one registry credential (Registry Host, default ghcr.io; Registry User; Registry Token) so private images can be pulled without making the package public or running docker login by hand on the worker.
- On every deploy, if the server has a credential, Dockify runs
docker login <host> -u <user> --password-stdinon the worker beforedocker compose pull. The token is sent over the SSH session's stdin — it never appears in the remote command line (ps, shell history). - If login fails, the deploy stops and the deployment log shows
registry login failed: …with the token redacted ([REDACTED]). - The token is stored like the SSH private key: a
0600file inDOCKIFY_SSH_KEY_DIR(<id>.registry-token); the database only holds its path. It is never returned by the API or shown in the UI. - When editing a server, leave the token empty to keep the current one; tick Remove registry credential to delete it (host, user, and token file).
- Servers without a credential deploy exactly as before (no login).
- Docker saves the login in
~/.docker/config.jsonof the SSH user on the worker (standarddocker loginbehavior). - API:
POST /api/serversandPATCH /api/servers/:idacceptregistry_host,registry_user,registry_token;PATCHalso accepts"registry_clear": true.
Creating a GHCR token (PAT):
- GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic).
- Select only the
read:packagesscope (pulling needs nothing more). Set an expiration that suits you. - Copy the token (
ghp_…) into the server's Registry Token field, with your GitHub username as Registry User andghcr.ioas Registry Host.
For an organization package, the token's owner must have read access to the package (or the org must allow the user via package settings). If the org enforces SSO, authorize the token for that org.
Dockify collects CPU, RAM, and disk usage from all online servers every 60 seconds in the background. Resource cards update via HTMX partial refresh (no page reload). A manual refresh button is available on each server detail page.
- CPU — core count via
nproc, usage % via two-sample/proc/stat(1s interval) - RAM — total and usage via
/proc/meminfo(MemTotal/MemAvailable), with human-readable "used / free" info - Disk — total and usage via
df -BG
Real-time WebSocket stats (GET /api/servers/:id/stats/live) stream live metrics every 1 second for the server detail page resource card.
The scheduler uses these metrics to auto-select the least-loaded server (score = CPU% × 0.5 + RAM% × 0.5).
Each server detail page includes an interactive SSH terminal powered by xterm.js and WebSocket. Open a full terminal session directly in the browser — no SSH client needed.
- WebSocket connection:
GET /api/servers/:id/console - Full xterm.js with FitAddon for responsive terminal sizing
- Window resize events propagate to the remote PTY
- Raw keystroke passthrough for a native terminal feel
The Initialize Worker action installs Docker, creates the dockify network, and deploys Caddy. It is idempotent — safe to run multiple times.
The Apps page lists all deployed apps. Click any app for its detail page with full management controls.
- Simple Mode — provide an image name, port, volumes, environment variables, resource limits, and an optional command override. Dockify auto-generates the docker-compose file.
- Advanced Mode — paste a full
docker-compose.ymlwith complete control over the stack.
The compose mode (simple / advanced) is tracked per app and used to render the correct edit form.
Optionally enforce container resource constraints via dropdown selects:
- Memory Limit —
128m,256m,512m,1g,2g,4g - CPU Limit —
0.25,0.5,1.0,2.0,4.0 - Log Max Size — max size per log file (
10m,50m,100m,500m,1g) - Log Max File — number of log files to keep (
1,3,5,10)
Defaults to "unset" (no limit applied). In Advanced mode, set resource limits directly in the compose YAML.
Override the container's default command/entrypoint. Useful for passing flags or running a specific script instead of the image's built-in CMD. Leave empty to use the image default.
Re-configure an existing app via the edit page. Dockify pre-fills the form with current settings (parsed from simple fields or the raw compose), saves changes, and automatically redeploys the app.
When creating or editing an app, set the server field to Auto-select (or server_id=0 via API). Dockify picks the least-loaded online worker VM based on CPU and RAM usage.
Pause and resume an app without undeploying or losing configuration. Stopping runs docker compose stop and removes the Caddy route. Starting re-creates the route and DNS record.
Roll back to the last successful deployment. Dockify restores the compose snapshot saved at deploy time and redeploys the app.
Each app keeps a history of deployments with status (running/failed), log output, commit SHA (for Git-triggered deploys), and timestamps. Viewable on the app detail page.
Stream container logs via SSH (docker compose logs). Lazy-load buttons on the app detail page let you tail the last 50, 200, or 500 lines with HTMX partial updates.
While an app is deploying or a server is initializing, the detail page auto-refreshes every 2 seconds until the operation completes.
Each app's environment variables are managed through a unified editor with per-variable type control:
- Plain — value is visible in the UI and editable inline
- Secret — value is masked (
••••••) in the UI and encrypted in exports
Multi-line support — values can span multiple lines (useful for private keys, certificates, etc.) using resizable textareas.
On deploy, all variables (plain + secret) are written to a .env file at /opt/dockify/apps/<name>/.env on the worker VM. In Simple mode, the compose file auto-generates ${KEY} environment variable references, so containers can access all variables without manual compose editing.
Editing secrets: On the edit page, secret values are shown as empty (like GitHub Actions). Leave empty to keep the existing value. Type a new value to override.
Import from .env: Click the import button to open a modal dialog, paste KEY=VALUE lines, and bulk-add variables. Imported variables default to the Secret type.
Each app can have arbitrary config files. Files are written to /opt/dockify/apps/<name>/<path> on the worker VM. Upload files directly from the browser (FileReader API) or paste content. Manage them inline on the app detail page.
Set auth_user and auth_pass on an app to require HTTP basic auth before accessing it. Caddy enforces this at the proxy level with bcrypt — the app itself does not need to handle authentication.
In simple mode, Dockify renames the first service in the generated compose to a sanitized version of the app name (replacing ., _, spaces with -). This prevents service name conflicts when multiple apps use the same image.
Dockify can auto-deploy on every push via GitHub or GitLab webhooks. When an app is created with a Git Repo URL and Branch, Dockify matches incoming webhooks by repo + branch and triggers a redeploy. A single webhook can trigger redeploys for multiple matching apps.
A global webhook secret is auto-generated on first startup. See the Webhook Security section below.
- In your app repo, go to Settings → Webhooks → Add webhook
- Fill in:
Payload URL: https://dockify.example.com/api/webhook/github Content type: application/json Secret: <copy from Dockify Settings page — Webhook Secret field> Events: Just the push event - In the Dockify UI, when creating the app, fill in:
- Git Repo URL:
https://github.com/user/repo.git - Branch:
main
- Git Repo URL:
Dockify verifies incoming webhooks using HMAC-SHA256 (GitHub) or secret token (GitLab). Non-push events are ignored gracefully (returns 200 with "ignored").
Dockify auto-generates a global webhook secret (64-character hex) on first startup, stored in the database. You can view, copy, regenerate (roll), or disable it on the Settings page (/settings). When disabled, webhooks are accepted without signature verification.
When the webhook secret is set, Dockify validates incoming webhooks:
- GitHub — HMAC-SHA256 via
X-Hub-Signature-256header - GitLab — plain token comparison via
X-Gitlab-Tokenheader
If the signature or token does not match, the webhook is rejected with 401 Unauthorized.
To configure:
- Go to Settings in the Dockify web UI
- Click Show → Copy the webhook secret
- Paste it as the Secret in your GitHub/GitLab webhook settings
- For GitHub Actions, pass the secret via
${{ secrets.DOCKIFY_WEBHOOK_SECRET }}(see examples below)
Note: When you Roll the secret, the old one stops working immediately. Update all CI secrets to match. Use Disable to turn off secret verification entirely.
Add repository secrets (Settings → Secrets and variables → Actions):
DOCKIFY_URL— your Dockify URL (e.g.https://dockify.example.com)DOCKIFY_WEBHOOK_SECRET— the webhook secret from Dockify Settings (for Option B)
Option A: Without webhook secret (simple; disable the webhook secret on the Settings page first, or use when no secret is configured)
name: Deploy via Dockify
on:
push:
branches: [main]
jobs:
test-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
echo "Running tests..."
# npm test, go test, etc.
- name: Trigger Dockify deploy
env:
DOCKIFY_URL: ${{ secrets.DOCKIFY_URL }}
run: |
curl -s -X POST $DOCKIFY_URL/api/webhook/github \
-H "Content-Type: application/json" \
-d '{
"ref": "refs/heads/main",
"after": "${{ github.sha }}",
"repository": {
"clone_url": "https://github.com/${{ github.repository }}.git"
}
}'Option B: With webhook secret (HMAC-SHA256, recommended for public-facing deployments)
name: Deploy via Dockify
on:
push:
branches: [main]
jobs:
test-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
echo "Running tests..."
# npm test, go test, etc.
- name: Trigger Dockify deploy
env:
DOCKIFY_URL: ${{ secrets.DOCKIFY_URL }}
DOCKIFY_WEBHOOK_SECRET: ${{ secrets.DOCKIFY_WEBHOOK_SECRET }}
run: |
REPO="https://github.com/${{ github.repository }}.git"
BRANCH="${GITHUB_REF#refs/heads/}"
COMMIT="${{ github.sha }}"
PAYLOAD=$(cat <<EOF
{"ref":"refs/heads/${BRANCH}","after":"${COMMIT}","repository":{"clone_url":"${REPO}"}}
EOF
)
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$DOCKIFY_WEBHOOK_SECRET" | sed 's/^.* //')
curl -s -o /dev/null -w "%{http_code}" \
-X POST $DOCKIFY_URL/api/webhook/github \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-H "X-Hub-Signature-256: sha256=${SIGNATURE}" \
-d "$PAYLOAD"- GitHub sends a push event to
POST /api/webhook/github - Dockify parses
ref→ branch,after→ commit SHA,clone_url→ repo URL - Finds all matching apps by
git_repo+git_branch - Triggers
deployWithCommit(app.ID, commitSHA)for each match — same deploy flow as UI - Records the deployment with commit SHA in history
When CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID are configured, Dockify automatically manages DNS records on deploy:
- Creates an A record pointing to the worker VM's IP on app deploy (120s TTL)
- Skips creation if a matching record already exists (deduplication)
- Upserts records if the worker IP changes on re-deploy
- DNS records are tracked in the database for cleanup
- Certificate-safe updates: When updating an existing A record with a different IP, Dockify temporarily disables Cloudflare's proxy (orange cloud → gray) to allow Caddy to complete HTTP-based certificate validation. Once the certificate is issued, the proxy is re-enabled to its original state.
No DNS automation occurs if the env vars are not set.
Export your configuration (servers + apps + secrets + config files) as YAML and import it into another instance.
Backups can be protected with a passphrase using AES-GCM encryption (PBKDF2 key derivation, 600,000 iterations):
- Secret environment variables, SSH keys, registry tokens, and auth passwords are encrypted before export
- Plain environment variables are exported as readable YAML (they are not sensitive by definition)
- The
is_secretflag is preserved on import, so plain/secret distinctions survive migration - Encrypted values use the prefix
enc:with base64 salt + nonce + ciphertext - The export page includes a client-side passphrase generator (32-character hex, via
crypto.getRandomValues) - Without a passphrase, data is exported without encryption (still useful for offline/internal backups)
When importing on a new instance, all servers and their SSH keys are restored automatically — no need to re-enter keys one by one, as long as the target VMs are reachable or use the same key pair.
Upload a previously exported YAML file via the Import page (/import):
- merge (default): skip entries that already exist by name
- replace: delete all existing servers and apps before import
- Encrypted values are validated by attempting decryption before the actual import
Both Export and Import are accessible from dedicated pages (/export, /import) and also linked from the Settings page for convenience.
The Settings page (/settings) provides centralized management:
| Feature | Description |
|---|---|
| Webhook Secret | View, copy, or roll (regenerate) the global webhook secret |
| Update Check | Checks GitHub Releases for a newer version |
| Run Update | Triggers a self-update via systemd-run — downloads and runs the latest update.sh |
The About page (/about) shows the current version, project description, and a Sponsor link. An update progress bar with live polling keeps you informed during self-updates.
Dockify's web UI is built with Go html/template + HTMX and fully custom CSS (no framework):
- Dark/Light mode — toggle via nav button, persisted in
localStorage(dockify-theme) - HTMX partial updates — resource cards, log viewer, and status badges update without full page reloads
- Responsive — adapts to mobile screen sizes (single breakpoint at 600px)
- Status badges — color-coded for all server and app states
- Confirm dialogs — destructive actions (undeploy, delete, rollback) require confirmation
- Relative timestamps — deployment and resource times rendered in browser timezone
For local development without real worker VMs, set DOCKIFY_DEV_MOCK=true:
- Enables a mock SSH client with realistic CPU/RAM/Disk responses
- Mock SSH console — simulated interactive terminal that echoes commands
- A yellow "Dev Mock Mode" banner appears above the nav bar
- Works with Air for live-reload:
air # starts at http://localhost:8080, auto-rebuilds on saveData is stored in ./data/ (SQLite DB, gitignored). See AGENTS.md for full development workflow details.
A universal update script detects your install mode and updates accordingly:
curl -fsSL https://raw.githubusercontent.com/coderbuzz/dockify/main/scripts/update.sh | bashOr manually per mode:
Mode 1 (Docker Compose):
cd /opt/dockify && docker compose pull && docker compose up -dMode 2 & 3 (Binary):
sudo systemctl stop dockify
sudo curl -fsSL -o /usr/local/bin/dockify \
"https://github.com/coderbuzz/dockify/releases/latest/download/dockify-linux-amd64"
sudo chmod +x /usr/local/bin/dockify
sudo systemctl start dockifyscripts/gen-ssh-key.sh generates a dedicated SSH key on any VM for external,
general-purpose access — e.g. handing an agentic AI SSH access for remote
debugging, running shell commands, or cleaning up a compromised VM. It is fully
independent of Dockify's automation key, so you can revoke or rotate it without
affecting Dockify.
curl -fsSL https://raw.githubusercontent.com/coderbuzz/dockify/main/scripts/gen-ssh-key.sh | bashThe script will:
- Generate an SSH key pair at
$HOME/.ssh/remote-access(ed25519, no passphrase) - Add the public key to
$HOME/.ssh/authorized_keys - Configure
KexAlgorithmsin/etc/ssh/sshd_config(requiressudo) - Output the private key content — copy this into your agentic-AI / SSH client config
The key is generic and separate from Dockify's /root/.ssh/dockify automation key.
To revoke access later: rm ~/.ssh/remote-access* and remove its line from
~/.ssh/authorized_keys.
dockify/
├── cmd/dockify/main.go
├── internal/
│ ├── ssh/ # SSH client, remote exec, worker init, mock client
│ ├── server/ # Server CRUD, resource monitoring
│ ├── app/ # App CRUD, deployment, rollback, stats collector
│ ├── caddy/ # Caddy Admin API client
│ ├── cloudflare/ # Cloudflare DNS API
│ ├── webhook/ # Git webhook handler (GitHub + GitLab)
│ ├── scheduler/ # Idle server auto-selection
│ ├── settings/ # Global settings, update checker
│ ├── backup/ # Export/import YAML with encrypted secrets
│ ├── db/ # SQLite layer
│ └── http/ # HTTP server, handlers, templates (HTMX + custom CSS)
│ ├── console.go # WebSocket SSH console
│ ├── stats_ws.go # WebSocket real-time resource stats
│ └── templates/ # HTML templates
├── scripts/ # Install, worker setup, update, release scripts
└── Dockerfile # Multi-stage Docker build
MIT © 2025 CoderBuzz