> For the complete documentation index, see [llms.txt](https://docs.hexabot.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.hexabot.ai/faq/how-can-i-deploy-my-hexabot-project/docker-deployment.md).

# Docker Deployment

Choose this method when you want a containerized deployment with Compose-managed services.

Use this path when you want Docker to manage the Hexabot app, PostgreSQL, or other supporting services.

For shared deployment concepts, see [How can I deploy my Hexabot project?](/faq/how-can-i-deploy-my-hexabot-project.md).

### Prerequisites

You need:

* A Linux server with SSH access
* Node.js `>= 24.17.0`
* One package manager: `npm`, `pnpm`, `yarn`, or `bun`
* Docker Engine with the Compose plugin
* NGINX
* Certbot
* PostgreSQL if you do not use the Compose PostgreSQL service

Helpful setup guides:

* [Setting Up Docker](/developer-guide/setting-up-docker.md)
* [CLI Command Reference](/developer-guide/cli-command-reference.md)
* [SMTP Configuration](/developer-guide/smtp-configuration.md)

<details>

<summary>Install Docker</summary>

Install Docker Engine with the official guide for your operating system:

* [Docker Engine installation guides](https://docs.docker.com/engine/install/)

If you run Ubuntu, you can use this example:

```bash
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
```

After Docker is installed, allow your user to run Docker without `sudo`:

```bash
sudo usermod -aG docker $USER
```

Then log out and back in before you continue.

If you need to keep working in the current session, use this temporary workaround:

```bash
sudo chmod 666 /var/run/docker.sock
```

Without the group step, Hexabot CLI commands that call Docker can fail with a permission error.

</details>

### Prepare the project

Install the CLI globally:

```bash
npm install -g @hexabot-ai/cli
```

Clone the project and prepare the Docker env file:

```bash
git clone git@github.com:YOUR_ORG/my-project.git
cd my-project
npm install
hexabot env init --docker
```

Use these commands during setup and validation:

```bash
hexabot check
hexabot config show
hexabot docker ps
```

Docker Compose reads `.env.docker`.

### Configure environment variables

Generate strong secrets before you edit `.env.docker`:

```bash
openssl rand -hex 32
```

Update all public URL and origin variables before the first production start.

Do not leave the `.env.docker` localhost defaults in place.

Those values control CORS, cookies, and links sent in emails.

#### Update the production domain values together

When you change `APP_DOMAIN`, also change these values to the same public domain:

| Variable            | Production value           |
| ------------------- | -------------------------- |
| `APP_DOMAIN`        | `mychatbot.ai`             |
| `API_ORIGIN`        | `https://mychatbot.ai/api` |
| `FRONTEND_BASE_URL` | `https://mychatbot.ai`     |
| `FRONTEND_ORIGIN`   | `https://mychatbot.ai`     |
| `HTTPS_ENABLED`     | `true`                     |

If these values still point to `http://localhost:*`, production requests can fail, cookies may behave incorrectly, and email links can point to the wrong host.

#### Secrets

Replace every development placeholder in `.env.docker` before the first public start.

Change these values at minimum:

* `JWT_SECRET`
* `PASSWORD_RESET_SECRET`
* `CONFIRM_ACCOUNT_SECRET`
* `SIGNED_URL_SECRET`
* `SESSION_SECRET`
* `DB_PASSWORD` if PostgreSQL uses password authentication

This is the typical set of values that must not stay on template defaults such as `dev_only`.

Generate them all in one step:

```bash
cat <<EOF
JWT_SECRET=$(openssl rand -hex 32)
PASSWORD_RESET_SECRET=$(openssl rand -hex 32)
CONFIRM_ACCOUNT_SECRET=$(openssl rand -hex 32)
SIGNED_URL_SECRET=$(openssl rand -hex 32)
SESSION_SECRET=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 16)
EOF
```

Copy the output into `.env.docker` and replace the existing values.

{% hint style="warning" %}
Do not deploy with `dev_only`, `change-me`, or any checked-in example secret.
{% endhint %}

Set these core values:

| Variable                   | Example value              | Notes                                                       |
| -------------------------- | -------------------------- | ----------------------------------------------------------- |
| `NODE_ENV`                 | `production`               | Use `production` for ongoing public deployment.             |
| `PORT`                     | `3000`                     | Default Hexabot app port.                                   |
| `APP_DOMAIN`               | `mychatbot.ai`             | Public domain name.                                         |
| `SSL_EMAIL`                | `admin@mychatbot.ai`       | Email used by Certbot.                                      |
| `API_ORIGIN`               | `https://mychatbot.ai/api` | Public API base URL.                                        |
| `FRONTEND_BASE_URL`        | `https://mychatbot.ai`     | Public app URL.                                             |
| `FRONTEND_ORIGIN`          | `https://mychatbot.ai`     | Allowed browser origin.                                     |
| `HTTPS_ENABLED`            | `true`                     | Enable public HTTPS-aware behavior.                         |
| `DB_TYPE`                  | `postgres`                 | Recommended production database.                            |
| `DB_SYNCHRONIZE`           | `false`                    | Set this explicitly to `false` before any production start. |
| `DB_AUTO_MIGRATE`          | `true`                     | Enable on one primary API node.                             |
| `API_IS_PRIMARY_NODE`      | `true`                     | Set `true` on the node that runs migrations.                |
| `SESSION_SECRET`           | `...`                      | Use a long random value.                                    |
| `JWT_SECRET`               | `...`                      | Use a long random value.                                    |
| `PASSWORD_RESET_SECRET`    | `...`                      | Use a long random value.                                    |
| `CONFIRM_ACCOUNT_SECRET`   | `...`                      | Use a long random value.                                    |
| `SIGNED_URL_SECRET`        | `...`                      | Use a long random value.                                    |
| `UPLOAD_DIR`               | `/uploads`                 | Keep a mounted upload path.                                 |
| `UPLOAD_MAX_SIZE_IN_BYTES` | `52428800`                 | `50 MB`. Match the NGINX body size.                         |

{% hint style="danger" %}
Set `DB_SYNCHRONIZE=false` explicitly in `.env.docker` before you start the production stack.

Do not leave it unset.

The production Compose file falls back to `DB_SYNCHRONIZE=true` when the variable is missing.

That can trigger TypeORM schema sync against a production database.
{% endhint %}

Database values depend on your PostgreSQL source:

* If you use the Compose PostgreSQL service, keep the Docker PostgreSQL settings from the template or overlay.
* If you use an external PostgreSQL server, set `DB_HOST`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD`, `DB_NAME`, `DB_SCHEMA`, or `DB_URL` for that server.

Optional SMTP variables:

| Variable             | Example value                    | Notes                                 |
| -------------------- | -------------------------------- | ------------------------------------- |
| `EMAIL_SMTP_ENABLED` | `true`                           | Enable transactional email.           |
| `EMAIL_SMTP_HOST`    | `smtp.example.com`               | SMTP host.                            |
| `EMAIL_SMTP_PORT`    | `587`                            | Common STARTTLS port.                 |
| `EMAIL_SMTP_SECURE`  | `false`                          | Use `true` for implicit TLS on `465`. |
| `EMAIL_SMTP_USER`    | `smtp-user`                      | SMTP username or API key user.        |
| `EMAIL_SMTP_PASS`    | `smtp-password`                  | SMTP password or API key.             |
| `EMAIL_SMTP_FROM`    | `Hexabot <noreply@mychatbot.ai>` | Sender address.                       |

{% hint style="info" %}
Hexabot reads SMTP settings at startup. Restart the API service after any SMTP change.
{% endhint %}

### Run the first database bootstrap

Fresh PostgreSQL deployments need one private bootstrap before public traffic.

Hexabot seeds the first admin and default data only outside production mode.

Set these variables in `.env.docker` before the first private start:

```env
SEED_ADMIN_EMAIL=admin@mychatbot.ai
SEED_ADMIN_PASSWORD=replace-with-a-strong-password
SEED_ADMIN_FIRST_NAME=Admin
SEED_ADMIN_LAST_NAME=User
NODE_ENV=development
DB_SYNCHRONIZE=true
DB_AUTO_MIGRATE=true
API_IS_PRIMARY_NODE=true
```

{% hint style="warning" %}
`DB_SYNCHRONIZE=true` is useful for the first bootstrap only.

After the first admin and schema exist, switch back to `NODE_ENV=production` and `DB_SYNCHRONIZE=false`.
{% endhint %}

Use the Docker development command for this bootstrap:

```bash
hexabot dev --docker --services postgres -d
```

{% hint style="danger" %}
This bootstrap command can also start pgAdmin on port `9000`.

On a public cloud server, that port may be reachable from the internet unless your firewall blocks it.

The dev pgAdmin service uses development credentials and is not meant for public exposure.

During bootstrap, restrict port `9000` with your firewall or trusted-network rules.

As soon as bootstrap is complete, stop the stack with `hexabot docker down`.
{% endhint %}

{% hint style="warning" %}
Do not use `hexabot start --docker` for the first bootstrap.

That command uses the production Compose stack.

The production Compose config sets `NODE_ENV=production`.

That overrides `NODE_ENV=development` from `.env.docker`.

In production mode, the admin seeder does not run.
{% endhint %}

Then confirm the services are running:

```bash
hexabot docker ps
hexabot docker logs api -f
```

Wait for the API to finish booting.

On a typical server, this usually takes less than a minute after the containers are up.

Keep following the API logs until you see:

```txt
Nest application successfully started
```

If that line does not appear, keep the log stream open and check for migration, database, or env errors above it.

Then verify that the admin user was seeded:

```bash
docker exec <postgres-container> psql -U <DB_USERNAME> -d <DB_NAME> -c 'SELECT email FROM users;'
```

The result should include the `SEED_ADMIN_EMAIL` value you set for bootstrap.

Before you expose the public domain, confirm that the seeded admin can sign in from a trusted network.

After the first bootstrap:

1. Set `NODE_ENV=production`
2. Set `DB_SYNCHRONIZE=false` in `.env.docker`
3. Keep `DB_AUTO_MIGRATE=true` on one primary API node only
4. Stop the bootstrap stack with `hexabot docker down`
5. Continue with the production start below

### Start the production stack

{% hint style="danger" %}
Before you run `hexabot start --docker`, recheck `.env.docker`.

Make sure `NODE_ENV=production` and `DB_SYNCHRONIZE=false` are both present.

If `DB_SYNCHRONIZE` is missing, the production Compose stack can default it to `true`.
{% endhint %}

Start Hexabot with Docker:

```bash
hexabot start --docker --services postgres --build -d
```

For day-to-day operations, these commands are useful:

```bash
hexabot docker ps
hexabot docker logs api -f
```

If `hexabot docker logs api -f` behaves unexpectedly in production, use Docker directly:

```bash
docker ps
docker logs my-project-api-1 -f
```

Use the API container name shown by `docker ps`.

This fallback is more predictable for live operations.

`hexabot docker logs` can resolve a different Compose scope than the one used to start the stack.

NGINX should proxy to the published Hexabot app port, usually `127.0.0.1:3000`.

### Configure NGINX

Install NGINX:

```bash
sudo apt update
sudo apt install nginx
```

Create a dedicated site config:

{% code title="/etc/nginx/sites-available/hexabot" %}

```nginx
server {
    listen 80;
    server_name mychatbot.ai;
    client_max_body_size 50M;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
```

{% endcode %}

This forwards both the admin UI and `/api` routes to the same Hexabot app.

Do not add a rewrite rule for `/api`.

Keep `APP_DOMAIN`, `API_ORIGIN`, `FRONTEND_BASE_URL`, `FRONTEND_ORIGIN`, and `HTTPS_ENABLED` aligned with the same public domain you set in `server_name`.

If you increase `UPLOAD_MAX_SIZE_IN_BYTES`, raise `client_max_body_size` to match.

Enable the site and remove the default site:

```bash
sudo rm -f /etc/nginx/sites-enabled/default
sudo ln -sf /etc/nginx/sites-available/hexabot /etc/nginx/sites-enabled/hexabot
```

Validate and reload NGINX:

```bash
sudo nginx -t
sudo systemctl reload nginx
```

### Enable HTTPS with Certbot

Install Certbot and the NGINX plugin:

```bash
sudo apt install nginx certbot python3-certbot-nginx
```

Verify DNS before you request the certificate:

```bash
# Verify DNS resolves to this server before running Certbot
dig +short mychatbot.ai
# The result should match your server's public IP
```

If the result does not match your server yet, wait for DNS propagation before you continue.

Request the certificate:

```bash
sudo certbot --nginx -d mychatbot.ai
```

Then verify and reload:

```bash
sudo nginx -t
sudo systemctl reload nginx
```

If you also use `www`, include it in the Certbot command.

Confirm automatic renewal is active:

```bash
sudo systemctl status certbot.timer
sudo certbot renew --dry-run
```

### Verify the deployment

Run these checks:

```bash
hexabot check
hexabot docker ps
hexabot docker logs api -f
curl http://127.0.0.1:3000/api/health
sudo nginx -t
sudo systemctl status nginx
dig mychatbot.ai
```

Final browser check:

```txt
https://mychatbot.ai
```

### Troubleshooting

#### The Docker stack starts, but the app is unreachable

Confirm the stack is up:

```bash
hexabot docker ps
hexabot docker logs api -f
```

Then confirm NGINX still points to the published host port, usually `127.0.0.1:3000`.

#### The browser loads the UI, but API requests fail

Do not rewrite `/api`.

Hexabot already serves the API under `/api` from the same app.

#### The first admin account was not created

The first admin is seeded only outside production mode.

For Docker deployments, run the first private bootstrap with `hexabot dev --docker --services postgres -d`.

Keep `SEED_ADMIN_*` variables set and `DB_SYNCHRONIZE=true`.

Then switch back to `NODE_ENV=production` and `DB_SYNCHRONIZE=false`.

#### Uploads fail

Confirm the upload path is mounted correctly inside the container.

Then keep `client_max_body_size` aligned with `UPLOAD_MAX_SIZE_IN_BYTES`.

#### DNS or HTTPS does not work

Check DNS first:

```bash
dig mychatbot.ai
```

Then recheck NGINX and Certbot:

```bash
sudo nginx -t
sudo systemctl status nginx
```

### Related pages

* [How can I deploy my Hexabot project?](/faq/how-can-i-deploy-my-hexabot-project.md)
* [Manual Deployment](/faq/how-can-i-deploy-my-hexabot-project/manual-deployment.md)
* [Setting Up Docker](/developer-guide/setting-up-docker.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.hexabot.ai/faq/how-can-i-deploy-my-hexabot-project/docker-deployment.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
