Documentation
Everything you need to deploy, manage, and scale apps on Dockhold.
Quick Start
Deploy your first app in three steps. No Dockerfile required – we auto-detect Python and Node.js projects.
Connect
Link your GitHub account. We auto-detect your project type and generate the right container config.
Configure
Store your API keys securely in the Vault. They're encrypted and injected into your runtime automatically.
Deploy
Push to main. We build your container and launch it in a secure, isolated runtime. Your app gets a public HTTPS endpoint automatically.
GitOps & Auto-Deploy
Enable automatic deployments whenever you push to your GitHub repository. No manual deploys needed – just git push and we handle the rest.
How It Works
Push
You push code to your main branch
Webhook
GitHub notifies Dockhold
Build
We build your new container
Deploy
Zero-downtime rolling update
Setup Instructions
Enable Auto-Deploy
Go to your app's GitOps settings from the dashboard.
Copy Webhook URL & Secret
Copy the webhook URL and secret from your GitOps settings.
https://api.dockhold.eu/webhooks/githubwhsec_xxxxxxxxxxxxxxxxAdd Webhook in GitHub
Go to your repo → Settings → Webhooks → Add webhook
Security
Webhook payloads are cryptographically verified using secure signatures. We validate every request before triggering a build – no one can deploy to your app without your secret.
Secrets
Store API keys, tokens, and other sensitive data in the Secret Vault. Secrets are encrypted at rest with a key unique to your account. Only the specific apps you choose will ever have access — other apps remain completely isolated.
How It Works
Add Secret
Go to Settings → Secrets and store any API key or credential
Encrypted
Stored with industry-standard encryption, isolated to your account
Bind to Apps
Explicitly bind each secret to the apps that need it, under any env var name
Injected
Available as environment variables only in the apps you chose
Using Secrets in Code
Bound secrets appear as standard environment variables in your app. Access them the same way you would any env var:
import os
# Secrets you've bound to this app are available as env vars
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
# Use them in your app
from openai import OpenAI
client = OpenAI(api_key=OPENAI_API_KEY)Audit Trail
All vault access is recorded in a tamper-evident, hash-chained audit trail. We integrity-check the chain nightly and alert immediately on any platform-administrator access to your secrets.
Memory & Persistence
Every app deployed on Dockhold gets a dedicated, private PostgreSQL database automatically provisioned in your isolated environment. This is your app's long-term memory.
Access your database using the DATABASE_URL environment variable:
import os
import psycopg2
# Dockhold injects this automatically - do not hardcode!
DATABASE_URL = os.getenv("DATABASE_URL")
# Connect to your app's dedicated database
conn = psycopg2.connect(DATABASE_URL)
# Store conversation history, embeddings, state, etc.
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS memory (
id SERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ DEFAULT NOW(),
content JSONB
)
""")LangChain Integration
If you're using LangChain, connect your app's memory directly:
import os
from langchain.memory import PostgresChatMessageHistory
from langchain.chains import ConversationChain
DATABASE_URL = os.getenv("DATABASE_URL")
# Persistent conversation memory
history = PostgresChatMessageHistory(
connection_string=DATABASE_URL,
session_id="user_123"
)
# Your app now remembers everything
chain = ConversationChain(memory=history, ...)What Persists
✓ Survives Restarts
- • Chat history & conversations
- • Vector embeddings
- • App state & checkpoints
- • User data your app stores
✗ Does Not Persist
- • In-memory Python variables
- • Files written to /tmp
- • Runtime cache
App Capabilities Pack
Every app deployed on Dockhold comes with built-in capabilities for vector storage, request tracing, managed secret bindings, and scheduled tasks. Useful for any production workload — and especially handy if you're building AI features on top.
Vector Memory (pgvector)
Your app's database includes the pgvector extension for storing and querying embeddings. Perfect for RAG, semantic search, and long-term memory.
import os
import psycopg2
from openai import OpenAI
DATABASE_URL = os.getenv("DATABASE_URL")
# Also available: VECTOR_STORE_URL (alias for clarity)
conn = psycopg2.connect(DATABASE_URL)
cursor = conn.cursor()
# pgvector is pre-installed - just use it!
cursor.execute("""
CREATE TABLE IF NOT EXISTS agent_memory (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536), -- OpenAI ada-002 dimensions
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
)
""")
# Create HNSW index for fast similarity search
cursor.execute("""
CREATE INDEX IF NOT EXISTS memory_embedding_idx
ON agent_memory USING hnsw (embedding vector_cosine_ops)
""")
# Store a memory with embedding
def remember(content: str, metadata: dict = None):
client = OpenAI()
response = client.embeddings.create(
model="text-embedding-ada-002",
input=content
)
embedding = response.data[0].embedding
cursor.execute(
"INSERT INTO agent_memory (content, embedding, metadata) VALUES (%s, %s, %s)",
(content, embedding, json.dumps(metadata or {}))
)
conn.commit()
# Retrieve similar memories
def recall(query: str, limit: int = 5):
client = OpenAI()
response = client.embeddings.create(
model="text-embedding-ada-002",
input=query
)
query_embedding = response.data[0].embedding
cursor.execute("""
SELECT content, metadata, 1 - (embedding <=> %s::vector) as similarity
FROM agent_memory
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_embedding, query_embedding, limit))
return cursor.fetchall()PGVector from langchain_community directly with your DATABASE_URL.Chain of Thought Tracing
Log your app's reasoning steps to understand and debug its decision-making process. View traces in the dashboard with a visual timeline.
First, create the traces table in your database (run once at startup):
-- Add this to your database initialization
CREATE TABLE IF NOT EXISTS agent_traces (
id SERIAL PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
step_type VARCHAR(50) NOT NULL, -- thinking, tool_call, llm_call, error
content JSONB NOT NULL, -- {"input": "...", "output": "..."}
duration_ms INTEGER,
tokens_used INTEGER,
model VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_traces_session ON agent_traces(session_id);
CREATE INDEX IF NOT EXISTS idx_traces_created ON agent_traces(created_at);Then log traces from your app code:
import os
import psycopg2
import json
import uuid
DATABASE_URL = os.getenv("DATABASE_URL")
conn = psycopg2.connect(DATABASE_URL)
class AgentTracer:
def __init__(self, session_id: str = None):
self.session_id = session_id or str(uuid.uuid4())
def log_step(self, step_type: str, input_data: str, output_data: str,
model: str = None, tokens: int = 0, duration_ms: int = 0):
"""Log a reasoning step to the traces table"""
cursor = conn.cursor()
cursor.execute("""
INSERT INTO agent_traces
(session_id, step_type, content, model, tokens_used, duration_ms)
VALUES (%s, %s, %s, %s, %s, %s)
""", (
self.session_id, step_type,
json.dumps({"input": input_data, "output": output_data}),
model, tokens, duration_ms
))
conn.commit()
# Usage in your app
tracer = AgentTracer()
# Log thinking
tracer.log_step(
step_type="thinking",
input_data="User asked: What's the weather in Berlin?",
output_data="I need to call a weather API to answer this question"
)
# Log tool call
tracer.log_step(
step_type="tool_call",
input_data="get_weather(location='Berlin')",
output_data='{"temp": 15, "condition": "cloudy"}',
duration_ms=230
)View your traces in the dashboard by clicking Traces on any running app.
Managed Secret Bindings
Store your API keys in the Vault once, then bind them to the specific apps that need them — under any environment variable name you choose. One secret can be shared across multiple apps, and each app only sees the secrets you explicitly bound to it.
Example: bind OPENAI_API_KEY in your Vault to multiple apps:
import os
# Secrets bound to this app are available as standard env vars
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # Bound in Settings → Secrets
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") # Bound in Settings → Secrets
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY") # Bound in Settings → SecretsScheduled Tasks (Cron)
Schedule recurring tasks for your app using cron expressions. Perfect for daily reports, periodic data syncs, or maintenance routines.
Creating a Scheduled Task
- Go to your app in the Dashboard
- Click Tasks to open the scheduler
- Click New Task
- Enter a name, cron schedule, and the command to run
# Format: minute hour day month weekday
* * * * * # Every minute
*/5 * * * * # Every 5 minutes
0 * * * * # Every hour (at minute 0)
0 9 * * * # Daily at 9:00 AM
0 0 * * 0 # Weekly on Sunday at midnight
0 0 1 * * # Monthly on the 1st at midnightTasks execute HTTP calls to your app's endpoints. Example command:
curl -X POST http://localhost:8080/api/daily-summary/api/cleanup or /api/sync.Monitoring & Metrics
Every running app displays live resource and activity metrics directly in your dashboard. No setup required — metrics appear automatically.
Resource Usage
See real-time CPU and memory consumption with visual progress bars. Limits are set by your plan tier.
Current usage vs. tier limit in millicores
RAM usage vs. tier limit in bytes
Activity Stats
If your app uses the Traces API, you'll also see session counts, success rates, average response times, token usage, and tool call counts.
Deploy History
Track total deployments and last build duration. The dashboard polls every 15 seconds to keep everything up to date.
Runtime Environment
Dedicated Compute
Apps run on dedicated performance tiers, not shared serverless functions. Your process can run indefinitely without cold starts or timeouts.
| Tier | CPU | Memory | Storage |
|---|---|---|---|
| Hobby (Free) | 0.25 vCPU (shared) | 512 MB | 2 GB |
| Pro | 1 vCPU | 1 GB | 10 GB |
| Scale | 2 vCPU | 4 GB | 20 GB |
Network Access
Every app has full outbound internet access on every tier — LLM APIs, third-party services, package registries, anything reachable over HTTPS. Apps cannot reach the control plane or other tenants' apps; isolation is enforced at the network layer.
Always-On Infrastructure
All Tiers - Always Running
Your apps run 24/7. No sleep. No cold starts. Peace of mind that your app is always available - unlike running on your laptop.
Environment Variables
The following environment variables are automatically injected into every app:
# Automatically injected by Dockhold
PORT=8080 # Your app MUST listen on this port
DATABASE_URL=postgres://... # Your dedicated database
# From your Secret Vault (encrypted at rest)
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
# Custom env vars from Dashboard
MY_CUSTOM_VAR=valueProject Structure
Supported Runtimes
Python
Latest stable (slim)
Detected via requirements.txt or pyproject.toml
Node.js
LTS (Alpine)
Detected via package.json
Minimal Python Project
my-app/
├── main.py # Entry point (must exist)
├── requirements.txt # Python dependencies
└── README.md # OptionalMinimal Node.js Project
my-app/
├── index.js # Entry point
├── package.json # Node dependencies + start script
└── README.md # OptionalCustom Dockerfile (Eject)
Need full control? Add a Dockerfile to your repo root. Your Dockerfile always takes priority – we never override it.
# Custom Dockerfile - full control
FROM python:3-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Dockhold injects PORT=8080
CMD ["python", "main.py"]Example Dockerfile
A minimal Dockerfile for a Python app:
FROM python:3-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy source code
COPY . .
# Dockhold injects PORT=8080
EXPOSE 8080
# Start your app
CMD ["python", "main.py"]Listening on PORT
Your app must expose an HTTP server on the PORT environment variable (default: 8080).
import os
from flask import Flask
app = Flask(__name__)
@app.route("/")
def health():
return {"status": "online", "app": "my-app-v1"}
@app.route("/chat", methods=["POST"])
def chat():
# Your app logic here
return {"response": "Hello from Dockhold!"}
if __name__ == "__main__":
port = int(os.getenv("PORT", 8080))
app.run(host="0.0.0.0", port=port)/ or /health. We use this to verify your app is running.Build Limits
To ensure fast builds and efficient resource usage, each tier has specific build constraints:
| Tier | Max Image Size | Build Timeout |
|---|---|---|
| Hobby (Free) | 100 MB | 5 minutes |
| Pro | 300 MB | 10 minutes |
| Scale | 1 GB | 20 minutes |
python:3-slim, node:lts-alpine), multi-stage builds, and --no-cache-dir for pip installs. Avoid including large ML models in your image – load them at runtime instead.Need help? Join our Discord community or email support@dockhold.eu