> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentsdr.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Dokploy on a VPS

> Deploy AgentSDR on your own server with Dokploy: Dockerfile build, environment, domain with HTTPS, Postgres, scheduled jobs and deploy on push.

[Dokploy](https://dokploy.com) is a self-hosted platform that builds and runs Docker apps on your own VPS, with domains and HTTPS handled for you. The AgentSDR maintainers deploy this way. Use it when you want deploy-on-push instead of managing Compose by hand.

<Info>
  **What you need**

  * A VPS with Dokploy installed ([Dokploy installation docs](https://docs.dokploy.com/docs/core/installation)) and access to its dashboard.
  * A domain whose DNS you control, pointing at the server's IP address.
  * The AgentSDR repository on GitHub (the public one, or your fork).
  * About 30 minutes.
</Info>

## Overview

Dokploy builds the repository's `Dockerfile` into an image and runs it as one **Application**. It does not run `docker-compose.yml`, so three things Compose gave you are set up by hand here:

| Compose gave you | On Dokploy |
| - | - |
| `db` service | A Dokploy **Postgres** database service, or an external PostgreSQL 16+. |
| `setup` service (creates the schema) | You run `bun scripts/db/setup.ts --if-empty` once inside the app container. |
| `cron` service | Dokploy **Schedule Jobs**, or an external cron. |

```mermaid theme={null}
flowchart LR
  gh[GitHub push] --> dok[Dokploy builds the Dockerfile]
  dok --> app[AgentSDR app container]
  app --> pg[(Postgres service)]
  sched[Schedule Jobs] --> app
  traefik[Traefik + Let's Encrypt] --> app
```

<Warning>
  Run exactly one replica of the app. The outreach sender loop has no distributed lock; two instances would each send.
</Warning>

<Steps>
  <Step title="Create the Postgres database">
    In your Dokploy project, create a service of type **Postgres** and deploy it. Make sure its Docker image is PostgreSQL 16 or newer (the database Advanced settings let you set a custom Docker image), and set a strong password (`openssl rand -hex 32`). On the database's page, copy the **Internal** connection details: the internal host name, port, user, password and database name. Use the internal credentials, not the external ones: the app runs in the same network, and the Dokploy docs advise against exposing the database publicly ([connection docs](https://docs.dokploy.com/docs/core/databases/connection)).

    Your connection string is:

    ```text theme={null}
    postgres://USER:PASSWORD@INTERNAL_HOST:5432/DATABASE
    ```

    Alternatively use any external PostgreSQL 16+ (a hosted one) and skip this step. The database must be **empty**.
  </Step>

  <Step title="Create the application from GitHub">
    In the same project, create a service of type **Application**. In its **General** tab choose your Git provider (GitHub, after connecting Dokploy to your GitHub account; a plain **Git** URL also works for a public repository), select the repository, and set the branch to `main` (or the branch you deploy). Save.
  </Step>

  <Step title="Set the build type to Dockerfile">
    Still in **General**, under **Build Type** choose **Dockerfile** and set:

    * **Dockerfile Path**: `Dockerfile`
    * **Docker Context Path**: `.`
    * **Docker Build Stage**: leave empty (the Dockerfile's last stage is the one to run).

    Field names are from the [Dokploy build type docs](https://docs.dokploy.com/docs/core/applications/build-type). Save.

    The image builds the Next.js app (`bun run build`) and needs **no secrets at build time**: Dokploy writes the environment you set into a `.env` file, and this repository's `.dockerignore` excludes `.env*` from the build context, so secrets never enter the image. All settings are read at runtime, and `BETTER_AUTH_URL` too, so you do not set Build-time Arguments.
  </Step>

  <Step title="Set the environment variables">
    Open the **Environment** tab and paste the variables below (generate each secret with `openssl rand -hex 32`; see the [Docker Compose page](/self-hosting/docker-compose) for what each is). Save.

    ```sh theme={null}
    DATABASE_URL=postgres://USER:PASSWORD@INTERNAL_HOST:5432/DATABASE
    DATABASE_SSL=disable
    BETTER_AUTH_URL=https://sdr.example.com
    BETTER_AUTH_SECRET=PASTE
    INTEGRATION_CREDENTIALS_KEY=PASTE
    UNSUBSCRIBE_SECRET=PASTE
    CRON_SECRET=PASTE
    OUTREACH_TICK_SECRET=PASTE
    RESEND_API_KEY=PASTE
    AUTH_EMAIL_FROM="AgentSDR <auth@yourdomain.com>"
    ```

    `DATABASE_SSL=disable` is for Dokploy's own Postgres, which does not use TLS inside the network. For a hosted external database leave `DATABASE_SSL` out so TLS stays on. `BETTER_AUTH_URL` must be the final public https origin with no trailing slash. Store a copy of `INTEGRATION_CREDENTIALS_KEY` outside Dokploy.
  </Step>

  <Step title="Add the domain">
    First create a DNS `A` record for your host (for example `sdr.example.com`) pointing at the VPS IP. Then open the **Domains** tab and add a domain with:

    * **Host**: `sdr.example.com`
    * **Path**: `/`
    * **Container Port**: `3000` (the image listens on 3000)
    * **HTTPS**: on
    * **Certificate**: Let's Encrypt

    Dokploy routes through Traefik and obtains the certificate ([domains docs](https://docs.dokploy.com/docs/core/domains)). Do not publish port 3000 on the host.
  </Step>

  <Step title="Deploy">
    Click **Deploy** and watch the build in the **Deployments** tab and the **Logs** tab. The first build takes several minutes. Next.js builds use a lot of memory; if the build is killed on a small VPS, add swap or build the image elsewhere (Dokploy recommends building in a CI pipeline and deploying a registry image for production).
  </Step>

  <Step title="Create the schema">
    The app starts but the database is empty until you create the schema, once. The image contains the setup script. Run this inside the app container, from the server's shell:

    ```sh theme={null}
    docker ps --format '{{.Names}}' | grep -i agentsdr      # find the container name
    docker exec -it CONTAINER_NAME bun scripts/db/setup.ts --if-empty
    ```

    It prints `Database ready: N tables.` (or `Database already initialised — nothing to do.` on a later run). Alternatively create a one-off Application **Schedule Job** with that command (see the next step) and run it once. Then restart the app from the dashboard.
  </Step>

  <Step title="Schedule the periodic jobs">
    Dokploy **Schedule Jobs** can run a command inside the application container on a cron schedule ([schedule jobs docs](https://docs.dokploy.com/docs/core/schedule-jobs)). The target container must be running. Create an **Application** job per row, with the schedule in standard cron syntax. The container already has `curl` and your environment variables, and the app listens on `127.0.0.1:3000` inside it:

    | Schedule | Command |
    | - | - |
    | `5 0 * * *` | `curl -fsS -X POST -H "x-tick-secret: $OUTREACH_TICK_SECRET" http://127.0.0.1:3000/api/outreach/build-queue` |
    | `30 3 * * *` | `curl -fsS -X POST -H "x-tick-secret: $OUTREACH_TICK_SECRET" http://127.0.0.1:3000/api/outreach/mailboxes/watch` |
    | `*/15 * * * *` | `curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3000/api/linkedin/jobs/run-outreach` |
    | `*/10 * * * *` | `curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3000/api/linkedin/jobs/run-search-queue` |
    | `*/10 * * * *` | `curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3000/api/linkedin/jobs/replay-webhooks` |
    | `0 0 * * *` | `curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3000/api/linkedin/jobs/reset-daily-limits` |

    If a job's shell does not expand `$OUTREACH_TICK_SECRET`, wrap the command as `sh -c '...'` with single quotes. Skip the `CRON_SECRET` rows if you do not use LinkedIn, and the `OUTREACH_TICK_SECRET` rows if you do not send email. What each job does: [jobs table](/self-hosting/production#scheduled-jobs). You can instead call the same endpoints with the public URL from any external cron ([From source](/self-hosting/from-source#scheduled-jobs-without-the-sidecar)).
  </Step>

  <Step title="Turn on deploy on push">
    In the application's settings enable **Auto Deploy**. With the GitHub provider Dokploy deploys when you push; for other Git providers copy the generated webhook URL into your repository's webhooks. The branch configured in Dokploy must match the branch you push to, otherwise Dokploy reports "Branch Not Match" ([auto deploy docs](https://docs.dokploy.com/docs/core/auto-deploy)).

    Before pushing an update, check [Upgrading](/self-hosting/production#upgrading): a release may name a database migration to run first.
  </Step>
</Steps>

## Check that it works

<Check>
  `https://sdr.example.com/sign-in` loads over HTTPS, the **Logs** tab shows no database errors, and the manual `build-queue` call from the schedule step returns JSON instead of `unauthorized`. Sign up, create your organization, then continue with [Going to production](/self-hosting/production).
</Check>

## Backups

Dokploy can back up its Postgres service to an S3 destination from the database's backups section. Keep a copy of `INTEGRATION_CREDENTIALS_KEY` separately; see [Backups and restore](/self-hosting/production#backups-and-restore).

## Troubleshooting

<AccordionGroup>
  <Accordion title="DATABASE_URL is not set — copy .env.example to .env.local and fill it in">
    The environment was not saved or the app was not redeployed after saving it. Check the **Environment** tab and redeploy.
  </Accordion>

  <Accordion title="This database already has tables">
    The setup script only runs on an empty database. With `--if-empty` it exits successfully instead. See [Troubleshooting](/self-hosting/troubleshooting).
  </Accordion>

  <Accordion title="Bad gateway or the certificate is not issued">
    Check that the DNS record points at the server, the **Container Port** is `3000`, and ports 80 and 443 are open on the VPS.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="Going to production" icon="rocket" href="/self-hosting/production">
    Sign-up policy, email, upgrades.
  </Card>

  <Card title="Troubleshooting" icon="life-buoy" href="/self-hosting/troubleshooting">
    Real error messages and fixes.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.