Deploy your first app, step by step
From an empty directory to a live URL on the PaaS. We build a tiny Node.js service — the same shape as the sample apps that ship with the platform — then configure it, give it a database, and finish with logs and HTTPS. No prior platform knowledge needed; every step says what it does and what you should see.
Prerequisites
- your registered Dokku SSH key (shown at
~/.ssh/dokku-deploy.pem); ask your operator to register its public half, not to publish a private key- this platform's
<public-ip>andprototype.imeme.name(on the default build,prototype.imeme.nameis<public-ip>.sslip.io)git, and Node.js (any recent version) to test locally- your machine on a network allowed to reach SSH. An empty
allowed_cidrspermits all sources; HTTP 80 and HTTPS 443 are public by default- for custom DNS, apex and wildcard A records targeting the Primary IPv4, with no AAAA records unless IPv6 is actually configured
Step 0 — Set up the deploy key
chmod 600 ~/.ssh/dokku-deploy.pem
Optional but recommended — teach SSH about the key once:
cat >> ~/.ssh/config <<'EOF'
Host dokku-paas
HostName <public-ip>
User dokku
IdentityFile ~/.ssh/dokku-deploy.pem
EOF
The rest of this guide uses the dokku-paas alias. Without it, add
-i ~/.ssh/dokku-deploy.pem to every ssh command and use
dokku@<public-ip> instead of dokku-paas in the git remote.
Step 1 — Scaffold the app
mkdir my-first-app && cd my-first-app
server.js — a dependency-free HTTP service with a /health endpoint.
Note the two platform contracts: it reads PORT from the environment
and it reads its config (GREETING) from the environment too.
'use strict';
const http = require('http');
const APP_NAME = 'my-first-app';
const PORT = process.env.PORT || 5000;
const GREETING = process.env.GREETING || 'Hello from the PaaS!';
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', app: APP_NAME }) + '\n');
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
app: APP_NAME,
message: GREETING,
database: process.env.DATABASE_URL ? 'linked' : 'not linked',
}) + '\n'
);
});
server.listen(PORT, () => {
console.log(`${APP_NAME} listening on port ${PORT}`);
});
package.json — the manifest that tells the platform this is a Node.js
app:
{
"name": "my-first-app",
"version": "1.0.0",
"private": true,
"main": "server.js",
"scripts": { "start": "node server.js" }
}
Procfile — declares the web process the platform runs:
web: node server.js
.gitignore:
node_modules/
Step 2 — Test it locally
node server.js & # starts on port 5000
server_pid=$!
curl http://localhost:5000/
curl http://localhost:5000/health
kill "$server_pid"
You should see the JSON greeting and {"status":"ok","app":"my-first-app"}.
If it works locally, it satisfies the platform's build requirements.
Step 3 — Commit it
git init -b main
git add -A
git commit -m "Initial commit"
Step 4 — Create the app on the platform
ssh dokku-paas apps:create my-first-app
The name must match ^[a-z0-9][a-z0-9.-]*$ (lowercase, digits, dots,
hyphens — no underscores). Creating the app reserves the name and its
URL; nothing runs yet.
Step 5 — Push and deploy
git remote add dokku dokku-paas:my-first-app
git push dokku main:master
Why main:master? The platform deploys only its master branch.
Your local branch is main, so push it across. If you forget the
:master, the push succeeds but nothing deploys (the output warns you).
Watch the output — it is the whole deploy pipeline:
-----> Cleaning up/-----> Building— the Node.js buildpack is detected and your app is built into a container image-----> Releasing— the new container starts-----> Deploying— zero-downtime checks: the platform confirms the new container is up before switching traffic to it-----> Deployed!followed by the app's URL
Step 6 — Verify it's live
curl https://my-first-app.prototype.imeme.name/
curl https://my-first-app.prototype.imeme.name/health
Expect the same JSON as locally. The post-deploy hook automatically enables Let's Encrypt for every public app, and cron renews certificates. Custom DNS must already be configured; wildcard DNS is not created by the app deploy. Certificate failures are explicit: inspect deploy output, and have the operator fix DNS/port 80 and retry as in step 10. Never claim HTTPS is ready without a successful request and valid certificate.
Step 7 — Change configuration without touching code
ssh dokku-paas config:set my-first-app GREETING="Hello, configured world"
curl https://my-first-app.prototype.imeme.name/
config:set restarts the app with the new environment — the greeting
changes with no commit and no push. Avoid full config dumps: they reveal
secrets. The platform automatically provisions a private
Open Terminal API URL and shared key into every app. These are sensitive
and do not create per-app API isolation; don't print them in logs or HTTP
responses. See Web shell for the trust boundary.
Step 8 — Give it a database
ssh dokku-paas postgres:link demo-db my-first-app
curl https://my-first-app.prototype.imeme.name/
Linking injects DATABASE_URL into the app's environment and restarts
it — the response's database field flips to linked. The same works
for Redis: ssh dokku-paas redis:link demo-cache my-first-app injects
REDIS_URL. Unlink with postgres:unlink demo-db my-first-app.
Your code never hardcodes a host, port, or password: it parses
DATABASE_URL, so the same artifact runs anywhere.
Step 9 — Read its logs and manage the process
ssh dokku-paas logs my-first-app # recent logs
ssh dokku-paas logs my-first-app -t # stream live (Ctrl-C to stop)
ssh dokku-paas ps:report my-first-app # process state
ssh dokku-paas ps:restart my-first-app # restart without a deploy
Everything the app prints to stdout/stderr shows up in logs.
Step 10 — Verify automatic HTTPS; retry if needed (operator step)
HTTPS uses a Let's Encrypt certificate, automatically enabled by the post-deploy hook for every public app, not just preinstalled apps. If issuance fails, the operator fixes DNS and port-80 reachability, then runs:
sudo paas-enable-https my-first-app # run on the host, not in the terminal
After successful issuance, verify https://my-first-app.prototype.imeme.name
and the certificate renews itself automatically (the platform runs a
renewal job daily). Your app changed nothing — it still serves plain
HTTP internally; TLS terminates at the platform edge.
Step 11 — Ship a change
# edit server.js, then:
git add -A && git commit -m "Change the greeting"
git push dokku main:master
Each push is a zero-downtime deploy: the new version is built and verified before traffic switches, and the old version drains for 60 seconds before retiring. If the new version crashes on boot, the deploy fails and the old version keeps serving.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Please specify a valid app name / create fails |
uppercase, underscores, or a leading dot/hyphen in the name | rename to match ^[a-z0-9][a-z0-9.-]*$ |
| Push succeeds but the site never changes | pushed to main, not the deploy branch |
git push dokku main:master |
Push asks for a password / Permission denied (publickey) |
deploy key not used | ssh-add ~/.ssh/dokku-deploy.pem, the ~/.ssh/config alias from step 0, or GIT_SSH_COMMAND="ssh -i ~/.ssh/dokku-deploy.pem" git push … |
ssh/push times out |
firewall allowlist, wrong IP or SSH unavailable | verify the target and ask your operator to check your CIDR |
URL returns 502 Bad Gateway |
app crashed or isn't listening on $PORT / bound to 127.0.0.1 |
ssh dokku-paas logs my-first-app; fix the bind to 0.0.0.0:$PORT |
| Deploy fails at the checks step | app exits within ~10 s of boot (crashes on start) | read the push output and logs; fix the boot crash and re-push |
| App can't reach Postgres/Redis | service not linked | postgres:link demo-db my-first-app / redis:link demo-cache my-first-app |
Where to go next
- Application requirements — the full
contract, including
app.jsondeploy-time health checks - Scale out:
ssh dokku-paas ps:scale my-first-app web=2 - Custom domain:
ssh dokku-paas domains:add my-first-app www.example.com(then point DNS at the platform and ask the operator to runsudo paas-enable-https my-first-app) - Persist files: ask your operator about
dokku storage:mountfor a host-backed volume - Back to Getting started