# Dockhold: Full Guide > The expanded, single-file version of https://dockhold.eu/llms.txt, with the > full text of every deploy recipe inline. If you only need the map and the > canonical steps, fetch llms.txt instead. This document is self-contained: an AI > coding tool can deploy an app to Dockhold using only what is below. > > Maintainers: this file mirrors the docs and recipe pages under > apps/marketing/src/pages/docs/. When a recipe's command or config changes, > update it here in the same change — this file does not auto-generate. Dockhold puts an app online at a live, always-on, secure HTTPS URL. Two deploy paths: connect a GitHub repo and push to your main branch, or run `npx dockhold deploy` in a local folder (no GitHub, no git). No servers to manage. Works for apps you wrote and apps your AI tool generated. ## The deploy model 1. Push your app to a GitHub repository. 2. Make the app listen on `0.0.0.0` and read its port from the `PORT` environment variable. Do not hardcode a port or bind to `localhost`. 3. Connect the repo at https://app.dockhold.eu. Dockhold auto-detects common stacks (Node, Python, Go, Rust, Ruby, Deno, Bun, Java, PHP) and builds them — no Dockerfile required. To control the build, add a `Dockerfile` at the repo root (it takes priority), or a `dockhold.json` at the root that points to one: `{"build":{"dockerfile":"path/to/Dockerfile"}}`. 4. Read all configuration from environment variables. Set plain values in the dashboard; set secrets (API keys, tokens, passwords) in the Vault, where they are encrypted and injected at runtime, never stored in your repo. 5. Need a database? Enable the managed Postgres add-on (a checkbox when you create the app, or Settings → Managed database on an existing app). Dockhold injects `DATABASE_URL`. Do not run your own database. 6. Push to your main branch to deploy. The app goes live at `https://.dockhold.app`; later pushes redeploy automatically. ## Deploy from your computer (CLI, no GitHub) Use when the app's code exists only as a local folder (no git remote). Requires Node.js >= 18. Free accounts included. 1. `npx dockhold login` from any terminal. A browser opens once to authorize the CLI on the Dockhold account. Headless machines: create an API token in the dashboard (Settings), then `dockhold login --token` and paste it at the prompt (never pass the token inline; the prompt keeps it out of history). 2. `cd` into the project folder and run `npx dockhold deploy`. The CLI packs the folder, uploads it over a one-time secure link, and the standard build pipeline takes over: a Dockerfile at the folder root wins; otherwise the stack is auto-detected (Node, Python, Go, Rust, Ruby, Deno, Bun, Java, PHP). The command prints the live URL: `https://.dockhold.app`. 3. Packing rules: `.git`, `node_modules`, `.dockhold`, and every `.env*` file are ALWAYS excluded; `.gitignore` is respected; a `.dockholdignore` file (same syntax) excludes more and takes priority. Upload cap: 500 MB after packing. Over the cap: exclude build artifacts and dependency folders, retry. 4. Flags on deploy: `--name ` (app name; else derived from the folder), `--env KEY=VALUE` (repeatable), `--db` (managed database; the app reads `DATABASE_URL`). 5. Configuration: `.env` files never upload. Set plain values with `--env` or in the dashboard; secrets in the Vault. `PORT` and `DATABASE_URL` are injected; read them, never define them. The app must listen on `0.0.0.0:$PORT`. 6. New version = run `npx dockhold deploy` again from the same folder. No git auto-deploy exists for these apps; dashboard Redeploy restarts the current version only. 7. Other commands: `dockhold list`; `dockhold logs [--app ] [--tail ] [--type app|build|db]`; `dockhold open [--app ]`. 8. Failure handling: an interrupted upload leaves nothing behind, rerun deploy; a "no detected stack" build failure means add a Dockerfile at the folder root; a URL that times out after a green build means the app is not listening on `0.0.0.0:$PORT`. ## The one rule that matters: listen on 0.0.0.0:$PORT Your app must listen on `0.0.0.0` and read its port from the `PORT` environment variable — never `localhost`, never a hardcoded port. Dockhold assigns `PORT` at runtime; an app that ignores it can't receive traffic. This is the single most common reason a deploy builds successfully but the URL times out. ## Rules for generated apps - Bind to `0.0.0.0` and the `PORT` env var — never `localhost` or a fixed port. - `PORT` and `DATABASE_URL` are set by Dockhold. Read them; never define or override them. - Apps are stateless: the container filesystem is ephemeral and lost on every restart and deploy. Persist data in the managed database, not on local disk. - Pick the deploy path by where the code lives. In a GitHub repo: connect it in the dashboard, then push to the main branch. Only local (no remote): run `npx dockhold login` then `npx dockhold deploy` from the folder. These are the only two deploy paths. ## Configuration and secrets - Plain config (feature flags, public URLs): set in the dashboard, read from the environment. - Secrets (API keys, tokens, passwords): set in the Vault. They are encrypted and injected into the app at runtime, never stored in the repo. - Dockhold does not read a committed `.env` — set everything in the dashboard. - Frontend frameworks inline browser-visible variables into the bundle when the app is built (Vite: `VITE_` prefix; Next.js: `NEXT_PUBLIC_` prefix), and the build does NOT receive dashboard or Vault variables — so setting one in the dashboard does nothing at build time. For a Vite single-page app (no server), configure it from the dashboard with runtime config: write the values into the page when the container starts and read `window.__APP_CONFIG__`. For Next.js, server code reads dashboard variables at runtime; only browser values need the `NEXT_PUBLIC_` prefix and must be set in code (e.g. `next.config.js`), for public values only. Never put a real secret behind a public prefix or in a committed file — anything shipped to the browser is public. ## Managed database Enable a managed Postgres database for an app and Dockhold provisions a private database in the app's isolated environment and injects its connection string as `DATABASE_URL` (plus `VECTOR_STORE_URL`, the same database, for vector search). Read `DATABASE_URL`; never define or override it. The database persists across restarts and deploys; the app's own filesystem does not. Every managed database ships with `pgvector` enabled, so embeddings / RAG / semantic search work with no extra setup. ## Import an existing database One-time seed of the managed database from a dump the user uploads. Requires the managed database to be on the app AND a paid plan (any account with at least one unit of compute). It is a load, not a sync or a backup. 1. Dump the source database: `pg_dump -Fc --no-owner --no-privileges yourdb > backup.dump`. The `-Fc` custom format is compact so more data fits under the cap; `--no-owner --no-privileges` drop ownership/grant statements the app's limited database user cannot run. Plain `.sql` and gzipped `.sql.gz` are also accepted. 2. Upload from the app's Database tab → Import data. The upload cap is 2 GB after compression. 3. On success the Database tab (the overview) refreshes to show the imported tables — that is the verification surface. The restore is all-or-nothing: it applies completely or leaves the database exactly as it was, and shows the error on failure. So import into an empty database — a table already present makes it fail cleanly rather than merge or overwrite. One import runs at a time per app. Ready-made template: https://github.com/dockhold/directory-starter is a browsable, searchable directory backed by the managed database. It seeds ~50 sample entries on first boot so the deploy is live immediately, then you replace them with your own via a one-time import (the repo ships a CSV-to-dump helper and a step-by-step recipe). Deploy it with the managed database enabled. --- # Recipes Each recipe is a complete path from a GitHub repo to a live URL for one stack. ## Deploy a Vite + React app A Vite app compiles to static files in `dist/`. Use a small Dockerfile that builds those files and then serves them on the assigned port — it pins exactly how the app builds and starts. Ready-made template: https://github.com/dockhold/vite-react-starter 1. Add the `serve` static server and a start script. ``` npm install --save-dev serve ``` ```json { "scripts": { "build": "vite build", "start": "serve -s dist -l $PORT" } } ``` `serve -s dist` serves the built app with single-page-app fallback (so client-side routes work); `-l $PORT` binds the assigned port on all interfaces. 2. Add a Dockerfile at the repo root: ``` FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine WORKDIR /app RUN npm install -g serve@14 COPY --from=build /app/dist ./dist CMD serve -s dist -l $PORT ``` A Dockerfile at the repo root takes priority. Without one, the build system may serve the project with its own static server that does not bind the assigned port (you'll get a blank page serving raw source) — so include it. 3. Connect the repo at https://app.dockhold.eu/new. Dockhold builds from your Dockerfile and runs it. 4. Configuration: a static SPA has no server, so it can't read dashboard variables directly. To configure it from the dashboard (e.g. an API URL), use runtime config — write the values into the page when the container starts and read `window.__APP_CONFIG__` (see the full-stack recipe and the fullstack-web template). Don't bake config into the build with a committed `.env.production`, and never put a secret in browser code — anything shipped to the browser is public. Troubleshooting: blank page and the browser fetches `/src/main.jsx` → the un-built source is being served; add the Dockerfile so the app is built first. 404 on refresh → keep the `-s` flag (SPA fallback). ## Deploy a Next.js app Dockhold runs Next.js as a real Node server, so server components, API routes, and SSR all work — no static export needed. 1. Make the start script bind the assigned port and all interfaces. ```json { "scripts": { "build": "next build", "start": "next start -H 0.0.0.0 -p $PORT" } } ``` Do not set `output: 'export'` in `next.config.js` unless you specifically want a static-only build — it disables API routes and server rendering. 2. Connect the repo at https://app.dockhold.eu/new. Dockhold detects Next.js, runs `npm install` and `npm run build`, then `npm run start`. No Dockerfile needed. 3. Environment variables: set them in the dashboard (Dockhold doesn't read a committed `.env`). Server code reads them from `process.env` at runtime; keep secrets in the Vault. Browser-visible variables need the `NEXT_PUBLIC_` prefix and are baked in at build — the build can't see dashboard variables, so set those in code (e.g. `next.config.js`), for public values only. 4. Database (optional): enable the managed Postgres add-on and read `process.env.DATABASE_URL` in server code. Troubleshooting: URL times out → the start script must include `-H 0.0.0.0 -p $PORT`. API routes 404 → you enabled `output: 'export'`. ## Deploy a static site (HTML/CSS/JS) If your project is just files, you only need a small server to serve them on the assigned port. 1. Add a one-line static server. If there's no `package.json`, run `npm init -y` first. ``` npm install --save-dev serve ``` ```json { "scripts": { "start": "serve -l $PORT ." } } ``` `serve -l $PORT .` serves the current folder on the assigned port. If your files live in a subfolder, point at it: `serve -l $PORT public`. 2. Connect the repo at https://app.dockhold.eu/new. Dockhold detects the Node project, runs `npm install`, and starts it with `npm run start`. Troubleshooting: 404 at root → the served folder must contain `index.html`. Assets 404 → use relative asset paths (`./style.css`), not absolute local paths. ## Deploy a Node / Express API An Express API is already a long-running server; it just needs to listen on the assigned port. ```js const express = require("express"); const app = express(); app.get("/health", (_req, res) => res.send("ok")); const port = process.env.PORT || 3000; app.listen(port, "0.0.0.0", () => { console.log(`listening on ${port}`); }); ``` ```json { "scripts": { "start": "node server.js" } } ``` Read config and secrets from `process.env`. For a database, enable the managed Postgres add-on and read `DATABASE_URL`: ```js const { Pool } = require("pg"); const pool = new Pool({ connectionString: process.env.DATABASE_URL }); ``` Connect the repo at https://app.dockhold.eu/new. Dockhold detects the Node project, runs `npm install`, and starts it with `npm run start`. Troubleshooting: URL times out → pass `"0.0.0.0"` to `app.listen` and use `process.env.PORT`. CORS errors → set the allowed origin from an env var, not a hardcoded `localhost`. ## Deploy a Python / FastAPI app For Python the most reliable, reproducible path is a minimal Dockerfile. `main.py`: ```python from fastapi import FastAPI app = FastAPI() @app.get("/health") def health(): return {"status": "ok"} ``` `requirements.txt`: ``` fastapi uvicorn[standard] ``` `Dockerfile` at the repo root: ```dockerfile FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # Shell form so $PORT is expanded at runtime CMD uvicorn main:app --host 0.0.0.0 --port $PORT ``` A Dockerfile at the repo root takes priority and tells Dockhold exactly how to build and run. The `CMD` is in shell form on purpose — that's what lets `$PORT` expand. Read config/secrets with `os.environ`. For a database, enable the managed add-on and read `os.environ["DATABASE_URL"]`. Connect the repo (with the Dockerfile) at https://app.dockhold.eu/new; Dockhold builds from your Dockerfile and runs it. Troubleshooting: URL times out → `CMD` must use `--host 0.0.0.0 --port $PORT` in shell form (no `[ ]` brackets). Import error → `main:app` means a variable `app` in `main.py`; match your file/variable names. ## Deploy a Lovable export Lovable exports a standard Vite + React project, so this is the Vite path plus two export-specific steps. 1. In Lovable, connect the project to GitHub so the code lives in a repo you own. 2. Add the `serve` start script and the Dockerfile from the Vite recipe above (build `dist/`, then `serve -s dist -l $PORT`). The Dockerfile matters: an exported SPA otherwise gets served as raw source and shows a blank page. 3. Carry over the Supabase config (project URL + anon key). The export reads them from `import.meta.env` at build time, which means baking them into the repo. The cleaner path on Dockhold: set them as dashboard variables and read them at runtime from `window.__APP_CONFIG__` (see the full-stack recipe and the fullstack-web template), so you can change or rotate them with no rebuild. A Supabase anon key is designed to be public, so it's safe in the browser; a service-role key or any other real secret must stay on a server, never in the frontend bundle. 4. Connect the repo at https://app.dockhold.eu/new and deploy. Troubleshooting: blank page → add the Dockerfile so the app is built before it's served. Supabase calls fail → the URL or anon key isn't reaching the app; set them as dashboard variables, read the runtime values (`window.__APP_CONFIG__`), and restart. ## Deploy a Bolt.new export Bolt.new builds different kinds of app, so identify the stack first. 1. Export the project to GitHub (or download and push it). 2. Open `package.json`: - Has `vite` → Vite SPA: add `"start": "serve -s dist -l $PORT"` and the Dockerfile from the Vite recipe (build, then serve `dist/`). - Has `next` → follow the Next.js recipe: `"start": "next start -H 0.0.0.0 -p $PORT"`. - Plain Node/Express → `app.listen(process.env.PORT, "0.0.0.0")`. 3. Move any value hardcoded in the sandbox (API base URL, key) out of the code and set it in the dashboard. A Node/Next.js server reads `process.env` at runtime. A Vite SPA has no server, so use runtime config: write the values into the page at container start and read `window.__APP_CONFIG__` (see the full-stack recipe). Keep real secrets out of the browser bundle. 4. Connect the repo at https://app.dockhold.eu/new and deploy. Troubleshooting: "no start script" → Bolt sandboxes use a `dev` script; production needs a `start` script. Requests go to localhost → replace hardcoded `http://localhost` with an env var. ## Connect a Postgres database 1. Enable the database: on the new-app page (https://app.dockhold.eu/new) check "Add a managed database," or on an existing app go to Settings → Managed database → Add database. 2. Dockhold provisions a dedicated database and injects `DATABASE_URL` (and `VECTOR_STORE_URL`). Read `DATABASE_URL`; never define or override it. ```js // Node (pg) const { Pool } = require("pg"); const pool = new Pool({ connectionString: process.env.DATABASE_URL }); ``` ```python # Python (psycopg) import os, psycopg conn = psycopg.connect(os.environ["DATABASE_URL"]) ``` Most ORMs (Prisma, Drizzle, SQLAlchemy) read `DATABASE_URL` directly. Run your migrations on startup — the database is empty when first provisioned and your app owns its schema. The database persists; the app filesystem does not. Vector search: every managed database ships with `pgvector` enabled. Use `DATABASE_URL` as normal, or `VECTOR_STORE_URL` if a tool expects a dedicated vector-store variable — both point at the same database. Troubleshooting: `DATABASE_URL` undefined → the database isn't enabled for this app; add it and redeploy. Data disappears after deploy → it's being written to the container filesystem instead of Postgres. Ready-made: `dockhold/postgres-starter` is a CRUD API on the managed database. ## Deploy a vector database (pgvector) The managed database ships with pgvector enabled — no install. Store embeddings and run similarity search. Template: https://github.com/dockhold/pgvector-starter (works with no API key via a placeholder embedding; swap in a real model). 1. Enable the managed database (DATABASE_URL injected; pgvector pre-enabled). 2. Create a table with a vector column sized to your model: `CREATE TABLE documents (id serial primary key, content text, embedding vector(1536));` 3. Store: embed the text with your model, then `INSERT INTO documents (content, embedding) VALUES ($1, $2::vector)` (parameterized). 4. Search: embed the query and order by distance — `SELECT content FROM documents ORDER BY embedding <-> $1::vector LIMIT 5` (`<->` Euclidean, `<=>` cosine). Add an ivfflat/hnsw index for large tables. Troubleshooting: "type vector does not exist" → use the managed database's DATABASE_URL (pgvector is pre-enabled there). Dimension mismatch → the vector(N) size must equal the model's output length. Random results → embed the query with the same model as the documents. ## Deploy a full-stack app (React + API + Postgres) Production apps keep the frontend and API as separate services. On Dockhold that's two apps with two URLs (the API with its own managed database). There's no app-count limit; you just need enough compute in your pool to cover both. Templates: https://github.com/dockhold/fullstack-api and https://github.com/dockhold/fullstack-web The two apps must learn each other's URL, so deploy in two passes: 1. Deploy the API (`fullstack-api`) at https://app.dockhold.eu/new and check "Add a managed database" so `DATABASE_URL` is injected. Copy its live URL (e.g. https://fullstack-api-xxxx.dockhold.app). It's a plain Express server (no Dockerfile needed) that stores data in Postgres. 2. Deploy the web app (`fullstack-web`): it builds from a Dockerfile (build, then serve `dist/` on `$PORT`). Then set the `API_URL` dashboard variable to the API URL and restart — it's read at runtime (injected at container startup), so no rebuild. 3. On the API app, set `ALLOWED_ORIGIN` to the web app's URL and restart so the browser is allowed to call it (CORS). Locking the API down: by default the API is public (CORS restrains browsers, not scripts). To require a token, make the API a private app (Access tab → Private → mint a token; requests then need `Authorization: Bearer `). Do NOT put that token in the React app — a browser ships its code to everyone, so a token in the frontend is readable and only slows bots. To truly limit the API to your app, keep the token on a server in front of the API (a backend-for-frontend: a Next.js route handler or a small proxy that holds the token in the Vault and forwards requests), so the browser never sees it. Troubleshooting: CORS error → set `ALLOWED_ORIGIN` on the API to the exact web URL and restart. "API_URL is not set" → set the `API_URL` dashboard variable and restart the web app. API 500 on the database → enable the managed database so `DATABASE_URL` is injected. ## Deploy connected services (automatic wiring) Several web services that call each other (a frontend + an API, say) can be deployed as one connected app, where Dockhold injects each service's live URL into the services that need it. It's the automatic version of the full-stack recipe above: you don't deploy one service, copy its URL, paste it into the next, and redeploy. You declare which service needs which, and the wiring happens. How to set it up. Two ways: the dashboard, or the MCP `deploy_group` tool (so an AI coding tool can create a connected app directly — see the MCP section below). There is no push-time/webhook path yet. Dashboard steps: 1. Go to https://app.dockhold.eu/new and choose "Deploy them together." 2. Add each service: a name (e.g. `web`, `api`), its GitHub repository URL, and the port it listens on. Each service is a separate repo and gets its own URL. 3. For each environment variable that needs a sibling's URL, choose "Link a service" and pick the target. The link is stored as the token `${services..url}` and resolves to that service's `https://.dockhold.app` URL once it's live. Example: on `web`, link `API_URL` to `api`; on `api`, link `WEB_ORIGIN` to `web` (for CORS). 4. For the service that needs a database (usually the API), tick "Add a managed database." Dockhold provisions a database for that service and injects `DATABASE_URL` automatically — read it, never define it. Each database belongs to the service that enabled it; a frontend that doesn't query a database leaves the box unchecked. 5. Deploy. Builds run in parallel. The contract your code must follow: read the variable name you chose. If you link `API_URL`, your app reads `process.env.API_URL` and calls it — never hardcode a sibling's URL. Dockhold fills the value in. A service with a managed database reads `process.env.DATABASE_URL`. Operational behavior an agent should expect: - Builds run in parallel, with no ordering. A reference resolves to an `https://.dockhold.app` URL after that service deploys. - Until a linked service is live, the variable is absent (not an empty or placeholder value). A dependent may restart once, automatically, when its target comes online and the URL is injected. - One service's build failing does not fail the others. Fix and redeploy that service; the wiring completes when it's live. - The connected-app view shows each link as connected or "waiting on ." First-cut limits: web services only (no URL-less background workers in a connected app yet); one whole GitHub repository per service (no monorepo subfolders or prebuilt images here yet); each service can enable its own managed database, but a single database shared by several services isn't supported yet, and connecting your own external database is coming later. ## Deploy an MCP server Serve a remote MCP server over the Streamable HTTP transport so any MCP client can connect to a live URL. Template: https://github.com/dockhold/mcp-server-starter 1. Use the official `@modelcontextprotocol/sdk`. Expose a single `POST /mcp` endpoint backed by `StreamableHTTPServerTransport` in stateless mode (a fresh `McpServer` per request; no shared session state). Register tools with `server.registerTool(name, { title, description, inputSchema }, handler)`. Listen on `0.0.0.0:$PORT`. 2. Connect the repo at https://app.dockhold.eu/new — it's a plain Node server, so no Dockerfile is needed. It goes live with the MCP endpoint at `/mcp`. 3. Connect a client over Streamable HTTP at `https://.dockhold.app/mcp`: MCP Inspector (`npx @modelcontextprotocol/inspector`), or Claude Code (`claude mcp add --transport http /mcp`), or any client's remote HTTP MCP option. Locking down: the endpoint is public by default. To require a token, make it a private app (Access tab → token); clients that support custom headers then send `Authorization: Bearer `. Never expose a tool that acts on secrets without auth. Troubleshooting: client can't connect → use the Streamable HTTP transport and a URL ending in `/mcp`. 406 → the client must send `Accept: application/json, text/event-stream` (real clients do). Times out → must listen on `0.0.0.0:$PORT`. ## Deploy from your AI tool (MCP) This is the reverse of the previous recipe: instead of hosting your own MCP server, you connect your AI coding tool (Claude, Cursor, any MCP client) to Dockhold's own remote MCP server and let it deploy and manage your apps for you. 1. Create an API token in the dashboard at https://app.dockhold.eu/settings/api-tokens. It starts with `dh_mcp_` and is shown once. Choose the access it needs: read-only (list apps and repos, read status/logs/resource usage) or read+deploy (also deploy, restart, set variables, resize). Optional: set an expiry and an IP allow-list. 2. Add Dockhold to your tool's MCP configuration, pointing at `https://api.dockhold.eu/mcp` and passing the token as a bearer header: { "mcpServers": { "dockhold": { "url": "https://api.dockhold.eu/mcp", "headers": { "Authorization": "Bearer dh_mcp_live_…" } } } } 3. The tool can then call these. Read: `list_apps`, `get_app_status`, `get_app_logs`, `list_github_repos`, `get_resource_usage`. Read+deploy: `deploy_app`, `redeploy_app`, `set_app_variable`, `deploy_group`, `resize_app`, `resize_database`, `resize_database_storage`. - `deploy_app` takes a GitHub repo URL and a name; poll `get_app_status` until the URL is live. Public repo: `repo_url` + `name` only. PRIVATE repo: call `list_github_repos` first (returns each connected repo with its `installation_id`), then pass `repo_url` + `name` + `github_installation_id`. Deploying a private repo turns auto-deploy on (future pushes redeploy it). - `resize_app` sets an app's memory (`app_id`, `memory_mb`) — use it when an app OOMs. `resize_database` sets a managed DB's memory (`db_ram_mb`; the DB restarts briefly). `resize_database_storage` grows a managed DB's disk (`storage_gb`; grow-only). Call `get_resource_usage` first for the valid sizes (its `*.steps_*` arrays) and to confirm the pool has room; a size past your pool is refused, same as the dashboard. - `deploy_group` deploys several connected services at once: pass a `services` map (each: `source` repo URL, `port`, optional `type:"web"`, optional `env`, optional `db:"enable"`); wire services by setting an env value to exactly `${services..url}`; give a service its own database with `db:"enable"` (read `DATABASE_URL`). It returns the group's services with their app ids; poll `get_app_status` on each. Public repos only. (`deploy_group` and `list_github_repos` are only offered when the instance has those features enabled.) Security: a token only does what you could do in the dashboard, on your own apps, within your plan — no cross-account access, no billing or account changes, and a read-only token can't deploy. Secrets still go in the Vault (the tool can set plain config variables, but secret-looking keys are refused). Revoke a token on the same settings page and it stops working immediately. Transport: the endpoint speaks JSON-RPC 2.0 over the MCP Streamable HTTP transport. Clients must send `Accept: application/json, text/event-stream`. ## Deploy n8n Self-host your own n8n automation instance. Template: https://github.com/dockhold/n8n-starter License note: n8n is under the Sustainable Use License (fair-code, not standard open source). You may run it for your own internal/business use and build workflows for clients; you may NOT offer it as a paid service, white-label it, or charge others for access without a commercial license from n8n. 1. Give it a larger app size, e.g. 1 GB (n8n needs more memory + a larger image than the free 256 MB), and enable the managed database — the template maps the injected `DATABASE_URL` to n8n's Postgres settings. Without the database, n8n stores everything on the ephemeral disk and loses it on restart. 2. Set `N8N_ENCRYPTION_KEY` to a stable secret in the Vault before saving any credentials (else they become unrecoverable on restart). 3. After deploy, set `N8N_HOST=.dockhold.app` and `WEBHOOK_URL=https://.dockhold.app/`, then restart, and create your owner account. Troubleshooting: data vanishes after deploy → enable the managed database and set a stable `N8N_ENCRYPTION_KEY`. Login redirect loops → set `N8N_HOST` / `WEBHOOK_URL` to the real https URL. Won't start on the free 256 MB → resize the app to 1 GB or more. ## Deploy a Telegram bot An always-on bot is a long-running worker. Template: https://github.com/dockhold/telegram-bot-starter 1. Get a token from @BotFather (`/newbot`). 2. Deploy the repo, then set `BOT_TOKEN` in the Vault and restart. The bot uses long polling (no webhook to configure) and also serves a small status page on `0.0.0.0:$PORT`. Until the token is set it runs idle without crashing. 3. Add commands in index.js with Telegraf (`bot.command(...)`, `bot.hears(...)`). Run a single replica — a long-polling bot scaled past one instance processes every update twice. High volume → switch to webhook mode (Telegram POSTs to your app URL on `$PORT`). Discord is the same shape: `discord.js` + a `DISCORD_TOKEN`, keep the health server. Troubleshooting: no response / "401 Unauthorized" → BOT_TOKEN missing or wrong. Replies twice → more than one instance running. --- # Troubleshooting The three failures almost every app hits on its first deploy. ## Hardcoded localhost URL Symptom: works locally, breaks deployed — network requests fail or the URL times out. Cause: `localhost` means "this same machine," and in a browser it means the visitor's machine, not your server. Two forms: - Frontend calling a hardcoded API URL. Read the base URL from config instead. A Vite SPA can't read dashboard variables at build time, so get it at runtime: ```js const API = window.__APP_CONFIG__.API_URL; // set API_URL as a dashboard variable fetch(`${API}/api/items`); ``` In Next.js, call the API from server code reading `process.env`. If frontend and API are the same app, use a relative path: `fetch("/api/items")`. - Backend binding to localhost. A server on `localhost`/`127.0.0.1` only accepts connections from inside its own container, so no outside traffic reaches it: ```js // Wrong: app.listen(3000, "localhost"); app.listen(process.env.PORT, "0.0.0.0"); ``` Rule: bind servers to `0.0.0.0:$PORT`; read every external URL from an env var (or use a relative path when it's the same app). ## Missing environment variables on deploy Symptom: `undefined` config, a crash on boot (`KeyError`, `Cannot read properties of undefined`), or a feature silently doing nothing — though it worked locally. Cause: your local `.env` file is (correctly) not committed, so its values never reach production unless you set them. Fix: set plain config as env vars in the dashboard; set secrets (API keys, tokens, passwords) in the Vault. Use the exact same names as your `.env`; a typo or case mismatch reads as missing. Use `.env.example` as the checklist. Frontend gotcha: Vite (`VITE_`) and Next.js (`NEXT_PUBLIC_`) inline browser-visible variables when the app is built, and the build can't see dashboard variables — so setting them in the dashboard does nothing. For a Vite SPA (no server), use runtime config: write values into the page at container start and read `window.__APP_CONFIG__`. For Next.js, server code reads dashboard variables at runtime; browser values need `NEXT_PUBLIC_` and must be set in code, for public values only. Never put a secret behind a public prefix — it ships to every visitor. `PORT` and `DATABASE_URL` are injected by Dockhold — read them, never define them. A missing `DATABASE_URL` means the managed database isn't enabled for the app yet. ## CORS error after deploying Symptom: browser console shows `blocked by CORS policy: No 'Access-Control-Allow-Origin' header`. The API works when called directly; only the browser, from your frontend, is blocked. Cause: browsers block cross-origin calls unless the server allows that origin; deployed, your frontend and API are on different URLs and the API isn't allowing the frontend's origin. Fix: allow the deployed frontend origin, read from an env var: ```js // Node / Express const cors = require("cors"); app.use(cors({ origin: process.env.FRONTEND_ORIGIN })); ``` ```python # Python / FastAPI import os from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=[os.environ["FRONTEND_ORIGIN"]], allow_methods=["*"], allow_headers=["*"], ) ``` Set `FRONTEND_ORIGIN` to the frontend's full URL (scheme included, no trailing slash). Don't use `origin: "*"` — it can't be combined with credentials and allows any site. Best: serve frontend + API from one origin and use a relative path, so there's no CORS at all. --- # Concepts ## Connecting GitHub Public repos deploy with no setup. A private repo needs permission to clone, and there are two ways to grant it (both work on every paid plan): 1. The Dockhold GitHub app (recommended). Install it and pick the repos to share. At build time Dockhold asks GitHub for a key that reads only those repos and expires in about an hour. Read-only, no long-lived token to manage, and auto-deploy on push is set up for you. 2. A personal access token. Create one on GitHub and store it in the Vault as `GITHUB_TOKEN`. A fine-grained token with read access to that repo's contents is enough. You add the push webhook yourself (see below). ## How deploys work Connect a repo once, then deploy by pushing to its main branch. On each push: Dockhold is notified, builds your app (auto-detecting the stack, or using your `Dockerfile` if present), and ships the new version with a zero-downtime rolling update. The app stays at the same `https://.dockhold.app` URL across deploys. Auto-deploy: with the GitHub app it is on from the start. With a personal access token, turn on auto-deploy in the app's GitOps settings, then add the push webhook it shows you on GitHub (Settings > Webhooks, `application/json`, push event). Dockhold verifies every push notification cryptographically before building. ## Runtime and limits Apps run as always-on, long-lived processes, not serverless functions. No cold starts, no request timeouts, and the app restarts itself if it crashes. Every app has full outbound internet (LLM APIs, third-party services, package registries) and is network-isolated from other tenants. You buy compute and choose a size per app; CPU rides with memory at 0.5 vCPU per 512 MB, so there is one knob. Per-app sizes: - 256 MB (free): 0.25 vCPU - 512 MB: 0.5 vCPU - 1 GB: 1 vCPU - 2 GB: 2 vCPU - 4 GB: 4 vCPU There is no limit on the number of apps, only on the size of your compute pool, so you can run many small apps or a few large ones from the same capacity. Injected into every app: `PORT` (listen on it, on 0.0.0.0) and, when the managed database add-on is on, `DATABASE_URL`. Plus your dashboard variables and bound Vault secrets. ## Instances (any paid plan) By default an app runs as a single copy. On a paid plan you can run several copies of the same app from its Size tab, so it stays online if one copy or its machine fails and incoming traffic is shared across them. Each copy reserves its own size from your compute pool (three copies of a 512 MB app use 1.5 GB), and copies are placed on different machines. Changing the number of copies rolls the app with no downtime. Free accounts run a single copy. ## Private apps (any paid plan) Flip an app to private and every request must present a valid access token, or it gets a `401` at the edge before reaching your app. - Create named tokens in the app's Access tab. The full token is shown once. - Send it as `Authorization: Bearer ` or HTTP Basic (token in the password field). Both work for HTTP, server-sent events, and WebSocket upgrades. - Tokens are stored only as hashed values with a server-side pepper and checked in constant time, so a database dump cannot be turned back into working tokens. - Optional per-token IP allow-list (CIDR, up to 32 entries), checked before the token. Brute-force attempts (more than 10 failures a minute from one IP) get `429`. - Never put a token in client-side code; a token in a browser bundle is public. ## Custom domains (any paid plan) Serve an app on a domain you own (for example `www.mysite.com`) with automatic HTTPS, instead of only the free `.dockhold.app` URL. Add a domain from the app's Domains tab; you can point several domains at one app. - Add the domain, then create the one DNS record the tab shows. A subdomain like `www` is a `CNAME` to `connect.dockhold.app`. Never hand out or hardcode an IP; `connect.dockhold.app` is the only place the address lives and it can move. - A bare apex (`mysite.com`) can't be a `CNAME` per the DNS spec. Either use `ALIAS`/`ANAME`/CNAME-flattening pointed at `connect.dockhold.app`, or run the app on `www` and turn on registrar forwarding from the apex to `www`. - Certificates are issued over the ACME HTTP challenge, so DNS must point at us before a certificate can issue (verify-then-issue). Status moves pending → verifying (issuing the cert) → live; renewal is automatic. - On Cloudflare, set the record to "DNS only" (grey cloud) or the proxy intercepts the challenge and the certificate never issues. - Zero-downtime cutover for a domain already serving real traffic: prove ownership first with a `TXT` record (`_dockhold-verify.`), let the cert issue, then switch the `CNAME`. - Domains follow the same entitlement as private apps: they keep serving through the payment grace window, and stop serving (falling back to the dockhold.app URL) if the account drops to the free plan. ## Logs and monitoring Every running app shows live CPU, memory, and deploy history in the dashboard with no setup; the view refreshes on its own. Apps that report traces through the Traces API also show session counts, success rates, average response times, token usage, and tool-call counts. ## Plans and pricing The live pricing page at https://app.dockhold.eu/pricing is the source of truth for current prices. Billed monthly in EUR; VAT may apply. You buy capacity and spread it across as many apps as you want; the limit is the size of your pool, not a number of apps. - Free allowance (every account, no card): 256 MB of compute + 10 GB of database storage, always-on runtime. Public apps only. - App size (compute): €5/month per 512 MB RAM + 0.5 vCPU. CPU rides with RAM. - Database memory: €10/month per 1 GB of dedicated database RAM. - Storage: €0.20/month per 1 GB of disk (first 10 GB free). Disk is allocated per database in 10 GB steps (10 / 20 / 30 / 50 / 100 GB) and is grow-only. - Private apps and custom domains are available on any paid plan (any account with at least one unit of compute). The managed database, GitOps auto-deploy, and the exportable audit log are available to every account, free included. - Enterprise — custom: bespoke capacity up to 64 GB RAM/CPU, priority support. Contact sales@dockhold.eu. ## Account and platform - Sign in / sign up: https://app.dockhold.eu/login (sign in with GitHub or Google; the free allowance needs no card; deploying your own private repos needs GitHub connected). - Dashboard: https://app.dockhold.eu - Status / uptime: https://stats.uptimerobot.com/UekCQqu8we - Security and trust: https://dockhold.eu/trust (EU hosting, isolation, encryption, tamper-evident audit log, subprocessors, GDPR, DPA on request) - Terms, Privacy, Impressum: https://app.dockhold.eu/legal - Hosted in the EU (Germany). --- # Docs - Quickstart: https://dockhold.eu/docs/quickstart - Deploy from your computer (CLI, no GitHub): https://dockhold.eu/docs/concepts/deploy-from-your-computer - How deploys work: https://dockhold.eu/docs/concepts/how-deploys-work - Connecting GitHub: https://dockhold.eu/docs/concepts/connect-github - Environment variables and secrets: https://dockhold.eu/docs/concepts/environment-variables - Databases and storage: https://dockhold.eu/docs/concepts/databases - Import a database: https://dockhold.eu/docs/concepts/import-a-database - Runtime and limits: https://dockhold.eu/docs/concepts/runtime - Private apps: https://dockhold.eu/docs/concepts/private-apps - Custom domains: https://dockhold.eu/docs/concepts/custom-domains - Logs and monitoring: https://dockhold.eu/docs/concepts/monitoring - Agent rules (Cursor / AGENTS.md / CLAUDE.md): https://dockhold.eu/agent-rules - All recipes: https://dockhold.eu/docs/recipes - Troubleshooting: https://dockhold.eu/docs/troubleshooting - Docs home: https://dockhold.eu/docs