Deploying OTB Works
What a customer does to get a working instance, from nothing to signed in.
This document was written and then followed literally on a real server (Ubuntu, x86_64, Docker 29.6.1, behind a reverse proxy). Every command below has been run. Where something did not work the first time, the instruction was corrected rather than worked around, and the correction is noted so you know it was found rather than assumed.
Budget 20 minutes, plus however long your licence takes to arrive.
Before you start
Section titled “Before you start”- A Linux host with Docker. Nothing else is installed on it.
- Somewhere for the data to live. A single directory is enough.
- A hostname people will use, and a decision about TLS (see step 2).
- Your instance id will be generated on first boot. You quote it to OTB to get a licence, so the order is: install, read the id, request a licence, install the licence.
Works runs as one container with a SQLite database on a volume. There is no separate database server to provision, no message broker, and no cloud dependency. If you can run a container and give it a directory, you can run Works.
Step 1, get the image
Section titled “Step 1, get the image”Normal:
docker pull ghcr.io/outsidethebox-io/otb-works:1.0.0Air-gapped, or any site where the server cannot reach a registry. OTB gives
you a .tar of the image. Copy it across however your change process allows,
then:
docker load -i otb-works-1.0.0.tardocker images | grep otb-worksBoth paths end with the same image, and the rest of this document is identical.
Step 2, decide how TLS is terminated
Section titled “Step 2, decide how TLS is terminated”This matters more than it looks. Works refuses to start if you give it an
http:// address that is not localhost, because session cookies are marked
Secure and a browser silently discards them on a plain-HTTP origin. Without
that refusal you would get a sign-in page that accepts your password and then
returns you to the sign-in page, for ever, with nothing in the log.
Pick one:
OTB_WORKS_TLS |
Use when | What happens |
|---|---|---|
off |
Something already terminates TLS for you: Traefik, nginx, an F5, a cloud load balancer, a Cloudflare tunnel | The app listens on 4173 and your proxy does the rest |
auto |
The server is reachable from the internet on 80 and 443, and DNS for your hostname points at it | A certificate is obtained automatically. The app listens only on 127.0.0.1, so the plain port is not reachable at all |
internal |
A LAN-only deployment with no public DNS | A certificate from a local authority. Same loopback binding |
If you already run a reverse proxy, use off. Setting OTB_TRUSTED_PROXIES
implies off on its own, so an existing deployment cannot suddenly find this
container competing for port 443.
Step 3, write the compose file
Section titled “Step 3, write the compose file”Create a directory and a docker-compose.yml in it:
mkdir -p /srv/works && cd /srv/worksservices: works: image: ghcr.io/outsidethebox-io/otb-works:1.0.0 container_name: otb-works restart: unless-stopped ports: # Publish on ALL interfaces, or name one. If something else on this # host reaches Works (a reverse proxy on another machine, say), bind # the address that proxy uses: # - "192.168.0.10:4173:4173" # See the note under step 6 before you do: it changes what the health # check has to ask for. - "4173:4173" environment: OTB_PUBLIC_URL: https://works.example.com OTB_WORKS_TLS: "off" OTB_SESSION_SECRET: ${OTB_SESSION_SECRET:?set this in .env} OTB_TRUSTED_PROXIES: "loopback,linklocal,uniquelocal" volumes: - works-data:/app/packages/server/data
volumes: works-data:Then the one secret, generated once and never changed:
cat > .env <<EOFOTB_SESSION_SECRET=$(openssl rand -hex 32)EOFchmod 600 .envIt is required and has no default. The session secret signs sign-in cookies; changing it signs everyone out.
OTB_NOTIFICATIONS_ENC_KEY used to be required here too. Since 1.1.0 it is
not: the instance generates its own at-rest encryption keys on first boot and
keeps them in instance-keys.json beside the database on the data volume.
Setting the variable still works and still wins, see
Installing and upgrading.
The notifications key
encrypts your mail provider’s credentials at rest, and without it the email
step is unavailable, which means invitations, password resets and sign-in
links cannot be sent.
Set OTB_PUBLIC_URL to the address people type in their browser, not the
container’s address. Invitation links, password resets and single sign-on
redirects are all built from it.
Step 4, start it and read the setup code
Section titled “Step 4, start it and read the setup code”docker compose up -ddocker compose logs | tail -40You are looking for two things in the log.
The instance id, which identifies this deployment for licensing:
instance identity: generated for this deployment - quote it to OTB when askingfor a licence (it is shown on the Licence screen)instanceId: otbw-XXXX-XXXX-XXXX-XXXXThe setup code, a one-time code that lets the first administrator be created:
This instance has no administrator yet. Open the setup page and paste the code below to create one:
https://works.example.com/setup
Setup code: <64 hex characters>The code exists only in this log. It is never written to disk and stops working the moment an administrator exists. If you lose it, restart the container and a new one is printed.
Step 5, walk the setup wizard
Section titled “Step 5, walk the setup wizard”Open /setup and work through it. Seven steps, and only two need anything you
do not already have.
- Administrator: paste the setup code, then your name, email and a password of at least 12 characters.
- Sign in without a password: enrol a passkey, or skip. You can add one later from Account > Security.
- Licence: the screen shows your instance id with a copy button. This step is mandatory; see below.
- This programme: name, currency, locale, time zone, financial year, budget.
- Email: your mail provider. Needed before anyone can be invited.
- Single sign-on: OIDC or SAML, optional, and it tests the connection before it will enable it.
- What this instance may do: whether it may check OTB for new versions. There is no default: you answer it.
About the licence step. An unlicensed instance does nothing except let you create an administrator and install a licence. Every other route refuses. So the order is: start the container, copy the instance id, send it to OTB, and install the licence you get back. A licence is tied to that instance id, so it will not work on a different deployment.
Step 6, check it is actually working
Section titled “Step 6, check it is actually working”curl -s -o /dev/null -w '%{http_code}\n' http://localhost:4173/api/health200 means the application is up.
If that returns 000, check what the port is actually published on before
assuming the app is broken:
docker port otb-works4173/tcp -> 0.0.0.0:4173 answers on localhost. 4173/tcp -> 192.168.0.10:4173
does not, you bound it to one address, so ask for that address instead.
This was found by following these instructions on a real server: the container
was healthy and serving traffic through its proxy the whole time, while the
documented check reported 000. A health check that fails on a working system
is worse than none, because the next thing you do is restart something that was
fine. Then, from a browser on the address in
OTB_PUBLIC_URL, sign in as the administrator you just created.
If sign-in appears to succeed and returns you to the sign-in page, the cause is
almost always the cookie: OTB_PUBLIC_URL does not match the address you are
actually using, or something in front is terminating TLS without telling the
app. Check step 2.
Step 7, back it up
Section titled “Step 7, back it up”The whole instance is the data volume. There is no state anywhere else.
docker compose stopdocker run --rm -v works-data:/data -v "$PWD":/backup alpine \ tar czf /backup/works-backup-$(date +%Y%m%d).tar.gz -C /data .docker compose startKeep .env with it. Restoring the volume without the session secret signs
everyone out; restoring without the notifications key makes the stored mail
credentials unreadable.
The instance id lives inside the database, so a restored backup is the same deployment and keeps its licence. That is deliberate.
Upgrading
Section titled “Upgrading”Covered in installing-and-upgrading.md. In short: most upgrades are applied by an administrator inside the product, with no terminal access at all. Some releases change the base image and say so, and those need somebody who can edit this compose file.
When something is wrong
Section titled “When something is wrong”| Symptom | Cause |
|---|---|
| Container exits immediately, log names a variable | OTB_SESSION_SECRET or OTB_PUBLIC_URL is missing. Both are deliberately required |
Refuses to start, mentions http:// |
Step 2. A plain-HTTP origin cannot hold a Secure cookie |
| Sign-in loops back to sign-in | Same cause, from the browser’s side |
Everything answers 402 |
No licence, or a licence for a different instance id |
Reads work, writes answer 402 |
The licence has lapsed. Reading and exporting still work; install a renewal |
| No setup code in the log | An administrator already exists. Use the sign-in page |