Four services, one server, zero excuses

Four services, one server, zero excuses

The platform had grown into four distinct services — a trading simulation Mini App, an acquisition bot, a fleet of AI agents, and an admin control panel — each with its own runtime, its own port, and its own opinions about how to start. During development, everything ran locally with ad-hoc scripts. Moving to production meant turning a collection of "works on my machine" processes into something that starts reliably, stays up, and can be updated without taking down the whole platform. The client had a single VPS. No Kubernetes, no managed services, no DevOps team. Just a server and the expectation that everything would work.

orchestration and systemd

service architecture diagram showing dependency chain

We set up Docker Compose as the orchestration layer for the data tier — PostgreSQL 16 and Redis 7 run as containers with persistent volumes and health checks. The application services themselves run as systemd units rather than containers, which was a deliberate choice: the agent processes need direct access to Telegram session files on disk, and the admin frontend benefits from Next.js's built-in process management. Each service — the admin API, the admin frontend, the acquisition bot, and the agent runner — has its own systemd unit file with proper dependency ordering. The database and Redis containers must be healthy before any application service starts. The bot and agents depend on the admin API being available because they share configuration stored in PostgreSQL. Getting this dependency chain right took several iterations of `After=`, `Requires=`, and health check scripts that actually verify service readiness rather than just process existence.

migrations and data

deploy script terminal output showing service health checks

Database migrations run through Alembic, which handles schema evolution as the platform's data model changes. But the more interesting migration challenge was the one-time transition from SQLite to PostgreSQL. The project started with SQLite during early development — fast to prototype, zero configuration. When it moved to production, all existing data needed to come along. SQLite-to-PostgreSQL migration is one of those things that sounds like a one-liner but isn't. Data types don't map cleanly — SQLite's dynamic typing means integers, booleans, and timestamps are all stored as whatever the ORM decided to write. Auto-increment behavior differs between the two databases, and foreign key references that SQLite silently accepted needed explicit handling in PostgreSQL. We wrote a migration script that reads each SQLite table, maps columns to PostgreSQL-compatible types, handles sequence resets for auto-increment fields, and preserves foreign key relationships. It ran once, successfully, and we kept it in the repo for documentation purposes — but nobody wants to run it again.

nginx and deploy

Nginx sits in front of everything as a reverse proxy, routing requests to the correct service based on subdomain. The admin panel, the API, the tracking link redirect endpoint, and the Mini App each have their own subdomain, each with SSL certificates managed by Certbot. Certificate renewal with multiple subdomains required a wildcard certificate approach — individual cert management for four subdomains would have been a maintenance headache. The Nginx configuration includes rate limiting on the public redirect endpoint and WebSocket upgrade handling for the admin panel's live features.

The deploy script ties everything together. It pulls the latest code, runs Alembic migrations, rebuilds the frontend, and restarts services in the correct order. The critical requirement was idempotency — running the script twice must produce the same result as running it once. No orphaned processes, no duplicate migrations, no half-built frontends. Each step checks preconditions before executing: migrations only run if there are pending revisions, the frontend only rebuilds if the source has changed, and service restarts use systemd's restart logic which handles already-stopped services gracefully. The script outputs a health check summary at the end — green checks for services that responded correctly, red flags for anything that didn't come up within the expected timeout.

results

The result is a production environment that the client's team can deploy to with a single command and monitor through systemd's standard tooling. Total downtime during a typical deploy is under 30 seconds — the time it takes for the services to restart sequentially. The honest limitation: this is a single-server setup with no redundancy. If the VPS goes down, everything goes down. For the current scale, that's an acceptable tradeoff — the platform serves hundreds of users, not millions. But the architecture is designed so that each service can be extracted into its own server when the time comes. Docker Compose for the data tier, systemd for app processes, and Nginx for routing — boring technology that works, and that someone besides us can understand and maintain.

Stack

Orchestration: Docker Compose (PostgreSQL 16, Redis 7)

Process management: systemd unit files

Migrations: Alembic, custom SQLite→PostgreSQL tooling

Proxy: Nginx, Certbot (SSL)

Deploy: Bash scripting (idempotent)

2026, «VOSGLOS». All rights reserved.