Installation
Running your own installation from the published image, with Docker Compose or on Kubernetes.
Alba Ticket is one container image, ghcr.io/garth/alba, published for every release and pulled without a login. It needs PostgreSQL, Typesense for search, an S3-compatible bucket for attachments and a mail server. This page runs it three ways: with Docker Compose, which brings those services with it and suits one server; on Kubernetes, where you bring them; and the image alone on anything else. To run it on a Coolify server, or on AWS or Azure with their managed services, follow those pages instead. Nothing here needs the source code.
With Docker Compose
In an empty directory, let the image write the compose file and a .env with every secret and password generated:
mkdir alba && cd alba
docker run --rm -v "$PWD:/out" -u "$(id -u):$(id -g)" ghcr.io/garth/alba /app/bin/setup
docker compose up -d
On Linux, -u makes the files yours rather than the container's. Docker Desktop on macOS and Windows does not need it; in Windows PowerShell, leave it out and write ${PWD} for $PWD.
With no options this is an installation on your own computer at http://localhost:4000, with email caught by Mailpit at http://localhost:8025. The command takes:
| Option | What it does |
|---|---|
--host tickets.example.com |
A server behind a reverse proxy with TLS: sets PHX_HOST, https, and the attachment storage at files.tickets.example.com |
--files-host files.example.com |
Another host name for the attachment storage, with --host |
--port 8080 |
Publishes the application on another port, when 4000 is taken |
--force |
Replaces an existing compose.yaml and .env |
It never replaces an existing .env without --force, because the CLOAK_KEY in it encrypts the secrets stored in the database. It prints what to do next; then see The first start.
Writing the files by hand
Instead of the setup command, download the compose file and the example environment into an empty directory:
mkdir alba && cd alba
curl -O https://albaticket.com/files/compose.yaml
curl -o .env https://albaticket.com/files/env.example
The two files are compose.yaml and env.example. The compose file runs the application with PostgreSQL, Typesense, RustFS for attachments and Mailpit to catch email until you point it at a real server.
Edit .env and set the two required secrets:
SECRET_KEY_BASE=$(openssl rand -base64 64 | tr -d '\n')
CLOAK_KEY=$(openssl rand -base64 32)
SECRET_KEY_BASE signs sessions and tokens. CLOAK_KEY encrypts secrets stored in the database (single sign-on client secrets, storage credentials, webhook secrets). Keep both safe: losing CLOAK_KEY makes those stored secrets unreadable.
Then:
docker compose up -d
The first start
The migrate service brings the database schema up to date and exits; the application then starts, installs the default vocabulary (statuses, workflow, ticket types, priorities, resolutions, link types and roles), creates its bucket in the RustFS service and registers it as its attachment storage and listens on http://localhost:4000. The first visit shows the setup page that creates the administrator account and names the installation. To try Alba Ticket on something to look at, tick Add sample projects to explore there: the installation opens with the demo's projects, made by sample people who cannot log in (see Sample data). It then asks, once, what the installation uses: every module is on, and four features are off until you switch them on, namely the REST API, the MCP server for AI agents, public request forms and iCloud calendars. Switch on what you want and press Continue; all of it can be changed later under Administration, Modules and features (see Modules and features). A script or an AI application cannot connect until the REST API or the MCP server is switched on there. From there, create projects, invite people or connect single sign-on, and import from Jira if you are migrating.
The compose file runs the newest release. To pin one, set ALBA_IMAGE in .env to a tag from Published images and run docker compose pull before docker compose up -d; that is also how you upgrade.
Going to production
- Set
PHX_HOSTto the public host name andPHX_SCHEME=https, and put a reverse proxy in front (see Reverse proxy and TLS). - Point
SMTP_HOST,SMTP_PORT,SMTP_USERNAME,SMTP_PASSWORD,SMTP_TLSandMAIL_FROMat a real mail server, and remove the Mailpit service. - Either expose RustFS behind the proxy, set
STORAGE_PUBLIC_BASE_URLto that address andRUSTFS_CORS_ALLOWED_ORIGINSto the address people open the application at, or set theSTORAGE_*variables to your own bucket and remove therustfsservice. Browsers talk to the bucket directly. - Change the database and RustFS passwords and the Typesense key in
.envbefore the first start, unless the setup command wrote it, which generates them; the database and RustFS ones cannot be changed afterwards without extra steps. - Arrange backups.
- Enter your licence key.
On Kubernetes
On Kubernetes you bring the services: a PostgreSQL 14 or later database (a managed one is the usual choice), an S3-compatible bucket for attachments and a mail server. Typesense is small enough to run in the cluster, and a manifest for it is below. The application itself is a Deployment, with a Job that runs the migrations before each version starts. Every manifest on this page is a starting point; names, sizes and storage classes are yours to change.
Configuration
Everything the image reads is an environment variable, listed in Configuration. Put the secrets in a Secret and the rest in a ConfigMap:
apiVersion: v1
kind: Secret
metadata:
name: alba-secrets
stringData:
DATABASE_URL: postgres://alba:<password>@postgres.example.internal:5432/alba
SECRET_KEY_BASE: <openssl rand -base64 64>
CLOAK_KEY: <openssl rand -base64 32>
TYPESENSE_API_KEY: <openssl rand -base64 32>
STORAGE_ACCESS_KEY_ID: <bucket access key>
STORAGE_SECRET_ACCESS_KEY: <bucket secret key>
SMTP_PASSWORD: <mail password>
---
apiVersion: v1
kind: ConfigMap
metadata:
name: alba-config
data:
PHX_HOST: tickets.example.com
PHX_SCHEME: https
PORT: "4000"
TYPESENSE_URL: http://typesense:8108
STORAGE_ENDPOINT: https://s3.eu-west-1.amazonaws.com
STORAGE_REGION: eu-west-1
STORAGE_BUCKET: alba-attachments
STORAGE_PUBLIC_BASE_URL: https://alba-attachments.s3.eu-west-1.amazonaws.com
SMTP_HOST: smtp.example.com
SMTP_PORT: "587"
SMTP_USERNAME: alba
SMTP_TLS: always
MAIL_FROM: tickets@example.com
STORAGE_PUBLIC_BASE_URL is the address browsers upload to and download from, which for a cloud bucket is the bucket's own address; the bucket needs a CORS rule allowing PUT and GET from https://tickets.example.com, as Requirements says. Every variable is described in Configuration.
Search
Typesense holds derived data only, rebuilt from the database on demand, so one replica with a small volume is enough:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: typesense
spec:
serviceName: typesense
replicas: 1
selector:
matchLabels: { app: typesense }
template:
metadata:
labels: { app: typesense }
spec:
containers:
- name: typesense
image: typesense/typesense:30.2
args: ["--data-dir", "/data", "--api-key", "$(TYPESENSE_API_KEY)"]
env:
- name: TYPESENSE_API_KEY
valueFrom:
secretKeyRef: { name: alba-secrets, key: TYPESENSE_API_KEY }
ports:
- containerPort: 8108
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: [ReadWriteOnce]
resources:
requests: { storage: 5Gi }
---
apiVersion: v1
kind: Service
metadata:
name: typesense
spec:
selector: { app: typesense }
ports:
- port: 8108
Migrations
The image does not migrate on start, so that several copies can start at once. Run the migrations as a Job before each version's Deployment, with the same image tag and the same environment, and wait for it to finish:
apiVersion: batch/v1
kind: Job
metadata:
name: alba-migrate-0-7-8
spec:
backoffLimit: 2
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: ghcr.io/garth/alba:0.7.8
command: ["/app/bin/migrate"]
envFrom:
- configMapRef: { name: alba-config }
- secretRef: { name: alba-secrets }
kubectl apply -f migrate.yaml
kubectl wait --for=condition=complete job/alba-migrate-0-7-8 --timeout=10m
The version in the Job's name lets a new Job run for each upgrade. Migrations are additive, so the running version keeps working while they run.
The application
The image's default command is the server, which listens on port 4000 and answers GET /health once it is ready. It keeps nothing on disk: attachments, imports and exports all go through the S3-compatible storage, so the pod needs no volume.
apiVersion: apps/v1
kind: Deployment
metadata:
name: alba
spec:
replicas: 1
selector:
matchLabels: { app: alba }
template:
metadata:
labels: { app: alba }
spec:
containers:
- name: app
image: ghcr.io/garth/alba:0.7.8
ports:
- containerPort: 4000
envFrom:
- configMapRef: { name: alba-config }
- secretRef: { name: alba-secrets }
readinessProbe:
httpGet: { path: /health, port: 4000 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /health, port: 4000 }
initialDelaySeconds: 30
periodSeconds: 15
resources:
requests: { cpu: 500m, memory: 512Mi }
limits: { memory: 2Gi }
---
apiVersion: v1
kind: Service
metadata:
name: alba
spec:
selector: { app: alba }
ports:
- port: 80
targetPort: 4000
Expose the Service with whatever Ingress you use; it must pass WebSocket upgrades, which the common controllers do by default, and it can keep a small request body limit because attachments never pass through the application. Terminate TLS there, with PHX_SCHEME=https in the ConfigMap so the application writes its own addresses correctly:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: alba
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
ingressClassName: nginx
tls:
- hosts: [tickets.example.com]
secretName: alba-tls
rules:
- host: tickets.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: alba
port: { number: 80 }
Apply the manifests in this order: the Secret and ConfigMap, Typesense, the migration Job (and wait for it), then the application. The first visit to the host name shows the setup page that creates the administrator account and then asks what the installation uses (the REST API, the MCP server, public request forms and iCloud calendars start off), and the rest of Going to production applies: real mail, backups of the database and the bucket, and a licence key.
Several replicas
Nodes form an Erlang cluster when they can find each other, which on Kubernetes is a headless Service; Clustering explains what they share and why it matters. Add the Service, the two variables, and raise replicas:
apiVersion: v1
kind: Service
metadata:
name: alba-nodes
spec:
clusterIP: None
publishNotReadyAddresses: true
selector: { app: alba }
ports:
- port: 4000
- In the ConfigMap,
DNS_CLUSTER_QUERY: alba-nodes. - In the Secret,
RELEASE_COOKIE: <openssl rand -base64 32>, one value for every replica.
Erlang distribution between the pods uses port 4369 and a dynamic range, authenticated only by the cookie; a NetworkPolicy should keep those ports to the pods themselves. An import's file is an upload in the storage bucket, so whichever replica runs the import job finds it and no shared volume is needed.
To upgrade, run a new migration Job with the new tag, wait for it, then set the Deployment to the same tag; the rolling update replaces the pods, and people's browsers reconnect by themselves.
Licence
Running Alba Ticket on your own servers needs a licence, which is a key: a block of text beginning ALBA1.. What a licence costs, and the seat count it is priced on, is at albaticket.com/pricing. Get one, or a 30-day trial key to evaluate with, from the licence portal at albaticket.com/licenses: log in with your email address, start a trial or buy a licence, and copy the key. In your installation an administrator pastes it under Administration, Licence, which then shows who the licence is for, how long it has left and what it allows beside what you use. The user guide describes the page.
The key is a signed statement that the application checks by itself with a public key built into it. Nothing is sent to us, so the check works on a network with no way out, and there is no licence server to keep reachable. The key is stored in the database; it is not an environment variable, and it is not part of a workspace export, because it belongs to the installation and not to the data.
A licence never stops anything working. Without a key, with an expired one, or with more members than it has seats, every page carries a notice saying so; a trial says that it is a trial; and administrators are reminded for 30 days before a licence expires. A renewed or larger licence is a new key from the same portal page, pasted over the old one.
Published images
Every release is published as ghcr.io/garth/alba with semantic version tags: 0.7.8 is this release, 0.7 follows the latest patch of that minor and latest the newest release. edge is the most recent commit that passed the full test suite, and each commit is also tagged sha-<commit>. Images are built for linux/amd64 and pulled without a login.
To pin the compose file to a release, set ALBA_IMAGE in .env and pull:
echo ALBA_IMAGE=ghcr.io/garth/alba:0.7.8 >> .env
docker compose pull
docker compose up -d
Without ALBA_IMAGE, the compose file runs latest.
Running the image alone
The image works without the compose file. It needs DATABASE_URL, SECRET_KEY_BASE, CLOAK_KEY and PHX_HOST. Run /app/bin/migrate once per deploy (a one-off container or job with the same environment), then start the image; its default command is /app/bin/server, which listens on port 4000 and answers GET /health. The server does not migrate on start, so several copies can start at once. It needs no volume: files live in the storage bucket. See Configuration for every variable.