- Docker and Docker Compose installed on the server
- PostgreSQL database (can be a managed service like Railway, Supabase, or a self-hosted instance)
-
Copy the environment file and configure it:
cp .env.example .env
Required variables to set before starting:
DATABASE_URL— PostgreSQL connection stringAUTH_SECRET— Random secret string (generate withopenssl rand -base64 32)ADMIN_EMAILS— Comma-separated admin email addressesOAUTH_PROVIDERS— Which OAuth provider(s) to enable (e.g.,google)OAUTH_<PROVIDER>_CLIENT_IDandOAUTH_<PROVIDER>_CLIENT_SECRET— Provider credentialsNEXT_PUBLIC_BASE_URL— Your public URL (e.g.,https://vaalit.example.com)MAIL_FROM,SMTP_HOST,SMTP_USER,SMTP_PASSWORD— Email sending
⚠️ NEXT_PUBLIC_*variables are baked into the Docker image at build time. If you build your own image, pass them as--build-arg. If you use the pre-built image, these defaults (vaalit.fyysikkokilta.fi) are already set and you cannot override them at runtime. -
Run database migrations:
# If running against a local/accessible database directly DATABASE_URL="postgresql://..." pnpm db:migrate # Or run migrations from inside the container (first start without CMD override): docker run --env-file .env ghcr.io/fyysikkokilta/fk-vaalimasiina:latest node src/db/migrate.js
-
Start the application:
docker compose up -d
The app listens on
127.0.0.1:8010by default. Put it behind a reverse proxy (nginx, Caddy, Traefik) to expose it.
server {
listen 80;
server_name vaalit.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name vaalit.example.com;
ssl_certificate /etc/letsencrypt/live/vaalit.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vaalit.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8010;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}The included update-deployment.sh script handles updates in three steps:
./update-deployment.shThis runs:
git pull— Updates repository files (for docker-compose.yml changes)docker compose pull— Pulls the latest Docker image from the registrydocker compose up -d— Restarts the container with the new image
If you're not tracking the git repository on the server, run:
docker compose pull && docker compose up -dIf you've forked the project and want to customize branding, build your own image:
docker build \
--build-arg NEXT_PUBLIC_BASE_URL="https://vaalit.myorg.example.com" \
--build-arg NEXT_PUBLIC_BRANDING_HEADER_TITLE_TEXT="My Guild Votes" \
--build-arg NEXT_PUBLIC_BRANDING_HEADER_TITLE_SHORT_TEXT="Votes" \
--build-arg NEXT_PUBLIC_BRANDING_FOOTER_HOME_TEXT="myorg.example.com" \
--build-arg NEXT_PUBLIC_BRANDING_FOOTER_HOME_LINK="https://myorg.example.com" \
-t my-registry/my-vaalimasiina:latest \
.Push it to your registry and update docker-compose.yml to use your image:
services:
app:
image: my-registry/my-vaalimasiina:latestThe included .github/workflows/ci.yml runs three jobs on every push:
- lint — Type checking, oxlint, oxfmt
- test — Playwright E2E tests against a temporary PostgreSQL database
- build — Builds and pushes a Docker image to GitHub Container Registry (GHCR), only on
masterbranch
-
In your fork's Settings → Secrets and variables → Actions, you don't need to add secrets for the container registry — GitHub Actions has built-in access to GHCR for the repository owner.
-
Update the branding env vars in
.github/workflows/ci.yml(search forNEXT_PUBLIC_BRANDING_*) to match your organization. -
Update
NEXT_PUBLIC_BASE_URLin the workflow to your production URL. -
The built image will be pushed to
ghcr.io/<your-github-username>/fk-vaalimasiina.
pnpm db:migrateRequires DATABASE_URL to be set.
After editing src/db/schema.ts, generate a migration:
pnpm db:generateThis creates a new SQL file in src/drizzle/. Commit it and run pnpm db:migrate to apply.
pnpx drizzle-kit studio # Open Drizzle Studio (database browser)
pnpx drizzle-kit push # Push schema directly (dev only, skips migration files)