Portfolio site with a Vite/React frontend and a same-repo Node/Express backend for the blog, likes, replies, and admin moderation.
Live site: https://smit-dev.codestacx.com
/portfolio home/blogblog index/blog/:slugblog post, likes, replies/admin/loginadmin sign-in/admin/postspost editor/admin/repliesreply moderation
npm installnpm run dev- Fallback admin creds:
admin/admin-dev-only npm run devincludesBLOG_APP_ENV=development,BLOG_DB_SCHEMA=blog_dev, and the local fallback flag automatically- Without the fallback path, set
BLOG_APP_ENV,BLOG_DB_SCHEMA,BLOG_ADMIN_USERNAME, andBLOG_ADMIN_PASSWORD npm run dev:clientornpm run dev:serverif you only need one side- Open
http://localhost:5173;/api/*proxies to127.0.0.1:3001
- Build:
npm run build - Run built app:
npm start - Coolify/Nixpacks should use
npm start - The server listens for
SIGTERM/SIGINT, stops accepting traffic, drains HTTP requests, and closes the Postgres pool before exit. Give it at least 10 seconds of shutdown grace in Coolify.
Configure the application health check in Coolify with:
- Protocol:
HTTP - Port:
3001(or the value ofPORTif you override it) - Path:
/api/health - Expected status:
200 - Interval:
30s - Timeout:
5s - Retries:
3 - Start period: at least
30s; use a longer value if the database migration/release job runs as part of deployment
/api/health is a cheap liveness check and does not require Postgres, so it is appropriate for restarting an unresponsive container. Use /api/ready for a database-backed readiness check or external monitoring: it returns 200 with { "ok": true } only when the application schema and storage are available, and fails when Postgres is unavailable. Run npm run db:migrate as a pre-deploy/release task before relying on readiness.
The app listens on PORT (default 3001) and should remain private behind Coolify's proxy. For a local check, run curl -i http://127.0.0.1:3001/api/health and expect HTTP/1.1 200 with { "ok": true }.
Required production values:
NODE_ENV=productionandBLOG_APP_ENV=production(startup rejects a mismatch)DATABASE_URLandBLOG_DB_SCHEMA=blog_prodBLOG_PUBLIC_ORIGIN=https://your-public-hostfor admin origin and CSRF checksBLOG_ADMIN_USERNAMEand a unique, strongBLOG_ADMIN_PASSWORDBLOG_VISITOR_COOKIE_SECRETcontaining at least 32 random characters
Keep database credentials, admin credentials, and the visitor-cookie secret in Coolify secrets rather than the image or repository. Rotate them if they are exposed. Do not enable BLOG_ALLOW_DEV_ADMIN_DEFAULTS in production.
Remote production Postgres connections use hostname-verified TLS by default. A Coolify/Docker database on a private IP or single-label internal service name may run without TLS only when BLOG_DATABASE_ALLOW_INSECURE_PRIVATE_NETWORK=1 is explicitly set. The application rejects that exception for public database hosts. Connections also enforce statement, lock, and idle-transaction timeouts.
Versioned migrations live in db/migrations/. Run the migration command once as a pre-deploy/release task, before starting the new application version:
npm run db:migrate
npm run db:smoke
npm run build
npm startThe migration runner uses a per-schema advisory lock, records applied versions in schema_migrations, and runs pending migrations in a transaction. Migration 001 is idempotent and safely adopts databases previously created from db/blog_dev.sql or db/blog_prod.sql. Before a migration can be recorded, and again during startup/smoke checks, the app validates the current version plus required tables, columns, nullability, keys, uniqueness, status checks, foreign keys, and cascade rules. Incomplete legacy schemas fail closed instead of being marked current. The older bootstrap SQL files remain for reference; use migrations for future deployment changes.
Run migration and smoke-check jobs with the same NODE_ENV, BLOG_APP_ENV, DATABASE_URL, BLOG_DB_SCHEMA, and database TLS settings as the application. The migration database role needs DDL permissions; the runtime role only needs access to the application schema and data after migrations complete.
Enable automated Postgres backups in the database platform and retain copies outside the application container. Replies, likes, sessions, and posts are stored only in Postgres; post deletion cascades to related replies and likes.
Before each schema migration, take or verify a recent backup. Regularly test a restore into an isolated database/schema, then run npm run db:smoke against the restored copy. Document the target recovery point and who can restore production; an untested backup should not be treated as a recovery plan.
The built-in request limiter is deliberately bounded but process-local. Run one application replica unless the reverse proxy provides global rate limiting. Before scaling to multiple Node replicas, move login/reply/like rate limits to an atomic shared store such as Redis. Keep the application port private behind Coolify; Express trusts forwarded client addresses only from loopback and private-network proxy hops.