Docker Compose Deployment
September 16, 2026 ยท View on GitHub
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.
Layout
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.
Image access
The api and identity images are public. The four client images (admin-portal, tms-portal, customer-portal, website) are private, because the Angular portals are closed source. Pulling those needs a commercial license and a token with read:packages. See COMMERCIAL-LICENSE.md.
Without a license, run identity-server and api and call the API directly. Remove the four client services from your copy of the compose file, or the pull fails.
Automated deployment (recommended)
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.
Manual deployment
Use this on first setup or when deploying outside CI. See VPS Setup first.
1. Copy the stack to the VPS
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).
2. Log in to GHCR and start
echo "$GHCR_PAT" | docker login ghcr.io -u <github-user> --password-stdin
docker compose pull
docker compose up -d
The token must have read:packages and access to the four private client packages. api and identity pull without it.
3. Configure nginx + SSL
See VPS Setup for the nginx copy + certbot steps.
4. Verify
curl -sf http://127.0.0.1:7000/health
curl http://127.0.0.1:7001/.well-known/openid-configuration
docker compose ps
Subdomains and ports
This 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.
Database migrations
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 identity operational store schema to the master DB (identity.persisted_grants and friends), where IdentityServer persists refresh tokens so sessions survive redeploys. Signing keys live separately in public.signing_keys, also in the master DB. Run the migrator before deploying an IdentityServer image that expects those tables.
Service management
docker compose logs -f # all services
docker compose logs -f api # one service
docker compose restart
docker compose pull && docker compose up -d # update images
Portainer
Portainer 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.
Troubleshooting
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
Next Steps
- VPS Setup - initial server configuration
- Environment Variables - full configuration reference