Compare commits

..

2 Commits

Author SHA1 Message Date
Gerhard Scheikl e865bc5985 deploy: add shopify.app.prod.toml (linumiq-invoice prod app, client_id c5cb73...) 2026-05-08 21:50:33 +02:00
Gerhard Scheikl 9557a3b335 deploy: split into dev/prod with separate Shopify configs and containers
- shopify.app.toml -> shopify.app.dev.toml (domain: invoice-app-dev.linumiq.com)
- New shopify.app.prod.toml will be created via shopify app config link --config prod
- docker-compose split into deploy/docker-compose.{dev,prod}.yml with distinct
  container names (linumiq-invoice-{dev,prod}), images, env files and bind mounts
- Caddyfile snippet maps both subdomains to their respective containers
- .env.{dev,prod}.example templates committed in deploy/
- deploy/README.md documents the layout and day-to-day workflow
2026-05-08 21:41:22 +02:00
9 changed files with 210 additions and 13 deletions
+3
View File
@@ -19,6 +19,9 @@ database.sqlite
.env .env
.env.* .env.*
!.env.production.example
!deploy/.env.dev.example
!deploy/.env.prod.example
+20
View File
@@ -0,0 +1,20 @@
# DEV environment for linumiq-invoice (custom app installed on linumiq-dev.myshopify.com).
# Copy to `.env.dev` on the server (in /docker/linumiq-invoice/dev/) and fill in real values.
# NEVER commit the real file.
# --- Shopify app credentials ---
# Partner Dashboard → Apps → linumiq-invoice-dev → API credentials.
SHOPIFY_API_KEY=fbc263e6cc28e8de031878d2a0f17444
SHOPIFY_API_SECRET=REPLACE_ME
# Public URL Shopify uses for OAuth, webhooks and admin embedding. Must match shopify.app.dev.toml.
SHOPIFY_APP_URL=https://invoice-app-dev.linumiq.com
# Must match `scopes` in shopify.app.dev.toml.
SCOPES=read_orders,write_orders,read_all_orders,read_customers,read_companies,read_files,write_files
# --- Runtime ---
NODE_ENV=production
PORT=3000
# DATABASE_URL is set in docker-compose.dev.yml (file:/data/prod.sqlite on the bind mount).
+20
View File
@@ -0,0 +1,20 @@
# PROD environment for linumiq-invoice (custom app installed on shop.linumiq.com / 5aiizq-ti.myshopify.com).
# Copy to `.env.prod` on the server (in /docker/linumiq-invoice/prod/) and fill in real values.
# NEVER commit the real file.
# --- Shopify app credentials ---
# Partner Dashboard → Apps → linumiq-invoice (prod) → API credentials.
SHOPIFY_API_KEY=REPLACE_ME
SHOPIFY_API_SECRET=REPLACE_ME
# Public URL Shopify uses for OAuth, webhooks and admin embedding. Must match shopify.app.prod.toml.
SHOPIFY_APP_URL=https://invoice-app.linumiq.com
# Must match `scopes` in shopify.app.prod.toml.
SCOPES=read_orders,write_orders,read_all_orders,read_customers,read_companies,read_files,write_files
# --- Runtime ---
NODE_ENV=production
PORT=3000
# DATABASE_URL is set in docker-compose.prod.yml (file:/data/prod.sqlite on the bind mount).
+13 -5
View File
@@ -1,10 +1,18 @@
# Append to your existing Caddyfile (or include via `import`). # Append to your existing Caddyfile (or include via `import`).
# DNS A/AAAA record for invoice-app.linumiq.com must point to this server first, # DNS A/AAAA records for both subdomains must point to this server first
# otherwise Caddy will fail to obtain a Let's Encrypt certificate. # (a wildcard *.linumiq.com record is sufficient).
#
# Caddy runs in Docker on the `caddy_net` network and reaches each app by
# container name (the apps do not publish host ports).
# Caddy runs in Docker on the `caddy_net` network and reaches the app by # DEV — installed on linumiq-dev.myshopify.com
# container name (the app does not publish a host port). invoice-app-dev.linumiq.com {
encode zstd gzip
reverse_proxy linumiq-invoice-dev:3000
}
# PROD — installed on shop.linumiq.com (5aiizq-ti.myshopify.com)
invoice-app.linumiq.com { invoice-app.linumiq.com {
encode zstd gzip encode zstd gzip
reverse_proxy linumiq-invoice:3000 reverse_proxy linumiq-invoice-prod:3000
} }
+75
View File
@@ -0,0 +1,75 @@
# 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
```bash
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`.
## Day-to-day redeploy
```bash
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):
```bash
# 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.
@@ -1,20 +1,20 @@
services: services:
app: app:
build: build:
context: . context: ..
dockerfile: Dockerfile dockerfile: Dockerfile
image: linumiq-invoice:latest image: linumiq-invoice:dev
container_name: linumiq-invoice container_name: linumiq-invoice-dev
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- .env.production - .env.dev
environment: environment:
# SQLite file lives on a named volume so it survives image rebuilds. # SQLite file lives on a bind mount so it survives image rebuilds.
DATABASE_URL: "file:/data/prod.sqlite" DATABASE_URL: "file:/data/prod.sqlite"
NODE_ENV: production NODE_ENV: production
PORT: "3000" PORT: "3000"
volumes: volumes:
- /docker/linumiq-invoice/data:/data - /docker/linumiq-invoice/dev/data:/data
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/healthz", "||", "exit", "0"] test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/healthz", "||", "exit", "0"]
interval: 30s interval: 30s
+29
View File
@@ -0,0 +1,29 @@
services:
app:
build:
context: ..
dockerfile: Dockerfile
image: linumiq-invoice:prod
container_name: linumiq-invoice-prod
restart: unless-stopped
env_file:
- .env.prod
environment:
# SQLite file lives on a bind mount so it survives image rebuilds.
DATABASE_URL: "file:/data/prod.sqlite"
NODE_ENV: production
PORT: "3000"
volumes:
- /docker/linumiq-invoice/prod/data:/data
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/healthz", "||", "exit", "0"]
interval: 30s
timeout: 5s
retries: 3
networks:
- caddy_net
networks:
caddy_net:
name: caddy_net
external: true
+42
View File
@@ -0,0 +1,42 @@
# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
client_id = "fbc263e6cc28e8de031878d2a0f17444"
application_url = "https://invoice-app-dev.linumiq.com"
embedded = true
name = "linumiq-invoice-dev"
[access_scopes]
# Read orders + customers + companies (B2B) for invoice data.
# read_files / write_files for the generated PDFs uploaded to Shopify Files.
# write_orders required to write the order metafield linking the latest PDF.
# read_all_orders allows access to orders older than 60 days for backfill.
scopes = "read_orders,write_orders,read_all_orders,read_customers,read_companies,read_files,write_files"
[webhooks]
api_version = "2026-07"
[[webhooks.subscriptions]]
uri = "/webhooks/app/uninstalled"
topics = [ "app/uninstalled" ]
[[webhooks.subscriptions]]
uri = "/webhooks/app/scopes_update"
topics = [ "app/scopes_update" ]
[[webhooks.subscriptions]]
uri = "/webhooks/orders/create"
topics = [ "orders/create" ]
[[webhooks.subscriptions]]
uri = "/webhooks/orders/updated"
topics = [ "orders/updated" ]
[auth]
redirect_urls = [
"https://invoice-app-dev.linumiq.com/auth/callback",
"https://invoice-app-dev.linumiq.com/auth/shopify/callback",
"https://invoice-app-dev.linumiq.com/api/auth/callback",
]
[build]
automatically_update_urls_on_dev = true
+2 -2
View File
@@ -1,6 +1,6 @@
# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration # Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
client_id = "fbc263e6cc28e8de031878d2a0f17444" client_id = "c5cb7360d17d3a4643ece1eb5f4ca417"
application_url = "https://invoice-app.linumiq.com" application_url = "https://invoice-app.linumiq.com"
embedded = true embedded = true
name = "linumiq-invoice" name = "linumiq-invoice"
@@ -39,4 +39,4 @@ redirect_urls = [
] ]
[build] [build]
automatically_update_urls_on_dev = true automatically_update_urls_on_dev = false