Authors:
Date of project start: September 10, 2024
CVT-Simulator is a CVT simulation application with a React frontend, a FastAPI backend, and the CINDER simulation model. Published deployments run entirely from Docker images; the application source repository does not need to be checked out on the server.
The repository publishes two public images to GitHub Container Registry:
ghcr.io/gr812b/cvt-simulator-backend
ghcr.io/gr812b/cvt-simulator-frontend
The root docker-compose.yaml is the canonical production deployment. It runs:
reverse proxy
|
v
cvt-frontend
|
| /api/v1/*
v
cvt-backend
|
v
PostgreSQL 17
|
v
Docker named volume
PostgreSQL is fully containerized. You do not install PostgreSQL, Python, Node, Alembic, or the CVT-Simulator repository on the server.
The deployment machine needs:
- Docker Engine
- the Docker Compose plugin (
docker compose) - an external Docker network used by the reverse proxy; the default name is
web - a reverse proxy attached to that external network
- optionally Watchtower, if automatic image updates are desired
The backend and PostgreSQL are not exposed as host ports. The frontend joins
the external web network so an existing reverse proxy can reach it directly.
If your reverse-proxy network is not named web, set WEB_NETWORK in .env
to its actual name.
Only these deployment files are required:
docker-compose.yaml
.env
The server does not need a Git checkout. Copy docker-compose.yaml from this
repository and create .env from .env.example.
For example:
mkdir -p /opt/cvt-simulator
cd /opt/cvt-simulator
# Copy docker-compose.yaml and .env.example here by your preferred method.
cp .env.example .envEdit .env:
POSTGRES_PASSWORD=replace-with-a-random-url-safe-password
CVT_TAG=latest
WEB_NETWORK=webA clean URL-safe password can be generated with:
openssl rand -hex 32Do not commit the real .env file.
The PostgreSQL password is used when the database is initialized. Do not simply
change POSTGRES_PASSWORD later on an existing database volume; changing the
Compose variable does not automatically change the password stored inside an
already-initialized PostgreSQL database.
If web already exists, nothing needs to be done.
Check it with:
docker network inspect webIf you intentionally use web and it does not exist yet:
docker network create webIf your reverse proxy already uses a differently named external Docker network,
leave that network alone and set WEB_NETWORK in .env to its name instead.
cd /opt/cvt-simulator
docker compose pulldocker compose up -d postgresCheck that it becomes healthy:
docker compose psThis is the one special step for a brand-new database:
docker compose run --rm cvt-backend \
sh -c "alembic upgrade head && python -m app.scripts.init_database"The initializer creates the deterministic demo account and the released library objects currently used by the frontend.
Do this once for a new database. Do not rerun database initialization as a normal update step.
docker compose up -dThe backend waits for PostgreSQL, applies any pending Alembic migrations, and then starts the API. The frontend waits for the backend health check.
Check the stack:
docker compose ps
docker compose logs --tail=100 cvt-backend
docker compose logs --tail=100 cvt-frontendPostgreSQL can be checked directly with:
docker compose exec postgres pg_isready -U cvt -d cvt_simulatorThrough the configured public hostname, the API health endpoint is:
https://YOUR-HOST/api/v1/health
The backend documentation is available at:
https://YOUR-HOST/docs
Once the database has been initialized:
docker compose up -dThere is no separate migration command to remember. The backend container runs:
alembic upgrade head
before starting Uvicorn on every container start. If there are no new migrations, Alembic simply leaves the schema at the current revision.
Normal production uses:
CVT_TAG=latestThe GitHub Actions container workflow publishes new latest backend and
frontend images whenever develop is pushed.
Watchtower can continue to update those two containers. When a new backend image is recreated, its startup command applies pending database migrations before the API starts.
The PostgreSQL service has this label:
com.centurylinklabs.watchtower.enable=false
so a normal Watchtower configuration ignores the database container. PostgreSQL
is intentionally pinned to the major-version image postgres:17-alpine; update
the database image deliberately rather than as part of an application release.
No Git pull, source build, or server-side repository checkout is involved in normal deployment.
The Containerize workflow supports two publishing paths:
- a push to
developpublisheslatestplus the commit SHA; - a manual workflow run publishes a supplied non-
latesttag plus the commit SHA.
Manual runs cannot overwrite latest.
Once this workflow exists on the repository's default branch, another branch can be tested without merging it:
- Open Actions -> Containerize -> Run workflow.
- Select the branch to build.
- Supply a unique image tag, for example
test-my-feature. - Run the workflow.
GitHub publishes matching backend and frontend images:
ghcr.io/gr812b/cvt-simulator-backend:test-my-feature
ghcr.io/gr812b/cvt-simulator-frontend:test-my-feature
To deploy those images, set:
CVT_TAG=test-my-featurethen run:
docker compose pull cvt-backend cvt-frontend
docker compose up -dWatchtower will subsequently follow that tag while the deployment uses it.
To return to normal production images, set:
CVT_TAG=latestthen run:
docker compose pull cvt-backend cvt-frontend
docker compose up -dIf Watchtower is disabled or you want to update immediately:
cd /opt/cvt-simulator
docker compose pull cvt-backend cvt-frontend
docker compose up -dThe backend startup handles migrations automatically.
Database files live in the Docker named volume:
cvt-postgres-data
Normal container replacement does not delete it.
These are safe with respect to the database volume:
docker compose restart
docker compose down
docker compose up -dDo not use this casually:
docker compose down -v-v deletes the Compose-managed named volume and therefore deletes the
PostgreSQL database.
Create a plain SQL backup:
cd /opt/cvt-simulator
docker compose exec -T postgres \
pg_dump -U cvt -d cvt_simulator \
> "cvt_simulator_$(date +%Y%m%d_%H%M%S).sql"For releases containing meaningful database migrations, making a backup before the new backend image is deployed is recommended.
Stop application traffic first:
docker compose stop cvt-frontend cvt-backendRecreate the database:
docker compose exec -T postgres \
psql -U cvt -d postgres \
-c "DROP DATABASE IF EXISTS cvt_simulator;"
docker compose exec -T postgres \
psql -U cvt -d postgres \
-c "CREATE DATABASE cvt_simulator OWNER cvt;"Restore:
docker compose exec -T postgres \
psql -U cvt -d cvt_simulator \
< your-backup.sqlStart the application again:
docker compose up -dView all services:
docker compose psFollow backend logs:
docker compose logs -f cvt-backendFollow frontend logs:
docker compose logs -f cvt-frontendFollow PostgreSQL logs:
docker compose logs -f postgresRestart only the application:
docker compose restart cvt-backend cvt-frontendSee the current database migration revision:
docker compose exec cvt-backend alembic currentThe production Compose file is intentionally image-only. Local source development remains documented by the component-specific guides:
This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) license.
- Free for use in personal, educational, or non-commercial projects.
- Commercial use requires a separate license. Please contact Kai Arseneau, Cameron Dunn, Travis Wing, or Grace McKenna for commercial licensing.
For more details, see the LICENSE file.