Self-hosting

One Docker image, one Helm release, Postgres included.

Docker image

Every commit on main is built into an image in the GitLab registry. The image runs the standalone Next.js server on port 3000 and contains drizzle-kit for migrations.

Terminal
docker pull registry.gitlab.com/peh/sprachrohr:latest

Helm chart

The chart lives in ci/sprachrohr and contains the deployment, a service, a Traefik ingress with cert-manager annotations, the secret holding the environment and a Bitnami PostgreSQL dependency.

Terminal
helm dependency update ./ci/sprachrohr
helm upgrade --install sprachrohr ./ci/sprachrohr \
  --namespace sprachrohr --create-namespace \
  --set image.tag=<commit sha> \
  --set env.AUTH_SECRET=<random secret> \
  --set env.AUTH_GOOGLE_ID=<client id> \
  --set env.AUTH_GOOGLE_SECRET=<client secret> \
  --set env.DEEPL_API_KEY=<deepl key> \
  --set env.APP_ORIGIN=https://app.example.com \
  --set env.MARKETING_HOSTS=example.com,www.example.com \
  --set postgres.auth.password=<postgres password> \
  --set postgres.auth.postgresPassword=<postgres password> \
  --set ingress.hosts[0].host=app.example.com \
  --set ingress.tls[0].hosts[0]=app.example.com

The database URL is derived from the postgres.auth values, so you only set the password. Set postgres.enabled=false and provide DATABASE_URL yourself to use an existing database.

Environment variables

VariableDescriptionStatus
DATABASE_URLPostgreSQL connection string used by the app and by the migration job.required
AUTH_SECRETRandom string that signs the session cookies (NextAuth).required
AUTH_GOOGLE_IDOAuth client id of your Google Cloud project.required
AUTH_GOOGLE_SECRETOAuth client secret. The redirect URI is <app origin>/api/auth/callback/google.required
DEEPL_API_KEYDeepL API key for suggestions and bulk translation.required
APP_ORIGINPublic origin of the application, used for links from the website and redirects. Default https://app.sprachrohr.net.optional
MARKETING_HOSTSComma separated hosts that serve the website instead of the app. Default sprachrohr.net,www.sprachrohr.net. Leave the hosts out of the ingress to run the app only.optional

Database migrations

The schema is pushed with drizzle-kit before every deployment. The CI pipeline runs it as a Kubernetes job with the same image; you can do the same by hand:

Terminal
kubectl create job --namespace sprachrohr --image=registry.gitlab.com/peh/sprachrohr:<commit sha> drizzle-migrate \
  -- sh -c "npx drizzle-kit push"
kubectl wait --for=condition=complete job/drizzle-migrate --namespace sprachrohr --timeout=120s

Ingress hosts

The same deployment serves the application and this website. The middleware decides by host: hosts listed in MARKETING_HOSTS get the website, every other host gets the app.

values.yaml
ingress:
  hosts:
    - host: app.example.com
    - host: example.com
    - host: www.example.com
  tls:
    - secretName: sprachrohr-app-tls
      hosts: [app.example.com]
    - secretName: sprachrohr-site-tls
      hosts: [example.com, www.example.com]
  • cert-manager issues one certificate per TLS entry via HTTP-01, so every host must resolve to the ingress before the first deploy.
  • Requests to app routes on a website host are redirected to APP_ORIGIN, so a wrong bookmark still ends up in the app.
  • The website is also reachable under /site on the app host, which is handy for previewing on localhost.
Sign-in only works on the app host. Add <app origin>/api/auth/callback/google as an authorised redirect URI in the Google Cloud console.