docs
GitHubNL
Self-hosting

Docker Compose

Appsweet plus a persistent Postgres in a single Compose file, for when you already run Zitadel.

Compose is the supported way to run Appsweet. The file below is the minimal example: it gives you Appsweet plus a persistent Postgres, and nothing else.

NOTE

This example is not a complete, sign-in-capable deployment. It defines two services, and neither of them is an identity provider — APPSWEET_OIDC_ISSUER below points at a placeholder you have to replace with a Zitadel instance you already run. Substitute a real issuer and the backend boots; leave the placeholder in place and it boots too, but nobody can sign in. If you do not already run one, read "You still need Zitadel" below before you start.

The Compose file

Drop this in a docker-compose.yml and run docker compose up -d. Images are selected by exact version plus digest, taken from the validated release manifest for that version — there is no latest tag, and no mutable tag is accepted as the source of truth.

docker-compose.yml
services:
  appsweet:
    image: ghcr.io/blendable-dev/appsweet-backend:0.1.0-beta.1@sha256:<digest-from-the-release-manifest>
    ports: ["3000:3000"]
    # Long form, not `depends_on: [db]` — see "Waiting for Postgres" below.
    depends_on:
      db:
        condition: service_healthy
    environment:
      APPSWEET_DATABASE_URL: postgres://appsweet:secret@db:5432/appsweet
      # The default 127.0.0.1 would not be reachable through the port mapping.
      APPSWEET_BIND_ADDR: 0.0.0.0:3000
      APPSWEET_PUBLIC_BASE_URL: http://localhost:3000
      # Read during startup, not on the first sign-in. Without the pair the
      # container exits before it binds a port. Point both at your issuer.
      APPSWEET_OIDC_ISSUER: https://auth.example.com
      APPSWEET_OIDC_CLIENT_ID: your-client-id

  db:
    # Not plain postgres:16 — the first migration runs
    # CREATE EXTENSION IF NOT EXISTS vector, which the stock image cannot do.
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_USER: appsweet
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appsweet
    volumes: [pgdata:/var/lib/postgresql/data]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appsweet -d appsweet"]
      interval: 5s
      timeout: 5s
      retries: 12

volumes: { pgdata: {} }

Appsweet keeps no state on its own filesystem, so it needs no volume of its own: workspace data lives in Postgres and uploaded files in S3-compatible object storage.

You still need Zitadel

Appsweet does not manage passwords, so the two services above cannot sign anyone in on their own. https://auth.example.com is a placeholder: replace it with the issuer URL and client ID of a Zitadel instance you run or subscribe to, and register $APPSWEET_PUBLIC_BASE_URL/auth/callback with it as a redirect URI.

NOTE

Zitadel is the only identity provider Appsweet supports today. Speaking standard OIDC discovery is not enough. The backend asks every issuer for the Zitadel-specific scope urn:zitadel:iam:user:resourceowner, and it resolves the signed-in user's workspace from three claims it looks up under their Zitadel names — urn:zitadel:iam:user:resourceowner:id, …:name and …:primary_domain. When those claims are missing the session is refused: the redirect back from the provider looks like it worked, then /v1/me answers 403 workspace_claims_required and no workspace ever loads. That is exactly how far a stock Keycloak, Authentik, Auth0 or Entra ID deployment gets. There is no claim-mapping configuration to bridge the gap — it is not implemented yet.

If you do not have one, the Appsweet repository ships a stack that brings up Zitadel alongside Appsweet, Postgres, and Garage object storage, already wired together and pinned to versions we test: infra/docker-compose.yml. Use that rather than the file above.

NOTE

That stack is not reproduced here, and copying fragments of it will not work: it builds the backend and its bootstrap containers from the repository's own build contexts, and a Zitadel instance needs an admin user, a project, an application, and a bootstrapped client ID before it will issue a single token. Any Zitadel snippet short enough for this page would be one you could not actually boot. The repository is private during the controlled beta, so ask for access if you need the stack and do not have it.

Waiting for Postgres

The depends_on above is deliberately in its long form with condition: service_healthy, paired with a healthcheck on db. The short form — depends_on: [db] — only waits for the database container to start, not for PostgreSQL inside it to accept connections. On a fresh volume that gap is seconds long, because the image has to initialise the data directory before it listens.

Appsweet connects to the database once during startup and does not retry, and the service above carries no restart policy. So with the short form, the first docker compose up -d on a new machine can leave the backend exited and stopped for good while Postgres finishes initialising, and curl localhost:3000/ready answers nothing at all. It usually looks fine on the second run, which is what makes it easy to mistake for a flake and "simplify" the long form away — please do not.

pg_isready is the conventional probe, and is what Appsweet's own stack uses: it exits zero only once the server is accepting connections for that user and database.

Why pgvector, not plain Postgres

Appsweet's semantic search stores embeddings in the same database as the data, so the very first migration it runs is CREATE EXTENSION IF NOT EXISTS vector. On a stock postgres image that statement fails, migrations abort, and the backend never reaches readiness.

pgvector/pgvector:pg16 is only one way to satisfy that. If you are substituting your own Postgres — a managed service, an existing cluster, a different major version — treat "pgvector is installed and creatable" as a hard requirement rather than a detail of the image chosen here. Most managed providers ship it as an extension you enable per database.

The registry is private during the beta

The appsweet-backend package is private for the duration of the controlled beta, so docker compose pull needs a ghcr.io credential scoped to read packages. Without one the pull fails as if the image did not exist.

NOTE

This is the minimum that boots, not the minimum that is usable. Two pieces are still missing: a Zitadel issuer, without which nobody signs in, and an S3-compatible endpoint for uploads, without which files are disabled entirely. Both are a development shortcut rather than a supported deployment — see Configuration.

Back to appsweet.appEdit this page on GitHub