diff --git a/.gitignore b/.gitignore index 7d98c59..d543316 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ database.sqlite .env .env.* +!.env.production.example +!deploy/.env.dev.example +!deploy/.env.prod.example diff --git a/deploy/.env.dev.example b/deploy/.env.dev.example new file mode 100644 index 0000000..66e0600 --- /dev/null +++ b/deploy/.env.dev.example @@ -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). diff --git a/deploy/.env.prod.example b/deploy/.env.prod.example new file mode 100644 index 0000000..9fc153d --- /dev/null +++ b/deploy/.env.prod.example @@ -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). diff --git a/deploy/Caddyfile.snippet b/deploy/Caddyfile.snippet index bbbc60c..5ac4947 100644 --- a/deploy/Caddyfile.snippet +++ b/deploy/Caddyfile.snippet @@ -1,10 +1,18 @@ # Append to your existing Caddyfile (or include via `import`). -# DNS A/AAAA record for invoice-app.linumiq.com must point to this server first, -# otherwise Caddy will fail to obtain a Let's Encrypt certificate. +# DNS A/AAAA records for both subdomains must point to this server first +# (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 -# container name (the app does not publish a host port). +# DEV — installed on linumiq-dev.myshopify.com +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 { encode zstd gzip - reverse_proxy linumiq-invoice:3000 + reverse_proxy linumiq-invoice-prod:3000 } diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..7fceaf5 --- /dev/null +++ b/deploy/README.md @@ -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. diff --git a/docker-compose.yml b/deploy/docker-compose.dev.yml similarity index 67% rename from docker-compose.yml rename to deploy/docker-compose.dev.yml index 8a8a6c8..8fefe5d 100644 --- a/docker-compose.yml +++ b/deploy/docker-compose.dev.yml @@ -1,20 +1,20 @@ services: app: build: - context: . + context: .. dockerfile: Dockerfile - image: linumiq-invoice:latest - container_name: linumiq-invoice + image: linumiq-invoice:dev + container_name: linumiq-invoice-dev restart: unless-stopped env_file: - - .env.production + - .env.dev 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" NODE_ENV: production PORT: "3000" volumes: - - /docker/linumiq-invoice/data:/data + - /docker/linumiq-invoice/dev/data:/data healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/healthz", "||", "exit", "0"] interval: 30s diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml new file mode 100644 index 0000000..bf5c713 --- /dev/null +++ b/deploy/docker-compose.prod.yml @@ -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 diff --git a/shopify.app.toml b/shopify.app.dev.toml similarity index 80% rename from shopify.app.toml rename to shopify.app.dev.toml index 12c18d0..4b7a1c9 100644 --- a/shopify.app.toml +++ b/shopify.app.dev.toml @@ -1,9 +1,9 @@ # Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration client_id = "fbc263e6cc28e8de031878d2a0f17444" -application_url = "https://invoice-app.linumiq.com" +application_url = "https://invoice-app-dev.linumiq.com" embedded = true -name = "linumiq-invoice" +name = "linumiq-invoice-dev" [access_scopes] # Read orders + customers + companies (B2B) for invoice data. @@ -33,9 +33,9 @@ api_version = "2026-07" [auth] redirect_urls = [ - "https://invoice-app.linumiq.com/auth/callback", - "https://invoice-app.linumiq.com/auth/shopify/callback", - "https://invoice-app.linumiq.com/api/auth/callback", + "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]