Skip to content

Latest commit

 

History

History
401 lines (310 loc) · 12.6 KB

File metadata and controls

401 lines (310 loc) · 12.6 KB

Deploy OpenBcon on a Hostinger VPS

This guide documents a production deployment on a Hostinger VPS where the Hostinger Docker Manager already runs Traefik on ports 80 and 443.

If no other service owns ports 80 and 443, use the default deploy/docker-compose.production.yml: the project Caddy container can manage public HTTPS directly. This guide is for the shared-entrypoint setup:

Internet
  -> Hostinger Traefik :443
  -> 127.0.0.1:8080
  -> OpenBcon Caddy :80
  -> api:8787 / python:8010
  -> PostgreSQL / MongoDB

Prerequisites

  • A Hostinger VPS with Docker Engine and Docker Compose
  • A DNS A record pointing the deployment hostname to the VPS public IPv4 address
  • No conflicting AAAA record pointing the hostname to another server
  • A working Hostinger Traefik application with ports 80 and 443 available for the hostname
  • A server-side OpenAI API key if live generation is enabled

Do not commit .env.production, database passwords, API keys, or the Traefik ACME storage file to Git.

Prepare DNS

Create an A record such as:

open.example.com -> YOUR_VPS_IPV4

Check the result from the VPS:

dig +short A open.example.com
dig +short AAAA open.example.com

The A response must be the VPS address. Remove or correct an unexpected AAAA record before requesting a certificate.

Configure production secrets

From the repository root:

cp deploy/.env.production.example deploy/.env.production
openssl rand -hex 32

Edit deploy/.env.production and set at least:

DOMAIN=your-domain.example
POSTGRES_PASSWORD=use_a_long_random_alphanumeric_value
MONGODB_ROOT_PASSWORD=use_a_different_long_random_value
APP_STATE_ENCRYPTION_KEY=the_64_character_hex_value_from_openssl
OPENBCON_OPENAI_API_KEY=your_server_side_key
SEED_DEMO_DATA=false

Test Mode can start without an SMTP server. It uses the console email provider and logs verification or password-reset previews in the API logs. Before switching to Live Mode, configure SMTP with your provider's values:

PUBLIC_APP_URL=https://your-domain.example
EMAIL_PROVIDER=console
EMAIL_FROM=OpenBcon <no-reply@your-domain.example>
# Set EMAIL_PROVIDER=smtp and uncomment/configure these values before Live Mode.
# SMTP_HOST=smtp.hostinger.com
# SMTP_PORT=465
# SMTP_SECURE=true
# SMTP_USER=no-reply@your-domain.example
# SMTP_PASSWORD=your_mailbox_password

To enable Google registration, create a Google OAuth web client and add this authorized redirect URI:

https://your-domain.example/api/auth/google/callback

Then set GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, and GOOGLE_OAUTH_REDIRECT_URI in deploy/.env.production. Never put these values in Vite frontend variables or commit them to Git.

Keep APP_STATE_ENCRYPTION_KEY stable. Changing it can make previously encrypted application settings unreadable.

Adapt the project Caddy service

The default production Compose file maps Caddy directly to ports 80 and 443. When Hostinger Traefik already owns those ports, change the caddy service to map only a loopback HTTP port:

ports:
  - "127.0.0.1:8080:80"

Do not map the project Caddy 443 port in this mode.

The Bootstrap Setup wizard applies this change automatically by mounting the repository's deploy/Caddyfile.http:

http://{$DOMAIN} {

Do not manually change the main deploy/Caddyfile; that file is reserved for direct Caddy HTTPS mode.

Keep the internal routes pointed at Docker service names:

handle_path /ai-api/* {
    reverse_proxy python:8010
}

handle /api/* {
    reverse_proxy api:8787
}

Do not add reverse_proxy 127.0.0.1:8080 to this file. That would make the project Caddy proxy to its own host-mapped port.

Configure Hostinger Traefik

Open Docker Manager and manage the existing traefik application. The Traefik configuration needs a router for the public hostname and a service pointing to the project Caddy port.

The Traefik application should have these static settings:

network_mode: host
command:
  - "--providers.docker=true"
  - "--providers.docker.exposedbydefault=false"
  - "--providers.file.filename=/etc/traefik/dynamic.yml"
  - "--providers.file.watch=true"
  - "--entrypoints.web.address=:80"
  - "--entrypoints.websecure.address=:443"
  - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
  - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
  - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
  - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
  - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
  - "--entrypoints.web.http.redirections.entrypoint.scheme=https"

Mount the dynamic configuration as a file. An absent bind source can be created as a directory by Docker, so create the file before starting Traefik:

volumes:
  - traefik-letsencrypt:/letsencrypt
  - /var/run/docker.sock:/var/run/docker.sock:ro
  - /etc/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro

Create /etc/traefik/dynamic.yml:

http:
  routers:
    openbcon:
      rule: "Host(`open.example.com`)"
      entryPoints:
        - websecure
      service: openbcon
      tls:
        certResolver: letsencrypt

  services:
    openbcon:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:8080"

Because this Traefik container uses host networking, 127.0.0.1:8080 refers to the VPS host and reaches the project Caddy mapping. If the Traefik installation does not use host networking, use a host-gateway address that is reachable from the Traefik container instead.

Make sure dynamic.yml is a regular file, not a directory:

file /etc/traefik/dynamic.yml

Then recreate Traefik without deleting its volumes:

docker compose up -d --force-recreate traefik
docker compose logs -f --tail=100 traefik

Start OpenBcon

For the first deployment, the repository can run a temporary public setup page so no SSH tunnel is required. Allow inbound TCP port 8090 temporarily in the VPS firewall, run the setup command below, and open the URL printed in the VPS terminal. The URL contains a one-time token and expires after 24 hours. Close port 8090 again after the form is saved; the deployment script stops the setup container automatically.

After saving, the page continues to display configuration, DNS, service, HTTPS, and /api/health checks. When deployment succeeds it shows the public application URL; when it fails it points you to the deploy.sh terminal output and the retry command.

The deployment script validates the Caddy configuration before starting the application, waits for the API, Python, and Caddy health checks, and then verifies the public HTTPS /api/health endpoint. A deployment is not reported as complete until that public check returns the healthy API response. If the wizard reports a refused connection, TLS handshake failure, timeout, or HTTP error, follow the next-step message in the wizard and inspect the Caddy or Traefik logs before retrying setup.

Run all Compose commands with the production env file. Compose does not automatically load .env.production for interpolation:

docker compose \
  --env-file deploy/.env.production \
  -f deploy/docker-compose.production.yml \
  up -d --build

Or use the repository deployment script, which supplies the env file for you:

./deploy/deploy.sh

To re-run the setup wizard for an existing deployment:

./deploy/deploy.sh --setup

Open the printed address in a browser, for example:

http://YOUR_VPS_IPV4:8090/setup?token=...

Do not leave port 8090 open after setup. It is not an application port and is only used for the temporary bootstrap wizard.

The setup form uses the admin email as the first OpenBcon administrator login and asks for a minimum 12-character admin password. After the API is healthy, the deployment script creates separate administrator rows and an owner workspace in both the Test and Live PostgreSQL databases. PostgreSQL stores only the password hash, and the one-time bootstrap password is removed from deploy/.env.production after successful initialization.

Rotate database credentials

The setup page shows the generated PostgreSQL and MongoDB credentials once. Save them in a password manager and rotate them as soon as the deployment is verified. Use alphanumeric passwords so they can safely be used in PostgreSQL URLs:

cd /opt/openbcon
cp deploy/.env.production "deploy/.env.production.backup.$(date +%Y%m%d%H%M%S)"
new_postgres_password="$(openssl rand -hex 32)"
new_mongodb_password="$(openssl rand -hex 32)"

Change the PostgreSQL role while the database container is running. The administrator username is fixed as admin:

postgres_user="admin"
docker compose --env-file deploy/.env.production -f deploy/docker-compose.production.yml \
  exec -T postgres psql -U "$postgres_user" -d postgres \
  --set=role_name="$postgres_user" --set=role_password="$new_postgres_password" \
  -c "ALTER ROLE :\"role_name\" PASSWORD :'role_password';"

Change the MongoDB root password with the fixed administrator username admin:

mongo_user="admin"
mongo_password="$(awk -F= '$1 == "MONGODB_ROOT_PASSWORD" { print $2; exit }' deploy/.env.production)"
docker compose --env-file deploy/.env.production -f deploy/docker-compose.production.yml \
  exec -T mongodb mongosh --username "$mongo_user" --password "$mongo_password" \
  --authenticationDatabase admin \
  --eval "db.changeUserPassword('$mongo_user', '$new_mongodb_password')"

Edit deploy/.env.production and update POSTGRES_PASSWORD, the password in all six PostgreSQL URLs/DSNs, and MONGODB_ROOT_PASSWORD. Do not change only the env file: the database accounts must already have the new passwords. Then validate and recreate the application containers without touching data volumes:

docker compose --env-file deploy/.env.production -f deploy/docker-compose.production.yml config >/dev/null
docker compose --env-file deploy/.env.production -f deploy/docker-compose.production.yml \
  up -d --force-recreate api python caddy
docker compose --env-file deploy/.env.production -f deploy/docker-compose.production.yml \
  ps

If a command fails, restore the backup env file and use the old password to recover before retrying. Never run docker compose down -v during credential rotation.

Check the services:

docker compose \
  --env-file deploy/.env.production \
  -f deploy/docker-compose.production.yml \
  ps

Inspect logs with the same --env-file flag:

docker compose \
  --env-file deploy/.env.production \
  -f deploy/docker-compose.production.yml \
  logs -f --tail=200 api

Verify the deployment

Test the private project Caddy mapping from the VPS:

curl -i http://127.0.0.1:8080/api/health

Test the public HTTPS route:

curl -i https://open.example.com/api/health

A healthy Node API returns HTTP 200 and JSON containing:

{
  "status": "ok",
  "database": "connected"
}

Do not use curl -k as the final fix. It only bypasses certificate verification and can hide a broken Traefik certificate configuration.

Updates and data safety

git pull --ff-only
docker compose \
  --env-file deploy/.env.production \
  -f deploy/docker-compose.production.yml \
  up -d --build

The API runs migrations automatically when AUTO_MIGRATE=true. Do not run docker compose down --volumes during a normal update. That command deletes the PostgreSQL and MongoDB data volumes.

After the deployment has included the private updater service, future updates can be started from Admin Console: open Updates, run Check updates, review the GitHub commit, then choose Install update. The agent backs up deploy/.env.production, force-switches to main, resets the checkout to origin/main, cleans untracked checkout files, and runs deploy/deploy.sh; it does not accept arbitrary Git URLs or shell commands. It may restart the API briefly, and the Admin Console waits for it to return. If the button says the service is unavailable, run ./deploy/deploy.sh once manually to create the updater and its token. Port 8788 must remain private; do not add a public firewall rule for it.

After the update reports success, the Admin Console clears browser Cache Storage, service workers, localStorage, and sessionStorage, then reloads the current page with a cache-busting URL. A locally stored browser session is cleared too, so sign in again after the reload; database data and Docker volumes are not removed.

Before production upgrades, back up PostgreSQL and MongoDB and review the migrations in server/db/migrations/.