- Home
- Skills
- DevOps & Cloud
- Docker & Coolify Self-Hosting Runbook
Works with the AI Agents you already use
Docker & Coolify Self-Hosting Runbook
Operations runbook for Docker and Coolify self-hosting: hardening, backups, and incident triage for solo developers.
$8.99
Docker & Coolify Self-Hosting Runbook
Example session with this skill installed
My Coolify app returns 502 Bad Gateway right after I redeployed it this morning. Ubuntu 22.04 VPS, 2 vCPU / 4 GB RAM, one Next.js app built with Nixpacks plus a Postgres database, both in Coolify. Logs show the build succeeded. Disk is at 91%. What do I do?
- Read your context and instructions
- Compiled the docker & coolify
Context: Ubuntu 22.04 VPS, 2 vCPU / 4 GB RAM, Coolify with its default proxy
(assumed), one Next.js app built with Nixpacks, one PostgreSQL database in Coolify, disk at 91%. The 502 started right after a redeploy, and the build succeeded.
A 502 from Coolify means the proxy is up and found a route for your domain, but it cannot reach a healthy app container. The build succeeding only tells you the image was built. It does not tell you the new container started, is listening on the right port, or can reach Postgres. Rungs 1–3 (outside check, DNS, TLS) are effectively passed: you are getting a 502 from the proxy, not a DNS or certificate error. So start at rung 4.
Placeholders used below, all
(assumed): domain app.example.com, app port 3000, server <server>, app container <app_container>, Postgres container <pg_container>, database user <user>, database <db>.
1. Triage ladder for this 502
Step 0: Back up the database first (before you change anything)
Your disk is above 85% full, so
do not write the dump to the server. Stream it to your own machine. Run this
from your laptop, logged in as root with your key:
ssh root@<server> 'docker ps --format "{{.Names}}\t{{.Image}}" | grep -i postgres'
Use the name it prints as <pg_container>, then:
ssh root@<server> 'docker exec <pg_container> pg_dump -U <user> -Fc <db>' > db-"$(date +%F)".dump
ls -lh db-*.dump
Good: the file is larger than a few KB. If you have PostgreSQL client tools locally, pg_restore -l db-YYYY-MM-DD.dump | head lists tables.
Not good: a 0-byte file or an error, which usually means the user or database name is wrong. Fix that before going further. The user and database names are shown on the database resource in Coolify.
Rung 4: Proxy
Find the containers and the proxy's own error:
sudo docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
sudo docker ps --filter name=coolify-proxy
sudo docker logs --tail 50 coolify-proxy 2>&1 | grep -iE 'error|502|refused|timeout'
Your app container is the one whose image or name matches the app resource in Coolify. Coolify uses generated names, so use that name as <app_container>. Then test the app from the proxy's side of the network:
sudo docker exec coolify-proxy wget -qO- -T 5 http://<app_container>:3000/
| Result | Meaning | Likely fix |
|---|---|---|
| HTML comes back | App is fine from the proxy, so the route is the problem | Check that the port Coolify sends traffic to (the app's exposed-port setting) matches the port the app listens on, then redeploy |
Connection refused | Wrong port, or the app is not listening on all interfaces | See the Next.js note below |
bad address | Proxy and app are not on the same Docker network | Redeploy the app from Coolify so it reattaches to the proxy network. If that fails, check that the app resource is on the same server and destination as before |
| Timeout | App is hung or still booting | Go to rung 5 |
Next.js note: do not judge the listen address by the "localhost" URL in the logs. If Nixpacks starts the app with next start, it listens on all interfaces, on $PORT or 3000. If your app uses output: 'standalone' and starts with node server.js, it listens on $HOSTNAME, so set the runtime environment variable HOSTNAME=0.0.0.0 in Coolify and redeploy. Also check that PORT, if you set it, matches the exposed port.
Rung 5: App container
sudo docker ps -a --filter name=<app_container>
sudo docker logs --tail 100 <app_container>
sudo docker inspect -f '{{.State.Status}} {{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Restarts={{.RestartCount}}' <app_container>
Restarting / exited: the logs show the crash on boot. After a redeploy this is usually a changed or missing env var (DATABASE_URL, NEXTAUTH_URL and similar), a missing migration, or a build-time variable that is only set at runtime (or the other way round). Fix the variable in Coolify and redeploy.
Exit code 137: this only means the process was killed. Blame memory only if OOMKilled=true or rung 7 shows a kernel kill. With 4 GB RAM and builds running on the same box, memory pressure is a common pattern.
"Unhealthy": the health check itself may be failing. Common causes are the wrong port, or curl/wget missing in the image. Nixpacks images are Debian-based and may not include curl.
Rung 6: Dependencies (Postgres)
sudo docker exec <app_container> getent hosts <db_host>
Use the internal hostname shown on the Postgres resource in Coolify as <db_host>, not localhost.
Good: it prints an IP.
Empty: the two containers do not share a network, so use the internal connection URL from the database resource in DATABASE_URL. Also check that Postgres itself is running:
sudo docker exec <pg_container> pg_isready -U <user>
Redeploying the app does not redeploy the separate database resource, so this morning's redeploy did not change your Postgres version.
Rung 7: Host (your disk is the red flag)
df -h /
free -h
sudo dmesg -T | grep -i -m5 'killed process'
uptime
sudo docker system df
sudo sh -c 'du -h /var/lib/docker/containers/*/*-json.log' | sort -h | tail
At 91%, a build plus a new image can briefly push the disk to 100%. That breaks writes (the Next.js .next/cache, logs, and Postgres itself). Fix this
before you redeploy again, or the next deploy fails too.
2. Fastest safe recovery
A. Roll back first if the old version worked. In Coolify's deployment list for the app, redeploy the previous working deployment. This only works while that image is still on the server, so
do not run docker image prune -a before you have rolled back or found the fix. Verify:
curl -sI https://app.example.com | head -1
Expect HTTP/2 200 (or a 3xx redirect to a page that loads).
B. Free disk, in this order. Each step is
⚠ changes state. The check that justifies it is the 91% from df -h / and the docker system df output above.
sudo docker builder prune # build cache: usually the largest, always safe
sudo docker image prune # dangling images only; keeps rollback images
If the log check above showed a multi-GB *-json.log file:
sudo truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log # ⚠ changes state
sudo journalctl --vacuum-size=200M # ⚠ changes state
df -h /
Target: under 80% used.
Do not run docker volume prune -a or docker system prune --volumes here. Your Postgres data lives in a volume. Never touch /data/coolify.
C. Fix the cause found in rungs 4–6 (port, HOSTNAME=0.0.0.0, env var, or DB hostname), then redeploy the app from Coolify
⚠ changes state. Verify with the curl -sI above and the in-network check:
sudo docker exec coolify-proxy wget -qO- -T 5 http://<app_container>:3000/ | head -c 200
D. Write a five-line incident note: symptom, rung that failed, root cause, fix, and the readiness line that would have prevented it (probably line 4 or 8).
3. Readiness score
Scores are guessed from your message, so all are (assumed). Correct me and I will rescore.
| # | Control | Score |
|---|---|---|
| 1 | SSH keys only, non-root sudo user | 1 (assumed) |
| 2 | Provider firewall, only 22/80/443 | 0 (assumed) |
| 3 | Dashboard on HTTPS domain; 8000/6001/6002 closed | 0 (assumed) |
| 4 | Swap + Docker log rotation | 0 (assumed; the full disk points this way) |
| 5 | Off-server DB backups on a schedule | 0 (assumed) |
| 6 | APP_KEY stored off-server | 0 (assumed) |
| 7 | Restore tested in last 90 days | 0 (assumed) |
| 8 | External uptime check + disk alert | 0 (assumed; 91% went unnoticed) |
| 9 | Image tags pinned (no latest for DB) | 0 (assumed) |
| 10 | Written update cadence + snapshot first | 0 (assumed) |
| Total | 1/20 (assumed). Under 12 means fix these before adding more apps |
4. Server baseline (short form)
SSH: create a sudo user (e.g. deploy
(assumed)) and copy your key. Confirm a new login works in a second terminal before you change sshd.
printf 'PasswordAuthentication no\nPermitRootLogin prohibit-password\n' | sudo tee /etc/ssh/sshd_config.d/10-hardening.conf
sudo sshd -t && sudo sshd -T | grep -iE '^(passwordauthentication|permitrootlogin)'
sudo systemctl restart ssh # ⚠ changes state; keep your current session open
Expect passwordauthentication no and permitrootlogin without-password. Keep root key access, because Coolify needs it.
2.
Firewall: use the provider's firewall to allow 22/80/443. Allow 8000/6001/6002 only from your IP. ufw alone does not block Docker-published ports.
3.
Updates: sudo apt install unattended-upgrades && sudo dpkg-reconfigure -plow unattended-upgrades. Leave automatic reboots off.
4. Swap (4 GB, after disk is freed): you need at least 14 GB free first (df -h /).
sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
grep -q swapfile /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h
Log rotation: merge { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } into /etc/docker/daemon.json. Then run sudo systemctl restart docker in a quiet window (⚠ changes state, restarts all containers) and redeploy the containers so they pick up the setting.
6. Time: timedatectl should show System clock synchronized: yes.
5. Coolify security (short form)
- The dashboard should be on a domain with HTTPS. Then close 8000/6001/6002, turn off open registration, and enable 2FA.
- Save the encryption key off the server in a password manager:
sudo grep '^APP_KEY=' /data/coolify/source/.env
6. Deploy checklist: Next.js app (short form)
- Build: Nixpacks works, but consider a Dockerfile you control for reproducible builds.
Health check: add a /health route that returns 200 only when Postgres is reachable, and configure it in Coolify so a failed deploy does not replace a healthy container. Make sure the test tool (curl/wget) exists in the image.
- Listen address and port: use
HOSTNAME=0.0.0.0for a standalone build. The exposed port must equal the app port. - Env: mark build-time variables (
NEXT_PUBLIC_*) separately from runtime ones. - Postgres image: pin the major version (e.g.
postgres:16(assumed)), neverlatest. - Memory limit: set one on the app so a leak cannot starve Postgres on a 4 GB host.
- Rollback: keep Coolify's image retention above one, and avoid
docker image prune -a.
7. Backups and updates (short form)
| Data | Method | Schedule | Retention |
|---|---|---|---|
| Postgres | Coolify scheduled DB backup to S3-compatible storage | Daily | 14 daily + 8 weekly |
| Coolify instance | Instance backup + APP_KEY in password manager | Daily | 14 |
| Whole server | Provider snapshot | Before every host update | Last 2 |
Restore test (verify step): restore into a new database, never over production.
sudo docker exec <pg_container> createdb -U <user> <db>_restore_test
ssh root@<server> 'docker exec -i <pg_container> pg_restore -U <user> -d <db>_restore_test --no-owner' < db-YYYY-MM-DD.dump
sudo docker exec <pg_container> psql -U <user> -d <db>_restore_test -tAc 'select count(*) from <table>'
Compare the count against <db>, then drop the test database. Do this every 90 days and record how long it took. The dump stays on your laptop and is fed back over SSH, so the full disk is not touched.
Updates: OS patches run automatically, with a weekly reboot if /var/run/reboot-required exists. Update Docker Engine monthly, after a snapshot. Update Coolify after reading the release notes and taking an instance backup. Postgres major versions are a planned project: dump, restore, test, then switch.
Volumes: check your engine with docker version --format '{{.Server.Version}}'. On 23+, docker volume prune -a deletes
named volumes, including database data. Never run it. Keep Coolify's "delete unused volumes" cleanup option off.
8. Top three actions this week (risk reduced per hour)
Get Postgres backups off the server on a schedule (S3-compatible target) and save APP_KEY in a password manager. About 30 minutes. Right now your only backup is the dump you streamed today.
2.
Fix disk for good: prune build cache, set Docker log rotation, add a disk-usage alert at 80%, and add an external uptime check. About 45 minutes. This stops the next 91% surprise.
3.
Add a /health endpoint plus a Coolify health check so a bad deploy does not replace a working container. About 30 minutes. That turns the next "502 after redeploy" into "deploy failed, old version still serving."
Want a tailored version?
Answer any of these (or say "skip"):
- What runs on the server besides the Next.js app and Postgres? Does the Next.js app use
output: 'standalone'? - Which provider? Is there a provider-level firewall? What is the total disk size?
- Where can backups go off the server (S3-compatible bucket, second server, none yet)?
- What is the longest outage you can tolerate, and how much data can you afford to lose?
- Which Docker Engine version? (
docker version --format '{{.Server.Version}}')
Paste the output of the docker exec coolify-proxy wget … check and the last 30 lines of the app container's logs, and the cause can usually be pinned down exactly.
Connects securely to your tools. The creator never sees your data.
About this skill
Self-hosting on a single VPS is often easier to set up than it is to keep running. This skill provides the exact operational runbook needed to manage production apps using Docker and Coolify, moving beyond basic tutorials into real-world maintenance, restore drills and incident triage.
What it does
- Readiness scoring evaluates your server against a 10-point checklist covering SSH hardening, firewalls, and backup integrity.
- Server baselining provides specific commands for user hardening, swap configuration, and Docker log rotation to prevent common disk-full outages.
- Incident triage delivers an "outside-in" diagnostic ladder to troubleshoot DNS, TLS, proxy errors (502/504), and container crashes.
- Backup and restore drills outline precise schedules and verification steps for PostgreSQL, MariaDB, and named Docker volumes.
- Maintenance cadence establishes a routine for OS patches, Docker engine updates, and image rebuilds, each with its own safety step, such as a provider snapshot before Docker Engine updates.
How it works
- Context input: provide your app stack, OS, provider, and current error state.
- Runbook generation: produces a tailored manual with copy-paste commands marked for state changes.
- Emergency triage: if you report an outage, it puts the triage ladder and recovery steps before baseline setup.
- Context refinement: follows the initial delivery with specific questions to tune the RTO, RPO, and hardware-specific configurations.
Frameworks & tools
This skill is designed for Ubuntu/Debian or RHEL-family VPS hosts running Docker Engine and the Coolify management dashboard. It supports apps built via Dockerfile, Docker Compose, or Nixpacks, with specific logic for PostgreSQL, MySQL, and MariaDB databases.
Why this beats prompting it yourself
Generic AI prompts often suggest docker system prune -a or other destructive commands that wipe your ability to roll back. This skill enforces a safety-first order of operations, ensuring you have verified off-server backups before anything destructive, and that every command that deletes, overwrites or restarts comes after the check that proves it is needed.
Use cases
- Set up a new VPS with Coolify for production-grade reliability.
- Troubleshoot a "502 Bad Gateway" or "Connection Refused" error in a Docker stack.
- Create a recurring maintenance and backup schedule for a small agency server.
- Harden a default cloud image to prevent password brute-force attacks.
Known limitations
Does not support Kubernetes or managed PaaS providers without shell access. Commands are written for Debian/Ubuntu and RHEL-family Linux hosts. Requires root or sudo privileges on the target server.
No support for Kubernetes or managed PaaS. Requires root/sudo shell access on Linux. Commands tailored for Debian/Ubuntu and RHEL families only.
How to use
- 1
Connect your agent to the Agensi MCP
Copy the prompt and paste it into Claude, ChatGPT, Cursor or any agent that supports MCP. It sets up the connection for you. Manual setup
- 2
Sign in to Agensi
Your agent shows a sign-in link. Sign in and click Allow.
- 3
Ask for what you need
Your agent loads this skill and uses it on your task. It always asks before any payment.
Reviews
No reviews yet
Be one of the first to try it. Every listed skill goes through the trust checks below.
Security scanned
Checked by our 8-point scan before listing
Fresh listing
Recently published to Agensi
Money-back guarantee
Doesn't do what its page promises? You get your money back.
Trust & safety
Security scan
Security scan passed 12 days ago
Permissions
No special permissions declared or detected