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
- the deploy key (ed25519 private key) from your platform operator, saved at
~/.ssh/dokku-deploy.pem- this platform's
54.170.127.122and54.170.127.122.sslip.io(on the default build,54.170.127.122.sslip.iois54.170.127.122.sslip.io)git, and Node.js (any recent version) to test locally- your machine on a network allowed to reach the platform — SSH and HTTP(S) are restricted to allowlisted networks; ask your operator if a connection times out
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 54.170.127.122
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@54.170.127.122 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
curl http://localhost:5000/
curl http://localhost:5000/health
kill %1
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.54.170.127.122.sslip.io/
curl https://my-first-app.54.170.127.122.sslip.io/health
Same JSON as locally, now served by the platform. DNS for
*.54.170.127.122.sslip.io resolves automatically (wildcard DNS), so the URL
works the moment the first deploy finishes. If you get a TLS error
instead, the app has no certificate yet — step 10 enables it with one
operator command; until then these same curls work over http://.
Step 7 — Change configuration without touching code
ssh dokku-paas config:set my-first-app GREETING="Hello, configured world"
curl https://my-first-app.54.170.127.122.sslip.io/
config:set restarts the app with the new environment — the greeting
changes with no commit and no push. Inspect everything set on the app
with ssh dokku-paas config:show my-first-app (values are shown; treat
the output as sensitive).
Step 8 — Give it a database
ssh dokku-paas postgres:link demo-db my-first-app
curl https://my-first-app.54.170.127.122.sslip.io/
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 — Turn on HTTPS (operator step)
HTTPS uses a free Let's Encrypt certificate, enabled per app by a platform operator (the platform's pre-installed apps already have it):
sudo dokku letsencrypt:enable my-first-app # run on the host
From then on the app answers at https://my-first-app.54.170.127.122.sslip.io
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 |
your network isn't allowlisted on the platform firewall | ask your operator to add 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 re-enable Let's Encrypt) - Persist files: ask your operator about
dokku storage:mountfor a host-backed volume - Back to Getting started