|
Build and Deploy / build-and-push (push) Successful in 1m9s
Details
|
||
|---|---|---|
| .gitea/workflows | ||
| apps | ||
| packages/shared | ||
| .env.example | ||
| .gitignore | ||
| .npmrc | ||
| Dockerfile | ||
| QUICKSTART.md | ||
| README.md | ||
| SETUP.md | ||
| docker-compose.dev.yml | ||
| docker-compose.prod.yml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| turbo.json | ||
README.md
ludops-todo
A fullstack monorepo template for quickly scaffolding and deploying apps on your own infrastructure.
Stack: Node.js (Fastify) · React + Vite · PostgreSQL (Drizzle ORM) · Docker · Gitea CI/CD · Nginx Proxy Manager
Creating a new app from this template
1. Fork or copy the repository
In Gitea, use "Fork" or create a new repo and push this one under a new name.
Convention: ludops-<appname> (e.g. ludops-portfolio, ludops-dashboard).
2. Rename everything (global find & replace)
Search the entire repo for todo and replace with your app name (lowercase, no spaces).
| File | What changes |
|---|---|
.env.example |
APP_NAME and REGISTRY_IMAGE values |
Root package.json |
"name" field |
apps/web/src/App.tsx |
APP_NAME constant (display name) |
Tip: In VS Code use
Ctrl+Shift+Hto find & replace across all files.
3. Set repository variables in Gitea
In your new repo: Settings → Variables → Add variable
| Variable | Example value |
|---|---|
APP_NAME |
myapp |
REGISTRY_IMAGE |
git.ludops.com/ludops/ludops-myapp |
These are injected into the CI runner automatically — no .env file needed on the server for the pipeline.
4. Create the database
Connect to your shared Postgres instance and create the app's database:
sudo docker exec -it ludops-postgres psql -U gitea -c "CREATE DATABASE todo_db;"
The DATABASE_URL in docker-compose.prod.yml automatically uses ${APP_NAME}_db.
5. Push to trigger the CI pipeline
git push
Gitea Actions will:
- Build and push the Docker image to the registry
- Pull the new image on the server
- Run
drizzle-kit pushto sync the DB schema - Start the container with
docker compose up -d
6. Route your domain
In Nginx Proxy Manager, add a new proxy host:
- Domain:
myapp.yourdomain.com - Forward hostname:
ludops-myapp-app← matchescontainer_namein docker-compose - Forward port:
3000 - Enable SSL via Let's Encrypt
That's it. Your app is live.
Project structure
.
├── apps/
│ ├── api/ # Fastify backend
│ │ ├── src/
│ │ │ ├── index.ts # Server entry point — add your API routes here
│ │ │ └── db/
│ │ │ ├── index.ts # Drizzle client
│ │ │ ├── schemas.ts
│ │ │ └── schemas/
│ │ │ └── visit-logs.schema.ts # Keep: tracks page visits
│ │ └── drizzle.config.ts
│ └── web/ # React + Vite frontend
│ └── src/
│ └── App.tsx # Start building your UI here
├── packages/
│ └── shared/ # Types shared between api and web
│ └── index.ts
├── docker-compose.prod.yml # Production container config
├── Dockerfile # Multi-stage build
├── .env.example # ← copy to .env on the server
└── .gitea/workflows/
└── deploy.yml # CI/CD pipeline
Local development
# Install dependencies
pnpm install
# Start both api and web in watch mode
pnpm dev
- Web: http://localhost:5173
- API: http://localhost:3000
You'll need a local Postgres instance and a .env or the DATABASE_URL env var set for the API.
Adding database tables
- Create a new schema file in
apps/api/src/db/schemas/ - Export it from
apps/api/src/db/schemas.ts - Push to the server — the CI pipeline runs
drizzle-kit pushautomatically
Adding shared types
Add exports to packages/shared/index.ts. They are available in both api and web as @ludops/shared.
Built-in features kept from the todo
| Feature | Where |
|---|---|
| Visit logging (IP + user agent per API request) | apps/api/src/db/schemas/visit-logs.schema.ts |
/api/status endpoint (health + visit count) |
apps/api/src/index.ts |
| SPA fallback routing | apps/api/src/index.ts |
| Static file serving (web dist) | apps/api/src/index.ts |