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.

Deployment is handled by the deploy.yml GitHub Actions workflow. Pushing to the prod branch (or running it manually) will:

  1. Build and push all six images to GHCR.
  2. Copy deploy/docker-compose.yml to ~/deploy/logistics/ on the VPS.
  3. Write .env from the DOCKER_ENV secret and append GITHUB_REPOSITORY + IMAGE_TAG.
  4. 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.

SubdomainHost port (loopback)Override variable
api.logisticsx.app7000API_PORT
id.logisticsx.app7001IDENTITY_SERVER_PORT
admin.logisticsx.app7002ADMIN_PORTAL_PORT
tms.logisticsx.app7003TMS_PORTAL_PORT
customer.logisticsx.app7004CUSTOMER_PORTAL_PORT
logisticsx.app (website)7005WEBSITE_PORT
portainer.logisticsx.app9000 (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