S-Cart overview

🌐 Language: 🇻🇳 Tiếng Việt · 🇬🇧 English (current)

🐳 S-Cart with Docker

Run S-Cart (GP247 / Laravel 13) with Docker — on your own machine (dev) and on a real server (prod)

⬅️ Back to README · 🐳 Docker Docs · 💬 Facebook group

Introduction

This guide sets up S-Cart with Docker from scratch, for anyone who wants to try it on a local machine or deploy it to a server without installing PHP, Composer or MySQL themselves. After reading it you will be able to run both the dev and prod environments, know the everyday commands, and fix the common problems on your own.

Table of contents

  1. Requirements
  2. What's in the stack
  3. Run the DEV environment
  4. Run the PROD environment
  5. Everyday commands
  6. When to rebuild / restart
  7. Where your data lives
  8. The built-in MySQL
  9. Run several projects on one host
  10. Conditions & rules
  11. Q&A

🧰 Requirements

Component Requirement
Docker Docker Desktop (Windows/Mac) or Docker Engine + Compose plugin (Linux)
Source code This repo on your machine/server: git clone https://github.com/gp247net/s-cart.git
Windows Use WSL2; for the best speed, keep the project inside the WSL2 filesystem (e.g. ~/projects/s-cart) rather than /mnt/c/...

🧱 What's in the stack

Service Role Image
app PHP-FPM running Laravel built from docker/php
webserver Nginx, serves public/, forwards .php to app nginx:1.27-alpine
queue php artisan queue:work (e-mail, background jobs) shares the app image
scheduler Calls php artisan schedule:run every 60 seconds shares the app image
mysql-local MySQL 8.4 (optional, enabled with COMPOSE_PROFILES=db-local) mysql:8.4
node Builds/serves assets (Vite) — runs only when needed node:22-alpine

There are two completely separate compose files — always use the right one for the environment:

docker-compose.yml — DEV docker-compose.prod.yml — PROD
Used on Your local machine A real server
Command docker compose ... docker compose -f docker-compose.prod.yml ...
Default web port 8000 80
APP_ENV / APP_DEBUG local / true (pinned in the compose file) production / false (pinned in the compose file)
app runs as root (avoids bind-mount permission errors) www-data from SC_DOCKER_WWWUSER/SC_DOCKER_WWWGROUP
Xdebug, Vite hot-reload Yes No
Container names scart-app, scart-nginx… scart-app-prod, scart-nginx-prod…

💻 Run the DEV environment

  1. In the project folder, create the config file:

    cp .env.example .env
    

    .env.example already works out of the box (it uses the MySQL bundled in Docker) — nothing to edit.

  2. Start all containers (the first run takes a few minutes to build the image):

    docker compose up -d --build
    

    Check with docker compose ps — the services should be running.

  3. Install S-Cart:

    docker compose exec app php artisan key:generate
    docker compose exec app php artisan gp247:install
    docker compose exec app php artisan gp247:shop-sample   # optional: sample data
    

    gp247:install asks for confirmation — type yes.

  4. Open your browser:

    Page Address
    Storefront http://localhost:8000
    Admin http://localhost:8000/gp247_admin — admin / admin
    Vite dev server (asset hot-reload) http://localhost:5173

    🔑 Change the admin password right after your first login.

💡 To change ports or the database, edit .env before step 2: SC_DOCKER_APP_PORT, SC_DOCKER_DB_PORT, DB_*, COMPOSE_PROFILES. Each variable is explained right in the #========DOCKER========= section of .env.example. Set APP_URL=http://localhost:8000 so links in e-mails (generated by the queue/CLI) carry the right port.


🏭 Run the PROD environment

  1. On the server, create .env from the sample:

    cp .env.example .env
    
  2. Pick one of two database options:

    A. Remote / managed database (RDS, Cloud SQL…) B. MySQL bundled in Docker
    DB_HOST your-remote-mysql-host mysql-local
    COMPOSE_PROFILES (empty) — no mysql-local container is created db-local
    SC_DOCKER_DB_ROOT_PASSWORD not needed required, change it from change_me_root
  3. Edit .env — at least these lines (example for option A):

    APP_ENV=production
    APP_DEBUG=false
    
    DB_CONNECTION=mysql
    DB_HOST=your-remote-mysql-host
    DB_PORT=3306
    DB_DATABASE=scart-db-prod
    DB_USERNAME=scart-user
    DB_PASSWORD=your_real_password   # MUST change from the sample value "password"
    COMPOSE_PROFILES=
    
    SC_DOCKER_WWWUSER=1000           # run `id -u` on the server
    SC_DOCKER_WWWGROUP=1000          # run `id -g` on the server
    

    Don't set SC_DOCKER_APP_PORT unless you need a port other than 80 (e.g. behind a reverse proxy) — the prod compose file defaults to 80.

  4. Start the containers:

    docker compose -f docker-compose.prod.yml up -d --build
    
  5. Install S-Cart (first time only):

    docker compose -f docker-compose.prod.yml exec app php artisan key:generate
    docker compose -f docker-compose.prod.yml exec app php artisan gp247:install
    
  6. Build the CSS/JS assets (they are not baked into the image — run again whenever CSS/JS changes):

    docker compose -f docker-compose.prod.yml run --rm node
    
  7. Open http://your-domain and http://your-domain/gp247_admin to check.

⚠️ Every prod command includes -f docker-compose.prod.yml. Copy the commands as-is, don't type them from memory — without -f, Docker silently uses the DEV file. See Q7.

Shortcut — only valid for the current terminal session:

export COMPOSE_FILE=docker-compose.prod.yml       # bash/zsh
# $env:COMPOSE_FILE = "docker-compose.prod.yml"   # PowerShell

This variable is gone in a new SSH login, a new tab, or in cron/deploy scripts. In scripts and CI, always write the full -f docker-compose.prod.yml.


🔁 Everyday commands

The commands below are written for DEV. On prod, add -f docker-compose.prod.yml after docker compose. Not sure which file your shell targets? Run docker compose config --services.

Update the code after git pull:

git pull
docker compose exec app composer install --no-interaction --optimize-autoloader   # if composer.lock changed
docker compose exec app php artisan gp247:update                                   # if gp247/* packages moved to a new version
docker compose exec app php artisan migrate --force                                # if there are new migrations
docker compose run --rm node                                                       # if assets changed
docker compose exec app php artisan config:cache

Ordinary code changes need no rebuild or restart — see section 6.

Other commands:

Task Command
Run any artisan command docker compose exec app php artisan <command>
PHP / Nginx logs docker compose logs -f app · docker compose logs -f webserver
Laravel log tail -f storage/logs/laravel.log (PowerShell: Get-Content storage\logs\laravel.log -Wait -Tail 50)
Shell into the container docker compose exec app sh
Stop everything (data is kept) docker compose down
Back up the built-in MySQL docker compose exec mysql-local mysqldump -u root -p"$SC_DOCKER_DB_ROOT_PASSWORD" scart > backup.sql
Update PHP dependencies docker compose exec app composer update --no-interaction --optimize-autoloader, then docker compose restart queue scheduler

💡 Set LOG_STACK=daily in .env to rotate the Laravel log daily instead of letting one laravel.log grow forever.


🔨 When to rebuild / restart

Rule of thumb: files copied into the image (by docker/php/Dockerfile) → rebuild after editing. Files that are only mounted → just restart/recreate. Application code → nothing.

Change Rebuild? Restart? Command
docker/php/Dockerfile, php.ini, entrypoint.sh, prod-guard.sh Yes Yes docker compose build app && docker compose up -d
.env: SC_DOCKER_PHP_VERSION, SC_DOCKER_WWWUSER, SC_DOCKER_WWWGROUP Yes Yes docker compose up -d --build
.env: runtime variables (DB_HOST, SC_DOCKER_APP_PORT, SC_DOCKER_DB_PORT, SC_DOCKER_XDEBUG_MODE…) No Yes docker compose up -d
docker-compose*.yml No Yes docker compose up -d
docker/nginx/default.conf No Yes docker compose restart webserver
docker/mysql/my.cnf No Yes docker compose restart mysql-local
PHP / Blade code No No — (re-run config:cache if you use it)
composer.json / composer.lock No No docker compose exec app composer install ...
package.json / JS-CSS assets No No docker compose run --rm node
APP_ENV / APP_DEBUG in .env No No No effect inside the containers — the compose file pins both for app, queue and scheduler. To change them, edit the compose file

Ran php artisan config:cache before? .env changes only take effect after you run config:cache (or config:clear) again.


💾 Where your data lives

The image contains no code — the whole project folder is bind-mounted from your machine into the container (./:/var/www/html). So rebuilding or recreating containers does not lose these folders:

  • app/GP247 — the controllers, helpers, plugins and templates you customized
  • public/GP247, public/vendor, resources/views/vendor
  • storage/app/public — product images, uploaded files

The exceptions live in Docker named volumes (much faster than a bind-mount on Windows):

Content DEV volume PROD volume
vendor/ scart_scart-vendor scart-vendor
node_modules/ scart_scart-node-modules scart-node-modules
MySQL data scart_scart-mysql-local-data-dev scart-mysql-local-data-prod
  • Volumes survive docker compose down; they are only lost with docker compose down -v or docker volume rm.
  • You can't browse vendor/ and node_modules/ from your file explorer — that's expected, you shouldn't edit them by hand. Losing these volumes is harmless: the container reinstalls them on start.
  • See the real volume names with docker volume ls. PROD names are pinned in docker-compose.prod.yml; DEV names carry the scart_ project prefix.

⚠️ Upgrading a prod server that ran an older version? Run docker volume ls before pulling the new docker-compose.prod.yml. If the MySQL volume is named like scart_scart-mysql-local-data-prod (with a prefix), rename/copy it to scart-mysql-local-data-prod, or edit name: in the compose file to match — otherwise MySQL starts on a new, empty volume.

When moving to another server, take the folders listed above with you (via git, rsync, or a separate backup).


🐬 The built-in MySQL

With COMPOSE_PROFILES=db-local, the mysql-local container creates the database and user from .env:

.env variable Becomes DEV default (if unset)
DB_DATABASE MYSQL_DATABASE scart
DB_USERNAME MYSQL_USER scart
DB_PASSWORD MYSQL_PASSWORD scart
SC_DOCKER_DB_ROOT_PASSWORD MYSQL_ROOT_PASSWORD root_secret
  • PROD has no defaults — all 4 variables must be set.
  • Laravel reads the same .env, so the values always match; just use DB_HOST=mysql-local (the service name, not a real hostname).
  • Using HeidiSQL/DBeaver on your machine (DEV only): connect to 127.0.0.1, port SC_DOCKER_DB_PORT (default 3306).

List the databases inside the container:

docker compose exec mysql-local mysql -u root -p"$SC_DOCKER_DB_ROOT_PASSWORD" -e "SHOW DATABASES;"

Add a missing database/user without touching existing data:

docker compose exec mysql-local mysql -u root -p"$SC_DOCKER_DB_ROOT_PASSWORD" -e "CREATE DATABASE IF NOT EXISTS your_db; CREATE USER IF NOT EXISTS 'your_user'@'%' IDENTIFIED BY 'your_pass'; GRANT ALL ON your_db.* TO 'your_user'@'%';"

Full reset — ⚠️ deletes all MySQL data, cannot be undone, back up first:

docker compose down
docker volume ls | grep mysql-local-data            # check the real name before deleting
docker volume rm scart_scart-mysql-local-data-dev   # DEV; PROD: scart-mysql-local-data-prod
docker compose up -d

🏢 Run several projects on one host

Docker tells stacks apart by project name, not by folder. Every container name, image tag, volume and network is derived from one variable, SC_DOCKER_INSTANCE (unset = scart). So every project needs its own SC_DOCKER_INSTANCE — two folders that both leave it empty are the same stack to Docker: up in the second folder takes over the first project's containers and its MySQL database, with no warning at all.

To add a 2nd, 3rd… project:

  1. Put each project in its own folder.

  2. In that project's .env, set its own instance name and host ports:

    SC_DOCKER_INSTANCE=shopa     # unique per project: shopa, shopb…
    SC_DOCKER_APP_PORT=8001      # a different web port per project
    SC_DOCKER_VITE_PORT=5174     # DEV only
    SC_DOCKER_DB_PORT=3307       # DEV only, with COMPOSE_PROFILES=db-local
    
    # KEEP THE SAME in every project:
    DB_HOST=mysql-local
    DB_PORT=3306
    
  3. Start as usual (docker compose up -d --build, add -f docker-compose.prod.yml for prod).

Result with SC_DOCKER_INSTANCE=shopa:

Identifier Default (unset) SC_DOCKER_INSTANCE=shopa
Project (dev / prod) scart / scart-prod shopa / shopa-prod
Containers scart-app, scart-nginx… shopa-app, shopa-nginx…
Image tag (dev / prod) scart-app:8.3 / scart-app:8.3-prod scart-app:8.3-shopa / scart-app:8.3-shopa-prod
DEV volumes scart_scart-vendor, scart_scart-mysql-local-data-dev… shopa_scart-vendor, shopa_scart-mysql-local-data-dev…
PROD volumes scart-vendor, scart-mysql-local-data-prod… shopa-vendor, shopa-mysql-local-data-prod…
Network (dev / prod) scart_scart / scart-prod_scart shopa_scart / shopa-prod_scart

What each variable does when several stacks share a host:

Variable Effect With several stacks
SC_DOCKER_INSTANCE Name of the project, containers, image tag, volumes, network Must differ
SC_DOCKER_APP_PORT Web port on the host (default dev 8000, prod 80) Must differ, or put a reverse proxy in front
SC_DOCKER_VITE_PORT Vite dev server port (DEV only) Must differ when running several dev stacks
SC_DOCKER_DB_PORT Host port of mysql-local for DB tools (DEV only) Must differ when several dev stacks enable db-local
DB_HOST / DB_PORT Where Laravel connects, inside the Docker network Keep mysql-local / 3306 — each stack has its own network
DB_DATABASE / DB_USERNAME / DB_PASSWORD The project's database May repeat if each stack has its own mysql-local; must differ when sharing one MySQL
COMPOSE_PROFILES db-local = own MySQL; empty = external DB Leave empty to let several stacks share one MySQL (saves RAM)
SC_DOCKER_PHP_VERSION PHP version of the image May differ
SC_DOCKER_WWWUSER / SC_DOCKER_WWWGROUP UID/GID of www-data (PROD) Match the owner of each project's folder
  • Several sites in prod → use a reverse proxy (Traefik, Caddy, or Nginx on the host) that publishes 80/443 and routes by domain to each stack; the stacks then don't publish ports themselves, and TLS is handled in one place.
  • Resources: each instance is 4–6 containers, so RAM/CPU grows with the instance count. To fit more sites on a small VPS, point the instances at one shared MySQL (one DB_DATABASE per site, empty COMPOSE_PROFILES=).

🚦 Conditions & rules (know before you act)

When starting PROD

  • A prod container refuses to start while sample secrets remain — with APP_ENV=production, DB_PASSWORD must not be password, and with COMPOSE_PROFILES=db-local, SC_DOCKER_DB_ROOT_PASSWORD must not be change_me_root. A live site running on a password everyone knows is a site waiting to be taken over. The check compares against the sample values only — it never judges the "strength" of a real password, and never blocks DEV.
  • Prod commands must include -f docker-compose.prod.yml — without it Docker doesn't fail, it uses the DEV file (debug on, running as root, Xdebug installed).
  • APP_ENV / APP_DEBUG are pinned in the compose file — editing .env doesn't change them inside the containers; this guarantees prod can never run in debug mode by accident. .env still needs both lines for commands run outside the containers.

When configuring the database

  • The built-in MySQL creates the database/user/root password only on its very first start (empty data volume). Changing DB_* or SC_DOCKER_DB_ROOT_PASSWORD in .env afterwards does not change MySQL — change it inside MySQL (CREATE USER, ALTER USER) or reset the volume.
  • Don't change DB_PORT to avoid port clashes between projects — DB_PORT is the port inside the Docker network, where MySQL always listens on 3306. The host-side port is SC_DOCKER_DB_PORT. (Older instructions said DB_PORT=3307 for a second project — if you followed them: set DB_PORT=3306 back, move 3307 to SC_DOCKER_DB_PORT, then docker compose up -d.)

When setting SC_DOCKER_INSTANCE

  • Lowercase letters, digits, - and _ only, starting with a letter or digit — Docker's project-name rule.
  • Choose it before the first up. To change it later: docker compose down first, then edit .env and up -d. The new stack starts on new, empty volumes — the old data isn't lost, it stays in the volumes with the old name.

When re-running gp247:install

  • The installer overwrites store data and some published files. Back up the database and app/GP247 first if the site already has data or customizations.

❓ Q&A

Q1: Do I have to rebuild the image after git pull?

→ No. The code is bind-mounted into the container, so it takes effect immediately. Rebuild only when you edit files in docker/php/ — see section 6.

Q2: Will rebuilding or removing containers lose my customizations and uploaded images?

→ No. app/GP247, public/GP247, storage/app/public… live on your disk. Only docker compose down -v or docker volume rm deletes MySQL data — see section 7.

Q3: composer install fails with "process timeout" on the first run?

→ Common on Windows when the project lives under /mnt/c/... or /mnt/d/.... Re-run docker compose exec app composer install --no-interaction --optimize-autoloader — it only has to finish once. For a real speed-up, move the project into the WSL2 filesystem (e.g. ~/projects/s-cart).

Q4: On Linux, files created by the container belong to root and need sudo to edit?

→ DEV runs app as root to avoid permission errors. To run it as your user, in the app service of docker-compose.yml set build.args.WWWUSER/WWWGROUP to your id -u/id -g and environment.PHP_FPM_ALLOW_ROOT: "false", then docker compose build app && docker compose up -d. ⚠️ On Windows with the project under /mnt/..., this causes touch(): Utime failed — keep the root default, or move the project into WSL2.

Q5: I deploy prod as root and get Permission denied writing composer.lock / vendor/?

→ www-data in the container runs as UID SC_DOCKER_WWWUSER (default 1000), so it can't write root-owned files. Grant access once with ACLs (replace 1000 if you use another UID):

apt-get install -y acl
cd /path/to/project
setfacl -R  -m u:1000:rwx .
setfacl -R -d -m u:1000:rwx .

Or dedicate a UID 1000 user to deployments and run chown -R 1000:1000 /path/to/project.

Q6: A prod container says [prod-guard] Refusing to start?

→ .env still holds a sample password. See which variable with docker compose -f docker-compose.prod.yml logs app, set a real password (or COMPOSE_PROFILES= if you use a remote DB), then docker compose -f docker-compose.prod.yml up -d. If the MySQL volume was already created with the sample password, also change it inside MySQL with ALTER USER.

Q7: I ran docker compose up -d --build on prod and forgot -f docker-compose.prod.yml?

→ The prod stack is not affected — Docker just starts an extra, separate DEV stack (names without -prod, its own volumes). You may see a port-conflict error rather than anything being overwritten. Stop the accidental DEV stack with docker compose -f docker-compose.yml down, then check prod with docker compose -f docker-compose.prod.yml ps.

Q8: I changed DB_DATABASE/the password in .env but MySQL didn't change?

→ MySQL reads these values only on its first start. Add the database/user with the command in section 8, or reset the volume (loses data).

Q9: Where are the Nginx and PHP-FPM logs?

→ Inside the containers — view them with docker compose logs -f webserver and docker compose logs -f app. To get the Nginx log on your machine, add the volume ./storage/logs/nginx:/var/log/nginx to the webserver service in the compose file.

Q10: How do I use a remote database instead of the built-in MySQL?

→ In .env: DB_HOST=your-remote-mysql-host and COMPOSE_PROFILES= (empty), then docker compose up -d. No mysql-local container is created. See the table in step 2 of PROD.


📅 Last updated: 2026-09-29 · ✍️ Author: GP247