/admin)kubasobecki/EventKit (private), deploy files in deploy/ghcr.io/kubasobecki/eventkit:<tag> (private package, tag = short commit SHA of main)web + eventkit_internal/opt/stack/eventkit/websecure, certresolver le, middleware secure-headers.github/workflows/deploy.yml): every merge to main builds the image and deploys it over SSH as deploy (since 2026-09-30)| Container | Image | Networks | Purpose |
|---|---|---|---|
eventkit-app |
ghcr.io/kubasobecki/eventkit:$EVENTKIT_TAG |
web, eventkit_internal |
Next.js + Payload 3 (app, admin, API) |
eventkit-db |
postgres:17-alpine | eventkit_internal |
Database |
Volumes: eventkit_db-data (Postgres), eventkit_media (uploads, /app/media).
| File | Contents |
|---|---|
compose.yml |
Copy of deploy/compose.yml from the repo |
.env |
EVENTKIT_TAG, DATABASE_URL, PAYLOAD_SECRET (mode 600) |
.env.db |
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB (mode 600) |
backups/ |
Daily pg_dump files (mode 700) |
deploy.log |
One line per deploy: time, old tag → new tag, ok / rolled back |
Secrets were generated on the server with openssl rand -hex and exist only there (and in Restic backups).
eventkit-db, not db. The app is also on the shared web network, where authentik-db-1 has the service alias db. Using db in DATABASE_URL resolves to the Authentik database and fails with password authentication failed. Any new stack whose app joins web must use a unique DB hostname.eventkit_internal is internal: true: the database has no outbound internet access and is not reachable from web.POSTGRES_PASSWORD in .env.db is read only on the first start (empty volume). To change it later: ALTER USER in Postgres, then update both .env.db and .env.prodMigrations run on the first HTTP request, not at container start. Check logs for Migrated:./admin/create-first-user is open to anyone. Create the admin right after a fresh deploy (new certificates appear in CT logs and bots arrive within minutes).mem_limit: 1g on the app container.docker login ghcr.io as root with a classic PAT, scope read:packages only. Expires 2027-09-30. Credentials in /root/.docker/config.json.Flow: merge to main → workflow deploy: check (tsc + lint, also on every PR) → build (push ghcr.io/kubasobecki/eventkit:<7-char sha>, Actions has Write access to the package) → deploy (ssh deploy@91.98.38.241 <tag>).
deploy user: not in the docker group. ~deploy/.ssh/authorized_keys has a forced command (restrict,command="sudo /usr/local/bin/eventkit-deploy \"$SSH_ORIGINAL_COMMAND\""), so whatever the client sends becomes an argument of the script. Sudoers: /etc/sudoers.d/eventkit-deploy (only this script)./usr/local/bin/eventkit-deploy <tag> (root, 755; source: deploy/eventkit-deploy in the repo): accepts only ^[0-9a-f]{7,40}$, lock via flock, pulls before touching .env, sets EVENTKIT_TAG, up -d app, waits up to 150 s for /admin, on failure restores the previous tag and exits 1 (the workflow turns red).DEPLOY_SSH_KEY (private key, exists only there), DEPLOY_HOST, DEPLOY_KNOWN_HOSTS (pinned ed25519 host key).authorized_keys (keep the restrict,command=... prefix) and the DEPLOY_SSH_KEY secret.compose.yml on the server is still updated by hand when deploy/compose.yml changes.Tag: service=eventkit (auto-discovered by backup.sh: /opt/stack/eventkit + volumes eventkit_*).
/root/backup.sh backup eventkit
/root/backup.sh list eventkit
Daily pg_dump at 3:40 AM (cron), 7-day local retention:
40 3 * * * docker exec eventkit-db pg_dump -U eventkit eventkit > /opt/stack/eventkit/backups/eventkit_$(date +\%Y\%m\%d).sql && find /opt/stack/eventkit/backups -name "*.sql" -mtime +7 -delete
Restic copies the eventkit_db-data files of a running database, which may be inconsistent. Restore the database from the SQL dump in backups/ (included in every Restic snapshot).
cd /opt/stack/eventkit && docker compose stop app
docker exec eventkit-db psql -U eventkit -d eventkit -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
docker exec -i eventkit-db psql -U eventkit -d eventkit < backups/eventkit_YYYYMMDD.sql
docker compose up -d
# Deploy / roll back to any tag (same script as CI, with health check and auto-rollback)
sudo /usr/local/bin/eventkit-deploy <sha>
# Deploy history
cat /opt/stack/eventkit/deploy.log
# Logs
docker compose logs -f app
# Health
curl -I https://eventkit.enclari.com/admin
docker compose ps
# DB shell
docker exec -it eventkit-db psql -U eventkit -d eventkit