How onlyapps.org runs on a single server — a Go API, a job queue inside PostgreSQL, Next.js for the site and console, Dokploy for deploys — and four lessons from launch day.
The new onlyapps.org — the public site, the console our team uses to edit it, the API and every background job — runs on a single VPS. No managed databases, no Redis, no external CI. Here is how it is put together and what we learned while deploying it.
The pieces
| Part | Technology | What it does |
|---|---|---|
| Website | Next.js 16 | Server-rendered pages in English, Serbian and Russian |
| Console | Next.js 16 | Editors, publishing, settings for the team |
| API | Go, chi, pgx, sqlc | All business logic, authentication, media |
| Worker | Go, River | Scheduled publishing, emails, IndexNow, cache refresh |
| Database | PostgreSQL 18 | Data and the job queue |
| Platform | Dokploy, Traefik, Let's Encrypt | Builds, routing and TLS on our VPS |
One database for data and jobs
Background jobs live in PostgreSQL through River, a Go job queue built on Postgres. The biggest win is transactions: when an editor publishes a post, the content change and the jobs it triggers — refresh the site cache, notify IndexNow, email subscribers — are written in one transaction. Either everything happens or nothing does, and there is no second system to keep alive.
Content as data, not HTML
Posts and documents are stored as structured JSON from the editor and rendered by the site component by component. No HTML is stored, so there is nothing to sanitize on the way out, and the same content renders consistently in every language.
Fast pages without a CDN
The site uses Next.js cache components. Pages are served from cache and invalidated by tags the moment something is published — the worker calls the site and only the affected pages are rebuilt. Media is stored on a Docker volume and served by the Go API on its own subdomain, with images converted into lighter WebP or JPEG variants. In our test runs Lighthouse mobile scores land in the low 90s for performance and 100 for SEO.
Deploying with Dokploy
The whole stack is one Docker Compose file with six services: website, console, API, worker, PostgreSQL and a backup container that runs pg_dump every night and keeps two weeks of dumps. Dokploy pulls the repository, builds the images on the server and routes the domains through Traefik with Let's Encrypt certificates. Database migrations run automatically when the API starts.
Four things we learned on launch day
- Unique service names on a shared network. On a server with many projects, Dokploy connects public services to one shared Docker network. Other projects there had their own
api,webandpostgresservices, so a plainhttp://api:8080could reach the wrong container. Internal addresses now use unique aliases such asonlyapps-api. - Define domains in one place. Our compose file declared Traefik routers, and Dokploy generated its own for the same hosts. We moved all domains to Dokploy and kept only middlewares — security headers and the www redirect — in the compose file, each on its own service.
- Large uploads need a longer read timeout. Traefik v3 closes a request body after 60 seconds by default, which cut off video uploads on slower connections. Raising the entrypoint read timeout fixed it.
- Passwords inside URLs. A base64-generated database password can contain a slash, which breaks a
postgres://connection string. Hex passwords avoid the problem entirely.
Tip
Before the first deploy, check whether your secrets end up inside URLs, and generate them in hex if they do.
Why we keep it this simple
Everything is tested before it ships — every change goes through linting, unit and integration tests, a production build and end-to-end tests with accessibility checks. The result is a stack a small team can understand completely, move to another server in an evening, and run for the price of one VPS.