Appsweet ships as a single container. Point it at a Postgres database and an OIDC issuer and you have the workspace running on hardware you control.
What it needs first
Two things, both required while the process is still booting:
- Postgres with the
vectorextension available. Appsweet has no embedded database, and its first migration runsCREATE EXTENSION IF NOT EXISTS vector. The stockpostgresimage does not carry it;pgvector/pgvector:pg16does. - A Zitadel instance.
APPSWEET_OIDC_ISSUERandAPPSWEET_OIDC_CLIENT_IDare read during startup, not on the first sign-in. Without them the container exits before it binds a port, so/readynever answers at all. Zitadel is the only provider that works: the backend requests a Zitadel-specific scope and reads Zitadel's organisation claims to resolve a workspace, so another OIDC provider signs the user in and then fails on/v1/me. Docker Compose has the details.
If you have Postgres but no issuer, Docker Compose explains what to point Appsweet at and what the repository's own stack bundles for you — its two-service example brings up Appsweet and Postgres, but no identity provider, so read that page before assuming it is a complete deployment.
Run it
With a database and an issuer in hand, the container on its own is one command.
# exact version + digest from the release manifest — there is no :latest tag
docker run -p 3000:3000 \
--add-host host.docker.internal=host-gateway \
-e APPSWEET_DATABASE_URL=postgres://appsweet:secret@host.docker.internal:5432/appsweet \
-e APPSWEET_BIND_ADDR=0.0.0.0:3000 \
-e APPSWEET_PUBLIC_BASE_URL=http://localhost:3000 \
-e APPSWEET_OIDC_ISSUER=https://auth.example.com \
-e APPSWEET_OIDC_CLIENT_ID=your-client-id \
ghcr.io/blendable-dev/appsweet-backend:0.1.0-beta.1@sha256:<digest-from-the-release-manifest>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 — substitute the version and digest for the release you are deploying.
The appsweet-backend package is private for the duration of the controlled beta. Pulling
it needs a registry credential scoped to read packages — until your Docker daemon is logged
in to ghcr.io with one, the pull fails as if the image did not exist.
APPSWEET_BIND_ADDR matters: the default is 127.0.0.1:3000, which inside a container
means the port mapping reaches nothing.
--add-host host.docker.internal=host-gateway is what makes that database address resolve.
The host.docker.internal alias is supplied automatically by Docker Desktop on macOS and
Windows, but not by Docker Engine on Linux — which is where most self-hosted installs
run. Without the flag the name does not resolve there, the connection fails while the
process is still booting, and the container exits. The flag is harmless on Docker Desktop,
so the command above is the same everywhere. If your Postgres is somewhere other than the
Docker host — a managed service, another machine — use its real hostname and drop the flag.
That address also assumes Postgres is listening on an interface the container can reach,
not only on localhost. A distribution default of listen_addresses = 'localhost' refuses
the connection even once the name resolves.
Check it came up
Appsweet applies its migrations on boot, then reports readiness once the database pool is attached.
curl http://localhost:3000/ready
Next steps
The command above leaves out object storage, so file uploads are disabled — fine while you are trying Appsweet out, not something to deploy. The repository's bundled stack wires storage up for you; Configuration lists the variables if you are assembling it yourself.