Deployment
Two independent deployments share the same codebase and Docker image build:
| env | container | backend domain | install target | partner-dashboard app | shopify config |
|---|---|---|---|---|---|
| dev | linumiq-invoice-dev |
invoice-app-dev.linumiq.com |
linumiq-dev.myshopify.com |
linumiq-invoice-dev |
shopify.app.dev.toml |
| prod | linumiq-invoice-prod |
invoice-app.linumiq.com |
5aiizq-ti.myshopify.com (= shop.linumiq.com) |
linumiq-invoice (prod) |
shopify.app.prod.toml |
Server layout (root server)
/docker/linumiq-invoice/
├── git/ # checkout of this repo (git pull here)
├── dev/
│ ├── docker-compose.yml # symlink → ../git/deploy/docker-compose.dev.yml
│ ├── .env.dev # secrets (NOT in git)
│ └── data/ # bind-mounted SQLite + cached assets
└── prod/
├── docker-compose.yml # symlink → ../git/deploy/docker-compose.prod.yml
├── .env.prod # secrets (NOT in git)
└── data/ # bind-mounted SQLite + cached assets
Both containers attach to the external caddy_net Docker network. Caddy reverse-proxies each subdomain to the correct container by name (see Caddyfile.snippet).
First-time setup on the server
sudo mkdir -p /docker/linumiq-invoice/{git,dev/data,prod/data}
sudo chown -R "$USER" /docker/linumiq-invoice
cd /docker/linumiq-invoice
git clone git@git.linumiq.com:LinumIQ/linumiq-invoice.git git
# DEV
cd /docker/linumiq-invoice/dev
ln -s ../git/deploy/docker-compose.dev.yml docker-compose.yml
cp ../git/deploy/.env.dev.example .env.dev # then edit secrets
docker compose up -d --build
# PROD
cd /docker/linumiq-invoice/prod
ln -s ../git/deploy/docker-compose.prod.yml docker-compose.yml
cp ../git/deploy/.env.prod.example .env.prod # then edit secrets
docker compose up -d --build
Append Caddyfile.snippet to your Caddy config and docker exec caddy caddy reload --config /etc/caddy/Caddyfile.
Container runs as a non-root user (uid 1000)
The image runs as the unprivileged node user (uid/gid 1000), not root. The
SQLite database is written to the /data bind mount, so the host directory
mounted at /data (e.g. /docker/linumiq-invoice/dev/data and
…/prod/data) must be writable by uid 1000, otherwise prisma migrate deploy
and DB writes fail on startup:
sudo chown -R 1000:1000 /docker/linumiq-invoice/dev/data
sudo chown -R 1000:1000 /docker/linumiq-invoice/prod/data
The dev container additionally runs with a read-only root filesystem
(read_only: true + tmpfs: /tmp), no-new-privileges, all Linux capabilities
dropped, and memory/pids/cpu limits. The app only writes to the /data bind
mount and the tmpfs /tmp, so this is safe. (The prod compose is intentionally
left unchanged.)
Day-to-day redeploy
cd /docker/linumiq-invoice/git && git pull
cd /docker/linumiq-invoice/dev && docker compose up -d --build # update dev
cd /docker/linumiq-invoice/prod && docker compose up -d --build # update prod
Run only the env you want to update.
Pushing config / extension changes to Shopify
From your dev machine (after git pull to keep configs in sync):
# DEV → linumiq-dev.myshopify.com
npx shopify app config use shopify.app.dev.toml
npx shopify app deploy --allow-updates
# PROD → shop.linumiq.com
npx shopify app config use shopify.app.prod.toml
npx shopify app deploy --allow-updates
The currently selected config is stored in .shopify/project.json (gitignored), so each developer machine remembers its own choice.