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.
docker pull registry.gitlab.com/peh/sprachrohr:latestHelm 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.
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.comThe 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
| Variable | Description | Status |
|---|---|---|
DATABASE_URL | PostgreSQL connection string used by the app and by the migration job. | required |
AUTH_SECRET | Random string that signs the session cookies (NextAuth). | required |
AUTH_GOOGLE_ID | OAuth client id of your Google Cloud project. | required |
AUTH_GOOGLE_SECRET | OAuth client secret. The redirect URI is <app origin>/api/auth/callback/google. | required |
DEEPL_API_KEY | DeepL API key for suggestions and bulk translation. | required |
APP_ORIGIN | Public origin of the application, used for links from the website and redirects. Default https://app.sprachrohr.net. | optional |
MARKETING_HOSTS | Comma 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:
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=120sIngress 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.
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.