Skip to main content

Self-hosting

Running the server yourself​

Self-hosting means running your Parlant agent on servers you operate, instead of on the Dashboard. The code stays the same. What changes is who looks after the surroundings: durable storage, credentials, network access, and keeping the process alive. This page takes the project your coding agent built from a laptop to a server, in the order the pieces depend on each other.

What you'll learn
  • How the production branch in your project sets durable stores
  • Where sessions, customers, and variables live, in MongoDB or your own database
  • Which environment variables and secrets the server reads
  • How to run the process in a container and scale it out
Talk this page through with your coding agent

Once your agent works locally, the next step is keeping it running on infrastructure you operate. You deploy the same parlant init project you have been developing. You also take on the responsibilities the Dashboard would otherwise handle for you: durable storage, credentials, network access, and process supervision.

Configure durable storage for production​

Your server needs a single place where it behaves differently on a server than on a laptop. In the parlant init project, that place is create_server() in config.py, the only function that branches on the environment.

In development, create_server() returns a server with in-process defaults. For production, you give it a branch that sets durable stores and turns hot reload off.

Three names in the function come from the parlant init project. services is the module that holds the backends your tools call, and _services_for picks which implementations each environment runs against. hot_reload restarts the server whenever your code changes, which is exactly right on a laptop and exactly wrong on a server:

import os

import parlant.sdk as p
import services
from dotenv import load_dotenv


def create_server() -> p.Server:
load_dotenv()
environment = p.resolve_environment() # reads PARLANT_ENV
services.use(_services_for(environment)) # the backends your tools call, chosen per environment

if environment is p.Environment.PRODUCTION:
mongo = os.environ["MONGODB_URI"]
return p.Server(
models={"*": "openrouter"},
session_store=mongo, # the three durable stores, covered next
customer_store=mongo,
variable_store=mongo,
hot_reload=False,
migrate=False, # True only for the deploy that upgrades Parlant
)

return p.Server(models={"*": "openrouter"}, log_level=p.LogLevel.DEBUG)

Note that p.resolve_environment() reads PARLANT_ENV, which is development when unset. Set it to production in the real environment of the process, not in .env. With that value in place, Parlant also switches the server to the production authorization policy and refuses to start without an admin key. We come back to that shortly.

Where state lives​

Now that the production branch exists, let's look at what it actually needs to make durable.

Parlant keeps three kinds of durable state, each behind its own store: sessions (conversations and their events), customers (records and group memberships), and variables (the values your context variables hold).

info

A context variable is a value that Parlant loads into your agent's context on every turn. It can belong to a customer, a group, an agent, or a global fallback. A plan tier is a typical example.

Each store accepts the same four options:

ValueEffect
"transient"In memory. Gone on restart.
"local"A SQLite file under the Parlant home directory. One process only.
A mongodb:// or mongodb+srv:// stringMongoDB. Use it with more than one process.
A store instanceYour own implementation.

Sessions and customers default to "local", while variables default to "transient". Set that one, or your values are gone on restart. One MongoDB connection string serves all three stores; each keeps its own collections under its own prefix.

One more option on the production branch is worth understanding: migrate. It decides whether the server may migrate the database's schema on startup after a Parlant upgrade. Parlant keeps it off by default, so that an accidental deploy of a newer version cannot rewrite your production data. Turn it on only for the deploy that is meant to.

MongoDB​

The simplest durable setup is to point all three stores at a MongoDB you operate, or at a managed one such as Atlas. The user in the connection string needs read and write access to the database named in it; Parlant creates the collections it needs on its own.

export MONGODB_URI="mongodb+srv://parlant:[email protected]/parlant"
tip

Test the production branch locally before you deploy it. Set PARLANT_ENV=production and the auth variables below, pointed at a scratch database. A misconfigured store fails at startup, not on the first customer.

Your own implementation​

Sometimes conversations must live in a database you already run. For that case, Parlant lets your coding agent implement the store interface over your database and pass the instance in.

This works because every store is a port, a component with a fixed interface that you can replace on its own. Nothing else in the server knows the difference.

p.SessionStore is the interface for sessions and their events. The outline below shows where your database client belongs. It is not usable until the required methods are implemented, and the database types and methods are supplied by your application:

import parlant.sdk as p


class MySessionStore(p.SessionStore):
"""Keeps sessions and their events in your database."""

def __init__(self, db: MyDatabase) -> None:
self._db = db

# create_session, read_session, update_session, delete_session, list_sessions,
# set_metadata, unset_metadata, commit_session_metadata, patch_event_metadata,
# upsert_labels, remove_labels,
# create_event, read_event, update_event, delete_event, list_events


async with p.Server(session_store=MySessionStore(db)) as server:
...

Of these methods, list_events is the one to make fast. It is the method every turn and every frontend read goes through, so filtering by session, trace id, kind, and minimum offset should hit an index.

Note that events are append-only and ordered by offset within a session. A delete from an offset onward must keep that ordering intact for what remains.

In practice, the customer store is the more common one to replace, because you almost certainly have a users table already.

Learn more about custom stores

Guests and customers walks through a CustomerStore over an existing database, including the guest record it must answer from code. For a store with no constructor shortcut, bind the port in configure_container; Overriding engine behavior shows how.

Environment and secrets​

Once storage is durable, the next thing your server needs is its credentials. The production process reads everything sensitive from its environment. The Dashboard sets these for a managed project; when you self-host, they are yours to set:

export PARLANT_ENV=production
export PARLANT_ADMIN_API_KEY="a-long-random-admin-key"
export PARLANT_CUSTOMER_JWT_SECRET="the-secret-your-backend-signs-with" # or PARLANT_CUSTOMER_JWKS_URL
export PARLANT_GUEST_TOKEN_SECRET="another-long-random-secret"
export PARLANT_GUEST_IDENTITY_MAX_AGE_DAYS=28 # optional; how long returning guests keep their conversations
export OPENROUTER_API_KEY="..." # or the key for whichever provider your models mapping uses
export MONGODB_URI="mongodb+srv://..."
export PARLANT_HOME=/var/lib/parlant

Here is what each secret is for. The admin key is what your backend uses to call the server. The customer secret or JWKS URL is how the server verifies the tokens your login system issues. And the guest secret signs anonymous sessions.

Learn more about auth

Authentication covers the token flow, and Authorization covers what each caller may do.

PARLANT_HOME is where the server keeps its local state: SQLite files when a store is "local", the indexing cache, logs, and diagnostic reports. It defaults to ./parlant-data in the working directory, so in a container, point it at a mounted volume.

Note that you must set PARLANT_HOME before the process starts. Parlant resolves paths at import time.

One more variable concerns what the server tells third parties about itself. By default, requests to model providers identify themselves as coming from Parlant: the word parlant is appended to the client library's User-Agent, and an X-Parlant-Client: parlant header is sent.

On OpenRouter, when OPENROUTER_HTTP_REFERER and OPENROUTER_SITE_NAME are unset, the requests also carry HTTP-Referer: https://parlant.io and X-Title: Parlant.

If your deployment must send nothing identifying to a third party, set PARLANT_CLIENT_IDENTITY=0 (false, no, and off also work).

Model provider keys belong in your secret manager. If you need per-agent credentials, use p.SecretRef (which names a secret instead of holding its value) in your model routes, and bind a p.SecretLoader (the component that fetches the value at runtime). This is far better than pasting keys into agent configuration.

tip

Model routing shows both p.SecretRef and p.SecretLoader in use.

Running the process​

With storage and secrets in place, the remaining question is how to keep the process itself running. ./run.sh from parlant init installs dependencies and runs main.py, and it works unchanged in a container. Here is a minimal image for a pip project:

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

ENV PARLANT_HOME=/var/lib/parlant
VOLUME /var/lib/parlant
EXPOSE 8800

CMD ["python", "main.py"]

Run it with the environment above and a volume for the home directory:

docker build -t my-agent .
docker run -p 8800:8800 --env-file production.env -v parlant-data:/var/lib/parlant my-agent

By default, the server listens on 0.0.0.0:8800; p.Server(host=..., port=...) changes that. Put TLS termination in front of it, a load balancer or reverse proxy, and allow your frontend's origin there.

Note that if the proxy sets forwarded headers, you should list its addresses in trusted_proxies on the auth config. Otherwise, rate limits will not see real client addresses.

Scaling out​

Once all three stores are on MongoDB, the process holds no conversation state. This means you can run several replicas behind the balancer and replace them freely.

Two things do stay per process. The first is the default model rate limiter, the component that spaces out requests to a model provider so you stay within its quota. It coordinates only one process's requests. The second is the local log file, which must not be shared.

So, bind a distributed p.RateLimiter if provider quotas matter across replicas, and give each replica its own log directory (or ship logs to a collector).

Health, logs, and telemetry​

Now that the process runs, you will want to know it is healthy, and to see what it is doing.

GET /healthz always answers 200 with a top-level status, and it is unauthenticated, so your load balancer and uptime probes work as they are. Parlant shows the per-component detail behind it only to admin callers. Health checks explains the four checks, the engine's reasons list, and how to tune the thresholds for your deployment.

Logs go to ${PARLANT_HOME}/logs/parlant.log. By default, Parlant rotates them at 10 MiB and keeps five backups. PARLANT_LOGS_DIR, PARLANT_LOG_ROTATION_MAX_BYTES, and PARLANT_LOG_ROTATION_BACKUP_COUNT change that; a max size of 0 disables rotation.

For telemetry, set the standard OpenTelemetry endpoint variables and the server exports to them with no code on your part:

export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://otel.example.com/v1/traces"
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="https://otel.example.com/v1/logs"
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="https://otel.example.com/v1/metrics"

With the logs endpoint set, your application logs go to the collector instead of the local file. Diagnostic reports, the per-turn archives you can download from /debug/reports as an admin, keep working either way, and PARLANT_ERROR_REPORTS_DIR moves them to a volume of their own.

Note that logs and reports can contain customer messages, so treat those directories as sensitive.

Checklist​

To bring it all together, here is what a production deployment should have in place:

  • PARLANT_ENV=production is set in the process environment, with the admin key, customer secret or JWKS URL, and guest secret.
  • All three stores are set on the production branch, and migrate is on only for the deploy that needs it.
  • Hot reload is off, and provider keys come from a secret manager.
  • PARLANT_HOME points at a persistent volume, one per replica.
  • TLS and CORS are handled in front of the server, and its proxy is in trusted_proxies.
  • Probes hit /healthz; logs and traces go to a collector if you run more than one replica.