Skip to content

Deployment Guide

Construct can be deployed via Docker (recommended) or as a bare-metal systemd service. Code changes made by the agent via the edit and shell tools must be deployed manually.

FileRole
deploy/DockerfileMulti-stage Docker build
deploy/docker-compose.ymlCompose configuration with volume mounts and env
.dockerignoreExcludes build artifacts, secrets, and dev files
src/main.tsApplication entry point
  • Docker and Docker Compose installed on the host
  • A ~/.construct/ directory for persistent data
  • A ~/.construct/.env file with required environment variables

Create the data directory and environment file:

Terminal window
mkdir -p ~/.construct/extensions/skills ~/.construct/extensions/tools

Create ~/.construct/.env with at minimum:

Terminal window
OPENROUTER_API_KEY=sk-or-v1-...
TELEGRAM_BOT_TOKEN=123456:ABC-DEF...

See Environment Configuration for all available variables. Note that DATABASE_URL, LOG_FILE, and EXTENSIONS_DIR are pre-set in the Dockerfile to point to /data/ paths, so you do not need to set them in your .env file.

From the project root:

Terminal window
docker compose -f deploy/docker-compose.yml up -d --build

This will:

  1. Build a multi-stage image using node:22-alpine
  2. Install dependencies in a builder stage, then copy only node_modules to the runtime stage
  3. Install git in the runtime stage (optional, for version control)
  4. Mount ~/.construct on the host to /data in the container
  5. Load environment variables from ~/.construct/.env
  6. Start the application with restart: unless-stopped
Terminal window
docker compose -f deploy/docker-compose.yml logs -f

Look for Construct is running in the output.

The host directory ~/.construct/ maps to /data inside the container:

~/.construct/ (host) --> /data/ (container)
.env (env_file, not mounted inside /data)
construct.db construct.db (SQLite database)
construct.log construct.log (log file)
extensions/ extensions/ (EXTENSIONS_DIR)
SOUL.md SOUL.md
IDENTITY.md IDENTITY.md
USER.md USER.md
skills/ skills/
tools/ tools/

The .env file is read by Docker Compose via env_file: — it is injected as environment variables into the container, not mounted as a file inside /data.

The Dockerfile (deploy/Dockerfile) uses a two-stage build:

Builder stage — installs dependencies:

FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

Runtime stage — copies dependencies and source:

FROM node:22-alpine
WORKDIR /app
RUN apk add --no-cache git
COPY --from=builder /app/node_modules ./node_modules
COPY package.json tsconfig.json ./
COPY src/ ./src/
COPY cli/ ./cli/

Environment defaults baked into the image:

  • DATABASE_URL=/data/construct.db
  • LOG_FILE=/data/construct.log
  • EXTENSIONS_DIR=/data/extensions

Entry point: node --import=tsx src/main.ts

The compose file (deploy/docker-compose.yml) is minimal:

services:
construct:
build:
context: ..
dockerfile: deploy/Dockerfile
volumes:
- ~/.construct:/data
env_file:
- ~/.construct/.env
restart: unless-stopped

Key points:

  • Build context is .. (project root), since the compose file lives in deploy/
  • restart: unless-stopped ensures the container restarts if the process exits

After the agent edits its own source code via the edit and shell tools, changes must be deployed manually. There is no automatic self-deploy mechanism.

After code changes are committed inside the container:

Terminal window
docker compose -f deploy/docker-compose.yml up -d --build

The --build flag rebuilds the image with the latest source. The container restarts with the new code.

On bare-metal deployments:

Terminal window
cd /opt/construct
git pull
pnpm install --frozen-lockfile
sudo systemctl restart construct

Wait 5 seconds and verify: sudo systemctl is-active construct

For bare-metal deployment without Docker:

Terminal window
git clone <repo> /opt/construct
cd /opt/construct
pnpm install --frozen-lockfile
Terminal window
cp .env.example .env
# Edit .env with your API keys and configuration
[Unit]
Description=Construct Telegram Bot
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/construct
ExecStart=/usr/bin/node --env-file=.env --import=tsx src/main.ts
Restart=on-failure
User=construct
[Install]
WantedBy=multi-user.target
Terminal window
sudo systemctl enable construct
sudo systemctl start construct

The systemd unit is named construct by default. The agent process needs passwordless sudo for systemctl restart construct and systemctl is-active construct if you want the agent to restart itself via the shell tool.