> New to Joinery? The Quick Start guide walks you through renting a server, configuring your domain, and installing Joinery step by step — no prior experience required.
Deploy Joinery on a fresh Ubuntu 24.04 server, either in a Docker container or directly on the host (bare-metal). The same install.sh script handles both — the deployment mode is auto-detected from whether a port is supplied.
Docker:
mkdir -p /tmp/joinery && \
curl -sL https://dev.getjoinery.com/utils/latest_release | tar xz -C /tmp/joinery && \
cd /tmp/joinery/maintenance_scripts/install_tools && \
sudo ./install.sh docker && \
sudo ./install.sh site mysite example.com 8080Bare-metal:
mkdir -p /tmp/joinery && \
curl -sL https://dev.getjoinery.com/utils/latest_release | tar xz -C /tmp/joinery && \
cd /tmp/joinery/maintenance_scripts/install_tools && \
sudo ./install.sh server && \
sudo ./install.sh site mysite example.com# Copy the archive to the target server
scp joinery-X-Y.tar.gz root@YOUR_SERVER:~/
ssh root@YOUR_SERVER
tar -xzf joinery-X-Y.tar.gz
cd maintenance_scripts/install_tools
# One-time host setup
sudo ./install.sh docker # OR: sudo ./install.sh server
# Create your first site (password auto-generated — save it!)
sudo ./install.sh site mysite mysite.com 8080 # Docker (with port)
sudo ./install.sh site mysite mysite.com # Bare-metal (no port)The presence of a port signals Docker mode; omitting it signals bare-metal. To force either explicitly, use --docker or --bare-metal. The resolved mode is recorded in the site's Globalvars_site.php as deployment_environment (docker or baremetal) — the single source of truth the platform reads instead of probing for a container at runtime.
The joinery-X-Y.tar.gz archive contains:
public_html/ — application codeconfig/ — configuration templatesmaintenance_scripts/install_tools/ — installer, Dockerfile, defaultsmaintenance_scripts/sysadmin_tools/ — backup, restore, maintenance utilitiesNever use weak or example passwords in production. Auto-generation is the recommended path.
Omit the password and the installer generates a 24-character secure password, then displays it once at the end of installation:
sudo ./install.sh site mysite mysite.com 8080
# Output: "Auto-generated secure password: xK9mN2pQ7rT4vW8yB3cF6hJ1"Save the password immediately — it's also written to the site's Globalvars_site.php.
Use --password-file to avoid shell-escaping issues:
echo 'YourStr0ng&Secure#Pass@9' > /tmp/dbpass.txt
sudo ./install.sh site mysite --password-file=/tmp/dbpass.txt mysite.com 8080
rm /tmp/dbpass.txtShell and sed escaping forbid these characters in the database password:
| Character | Reason |
|---|---|
' | Breaks PHP string literals |
" | Breaks shell double-quoted strings |
\ | Escape character in shell, sed, and PHP |
$ | Variable expansion in shell |
` `` | Command substitution in shell |
! | History expansion in bash |
| newlines | Break sed replacement patterns |
@ # % ^ * ( ) - _ + = { } [ ] | : ; < > , . ? ~ / &sudo ./install.sh -y docker
sudo ./install.sh -y -q site mysite mysite.com 8080-y accepts all prompts; -q suppresses progress output.
sudo ./install.sh dockerChecks for Docker, installs Docker CE if missing, starts the daemon, verifies it's operational.
sudo ./install.sh site SITENAME [DOMAIN_NAME] [PORT] [OPTIONS]| Parameter | Required | Default | Notes |
|---|---|---|---|
SITENAME | Yes | — | Site & database name (e.g., mysite) |
DOMAIN_NAME | No | Server IP | Domain for VirtualHost |
PORT | No | 8080 | Host port for web traffic |
--themes).Each site needs unique ports. The installer detects conflicts and suggests the next pair:
| Site | Web port | DB port |
|---|---|---|
| site1 | 8080 | 9080 |
| site2 | 8081 | 9081 |
| site3 | 8082 | 9082 |
| Volume | Container path | Purpose |
|---|---|---|
{site}_postgres | /var/lib/postgresql | Database files |
{site}_uploads | .../uploads | User uploads |
{site}_config | .../config | Site configuration |
{site}_backups | .../backups | Database backups |
{site}_static | .../static_files | Generated files |
{site}_logs | .../logs | Application logs |
{site}_cache | .../cache | Runtime cache |
{site}_sessions | /var/lib/php/sessions | PHP sessions |
{site}_apache_logs | /var/log/apache2 | Apache logs |
{site}_pg_logs | /var/log/postgresql | PostgreSQL logs |
sudo ./install.sh serverInstalls and configures PHP 8.3, Apache (with mod_rewrite), PostgreSQL, Composer, Certbot, UFW, fail2ban, SSH hardening, and unattended security updates.
sudo ./install.sh site SITENAME DOMAIN_NAME [OPTIONS]Common options:
--activate THEME — activate a specific theme after install--with-test-site — create a companion test site (bare-metal only)/var/www/html/{sitename}/._site_init.sh to create directories, configure Globalvars_site.php, create the database, load the schema, install Composer deps, and create the Apache VirtualHost./var/www/html/{sitename}/
├── public_html/ # Application code
├── config/ # Site configuration
├── uploads/ # User uploads
├── logs/ # Application logs
├── static_files/ # Generated files
└── backups/ # Database backupsSSL is configured automatically when a domain (not localhost or an IP) is provided.
install.sh server).Certbot configures Apache directly:
sudo ./install.sh site mysite mysite.example.comThe installer adds Apache on the host (if not present), creates a reverse proxy mysite.example.com → localhost:8080, then runs Certbot against the proxy:
sudo ./install.sh site mysite mysite.example.com 8080sudo ./install.sh site mysite mysite.example.com --no-ssl# Bare-metal
sudo certbot --apache -d mysite.example.com
# Docker (after the host proxy exists)
sudo certbot --apache -d mysite.example.comThe installer detects domains behind Cloudflare's proxy (orange cloud) by matching the resolved IP against Cloudflare's IP ranges, and adapts:
| Mode | Browser ↔ Cloudflare | Cloudflare ↔ Origin | Origin cert |
|---|---|---|---|
| Flexible | HTTPS | HTTP | None required |
| Full | HTTPS | HTTPS (any cert) | Self-signed OK |
| Full (Strict) | HTTPS | HTTPS (valid cert) | Cloudflare Origin Certificate |
By default, fresh installs include only the core application. Use --themes to download stock themes and plugins from the upgrade server during site creation:
sudo ./install.sh site mysite mysite.com 8080 --themesTo download themes and plugins after the site exists, use upgrade.php:
# Docker
docker exec mysite php /var/www/html/mysite/public_html/utils/upgrade.php
# Bare-metal
php /var/www/html/mysite/public_html/utils/upgrade.phpThe --themes flag uses the same distribution system as upgrade.php. See Deploy and Upgrade for the upgrade pipeline.
Clone an existing site — database, uploads, settings — to a new server. The target machine pulls from the source.
INSERT INTO stg_settings (stg_name, stg_value)
VALUES ('clone_export_key', 'YourSecureRandomKey123');
-- When done:
DELETE FROM stg_settings WHERE stg_name = 'clone_export_key';Use a strong random key (32+ chars). HTTPS is required. Rotate or remove the key after cloning. Clone requests are logged on the source.
# Docker
sudo ./install.sh site newsite newdomain.com 8080 \
--clone-from=https://sourcesite.com \
--clone-key=YourSecureRandomKey123
# Bare-metal
sudo ./install.sh site newsite newdomain.com \
--clone-from=https://sourcesite.com \
--clone-key=YourSecureRandomKey123| Item | Behavior |
|---|---|
| Database (all tables) | Exact copy from source |
| All settings | Exact copy from source |
site_url setting | Updated to the target domain |
| Uploads directory | Exact copy from source |
| User accounts | Preserved from source |
clone_export_key | Removed on the new site |
Globalvars_site.php | Regenerated with new DB credentials |
| Themes & plugins | Downloaded from the source site |
-y).Use manage_domain.sh (in maintenance_scripts/sysadmin_tools/) to add, change, or remove domains on existing sites. Works for both Docker and bare-metal.
cd maintenance_scripts/sysadmin_tools
# Current state
sudo ./manage_domain.sh status mysite
# Assign a domain (with SSL via Let's Encrypt unless Cloudflare detected)
sudo ./manage_domain.sh set mysite example.com
# Without SSL (e.g. Cloudflare-proxied or testing)
sudo ./manage_domain.sh set mysite example.com --no-ssl
# Revert to IP-only access
sudo ./manage_domain.sh clear mysite
# Restore the previous configuration
sudo ./manage_domain.sh rollback mysite
# Remove SSL only, keep the domain
sudo ./manage_domain.sh remove-ssl mysiteFor Docker sites, set creates an Apache reverse proxy on the host and disables 000-default.conf so bare-IP requests don't fall through to Ubuntu's welcome page.
docker stop mysite
docker start mysite
docker restart mysite
docker ps --filter "name=mysite"# Docker
docker logs mysite # Startup
docker logs -f mysite # Follow
docker logs --tail 100 mysite # Last 100
docker exec mysite tail -100 /var/www/html/mysite/logs/error.log
# Bare-metal
tail -f /var/www/html/mysite/logs/error.log
tail -f /var/log/apache2/access.log# Docker
docker exec -it mysite bash
# Bare-metal — just use the host shell
cd /var/www/html/mysite/In Docker, never service apache2 restart — it kills the container. Use reload or graceful:
docker exec mysite service apache2 reload
docker exec mysite apache2ctl graceful
docker exec mysite apache2ctl configtestBare-metal:
sudo systemctl reload apache2
sudo apache2ctl configtest# Docker
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" mysite \
psql -h 127.0.0.1 -U postgres -d mysite
# Bare-metal
psql -U postgres -d mysite# Backup (Docker)
docker exec mysite pg_dump -U postgres mysite | gzip > backup.sql.gz
# Backup (bare-metal)
./maintenance_scripts/sysadmin_tools/backup_database.sh mysite
# Restore (Docker)
gunzip -c backup.sql.gz | docker exec -i mysite psql -U postgres -d mysite
# Restore (bare-metal)
./maintenance_scripts/sysadmin_tools/restore_database.sh mysite backup.sqlDocker — stop and re-create the container; volumes persist:
docker stop mysite && docker rm mysite
tar -xzf joinery-NEW-VERSION.tar.gz
cd maintenance_scripts/install_tools
sudo ./install.sh site mysite mysite.com 8080The container detects this isn't a fresh install and skips initial setup.
Bare-metal — use upgrade.php:
php /var/www/html/mysite/public_html/utils/upgrade.phpFor more detail on the upgrade pipeline, see Deploy and Upgrade.
# Docker
docker exec mysite php /var/www/html/mysite/public_html/utils/update_database.php
# Bare-metal
php /var/www/html/mysite/public_html/utils/update_database.phpremove_account.sh detects whether the site is Docker or bare-metal and handles both:
sudo ./maintenance_scripts/sysadmin_tools/remove_account.sh mysite
sudo ./maintenance_scripts/sysadmin_tools/remove_account.sh mysite -y # No prompt| Docker sites | Bare-metal sites |
|---|---|
| Docker container | Website directories |
| All Docker volumes (postgres, uploads, etc.) | Test site directories |
| Docker image | Apache VirtualHost |
| Build directory | PostgreSQL database |
docker logs mysiteCommon causes: port already in use (the installer normally detects this and offers alternatives), volume permission issues, or out of disk space.
The container's CMD should bring services up automatically. If not:
docker exec mysite service postgresql start
docker exec mysite service apache2 startsudo chown -R www-data:user1 /var/www/html/mysite
sudo chmod -R 775 /var/www/html/mysite
# Or:
./fix_permissions.sh mysite --productionAlmost always a syntax or escaping error.
joinery-install.sql.gz for SQL syntax.pg_hba.conf settings, authentication method, and database user permissions are not the cause — the installer handles all of those.Debugging:
docker logs mysite 2>&1 | grep -i "error\|fail"
docker exec -it mysite bash
su postgres -c "psql -d mysite -c '\\dt'"The composerAutoLoad setting was copied from the source and points to an invalid absolute path. Set it back to the portable relative path:
# Docker
docker exec -it mysite bash
PGPASSWORD='your_db_password' psql -U postgres -d mysite \
-c "UPDATE stg_settings SET stg_value = '../vendor/' WHERE stg_name = 'composerAutoLoad';"
# Bare-metal
sudo -u postgres psql -d mysite \
-c "UPDATE stg_settings SET stg_value = '../vendor/' WHERE stg_name = 'composerAutoLoad';"If the chosen port is in use, the installer shows existing Joinery containers and suggests the next available port pair, then prompts you to accept.
install.sh| Subcommand | Purpose |
|---|---|
install.sh docker | Install Docker (one-time) |
install.sh server | Set up bare-metal host (one-time) |
install.sh site … | Create a new Joinery site |
install.sh list | List existing sites |
| Flag | Description |
|---|---|
-y, --yes | Auto-accept all prompts (non-interactive) |
-q, --quiet | Suppress progress output; show errors and final summary |
install.sh site options:install.sh [-y] [-q] site [--docker|--bare-metal] SITENAME [DOMAIN] [PORT] [OPTIONS]
--password-file=FILE Read database password from file (recommended)
--activate THEME Activate this theme after install
--with-test-site Create a companion test site (bare-metal only)
--themes Download stock themes/plugins from upgrade server
--no-ssl Skip automatic SSL setup
--clone-from=URL Clone DB + uploads from an existing site
--clone-key=KEY Authentication key for clone sourceIf no password is given (and no --password-file), the installer auto-generates a 24-character password.
| Script | Purpose | Called by |
|---|---|---|
_site_init.sh | Internal site initialization (DB, config, Composer) | install.sh site, Dockerfile CMD |
fix_permissions.sh | Sets ownership and permissions on site files | _site_init.sh, manual |
Dockerfile.template | Template for building Docker images | install.sh site (Docker) |
default_Globalvars_site.php | Template for site configuration | _site_init.sh |
default_virtualhost.conf | Template for Apache VirtualHost | _site_init.sh |
_site_init.sh is internal — don't invoke it directly. Use install.sh site.Located in maintenance_scripts/sysadmin_tools/:
| Script | Purpose |
|---|---|
manage_domain.sh | Domain management: set, clear, status, rollback, remove-ssl |
backup_database.sh | Backup PostgreSQL database |
restore_database.sh | Restore PostgreSQL database |
backup_project.sh | Full site backup (files + database) |
restore_project.sh | Full site restore |
copy_database.sh | Copy database between sites |
remove_account.sh | Remove a site completely |
For multiple Docker sites sharing standard ports, install Apache on the host:
apt-get install -y apache2
a2enmod proxy proxy_http headers ssl rewrite
systemctl restart apache2Create /etc/apache2/sites-available/yoursite.conf:
<VirtualHost *:80>
ServerName yoursite.com
ServerAlias www.yoursite.com
ProxyPreserveHost On
ProxyRequests Off
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
RequestHeader set X-Real-IP %{REMOTE_ADDR}s
RequestHeader set X-Forwarded-For %{REMOTE_ADDR}s
RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>Enable and add SSL:
a2ensite yoursite
systemctl reload apache2
apt-get install -y certbot python3-certbot-apache
certbot --apache -d yoursite.com -d www.yoursite.comupgrade.php