LogisticsX deploys as a set of containers defined by a hand-maintained Docker Compose file under deploy/. The host runs plain docker compose; nginx (on the host) terminates TLS and reverse-proxies each subdomain to a loopback-bound container port.
deploy/
docker-compose.yml # main production stack
docker-compose.dev.yml # local dev infra (Postgres + migrator)
docker-compose.portainer.yml # Portainer (separate stack, one-time manual deploy)
.env.example # template for the DOCKER_ENV secret / .env
Run-ProdMigrator.ps1 # deliberate prod DB migrate+seed (loads .env, typed confirmation)
nginx/logisticsx.conf # host nginx reverse proxy (subdomains -> 127.0.0.1:port)
The main stack contains identity-server, api, admin-portal, tms-portal, customer-portal, and website. PostgreSQL is external (installed on the host or a managed instance) - it is not part of the compose file.
Deployment is handled by the deploy.yml GitHub Actions workflow. Pushing to the prod branch (or running it manually) will:
- Build and push all six images to GHCR.
- Copy
deploy/docker-compose.ymlto~/deploy/logistics/on the VPS. - Write
.envfrom theDOCKER_ENVsecret and appendGITHUB_REPOSITORY+IMAGE_TAG. docker compose pull && docker compose up -d --force-recreate --remove-orphans.
Required GitHub secrets: SSH_HOST, SSH_USER, SSH_KEY, GHCR_PAT, DOCKER_ENV (the full .env contents), FIREBASE_CREDENTIALS_JSON.
Use this on first setup or when deploying outside CI. See VPS Setup first.
mkdir -p ~/deploy/logistics && cd ~/deploy/logistics
# Copy deploy/docker-compose.yml here, then:
cp /path/to/repo/deploy/.env.example .env
nano .env # fill in production values (see Environment Variables)GITHUB_REPOSITORY and IMAGE_TAG are appended automatically by CI; for a manual run, either export them or rely on the compose defaults (suxrobgm/logistics-app + latest).
echo "$GHCR_PAT" | docker login ghcr.io -u <github-user> --password-stdin
docker compose pull
docker compose up -dSee VPS Setup for the nginx copy + certbot steps.
curl -sf http://127.0.0.1:7000/health
curl http://127.0.0.1:7001/.well-known/openid-configuration
docker compose psThis table is the canonical port reference - deploy/docker-compose.yml and deploy/nginx/logisticsx.conf must both agree with it.
| Subdomain | Host port (loopback) | Override variable |
|---|---|---|
api.logisticsx.app |
7000 | API_PORT |
id.logisticsx.app |
7001 | IDENTITY_SERVER_PORT |
admin.logisticsx.app |
7002 | ADMIN_PORTAL_PORT |
tms.logisticsx.app |
7003 | TMS_PORTAL_PORT |
customer.logisticsx.app |
7004 | CUSTOMER_PORTAL_PORT |
logisticsx.app (website) |
7005 | WEBSITE_PORT |
portainer.logisticsx.app |
9000 (separate stack) | - |
All app ports bind to 127.0.0.1, so the containers are reachable only through nginx. The override variables are optional; compose falls back to the defaults above when they are absent from .env. Changing one means editing the matching proxy_pass in the nginx config too.
Migrations are not run automatically in production. Apply them with deploy/Run-ProdMigrator.ps1, which loads deploy/.env, forces the Production environment, shows the target database host and requires you to type migrate-prod before running Logistics.DbMigrator once (--exit). See Environment Variables for the variables it needs.
The migrator also applies the Duende operational store schema to the master DB (Keys, PersistedGrants, ...), where the IdentityServer persists signing keys and refresh tokens so sessions survive redeploys. Run the migrator before deploying an IdentityServer image that expects those tables. The first deploy after introducing this store still logs everyone out once; later redeploys keep sessions alive.
docker compose logs -f # all services
docker compose logs -f api # one service
docker compose restart
docker compose pull && docker compose up -d # update imagesPortainer runs as its own compose project (deploy/docker-compose.portainer.yml) so a main-stack redeploy (--remove-orphans) never removes it. It is deployed once, manually - see VPS Setup. Access is via https://portainer.logisticsx.app (nginx → 127.0.0.1:9000); the port is never exposed publicly.
docker compose ps
docker compose logs api --tail 100
docker compose logs identity-server
sudo nginx -t && sudo tail -f /var/log/nginx/error.log- VPS Setup - initial server configuration
- Environment Variables - full configuration reference